📢博客主页:https://blog.csdn.net/2301_779549673
📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨
一篇文章中们讲解了socket编程的基本知识,这篇设计一个基于UDP协议的网络编程代码,能够简单的回显服务器和客户端代码!!!
我们要做到的是使 服务端 和 客户端 能够通信,所以需要分别实现两边的代码
下面的文章会使用到前面的
日期类
和互斥类
,需要的可去前面的文章中取,或者仓库中拉
🏳️🌈一、服务端
主函数通过智能指针构造Server类,并初始化和启动服务!
1.1 主函数逻辑
#include "UdpServer.hpp"
int main()
{
ENABLE_CONSOLE_LOG(); // 日期类方法,使日志在控制台输出
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(); // C++14标准
usvr->InitServer(); // 初始化服务端
usvr->Start(); // 启动服务端
return 0;
}
1.2 UdpServer 类
错误类型
我们需要列举可能会出错的情况,可以将这部分放置到 common.hpp
文件下,使其变得通用
enum {
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR
};
全局变量
UdpServer
类成员变量需要文件描述符,IP,端口,运行状态;初始化函数创建socket套接字,并将套接字进行绑定;启动函数收客户端的消息并回复客户端!
const static int gsockfd = -1;
const static std::string gdefaultip = "127.0.0.1"; // 表示本地主机
const static uint16_t gdefaultport = 8080;
基本结构
class UdpServer{
public:
UdpServer(uint16_t localport = gdefaultport);
void InitServer(){}
void Start(){}
~UdpServer(){}
private:
int _sockfd; // 文件描述符
uint16_t _localport; // 端口号
std::string _localip; // 本地IP地址
bool _isrunning; // 运行状态
};
构造函数、析构函数
构造函数
需要初始化 文件描述符、端口号、本地ip地址以及运行状态析构函数
只需要在确保有文件的前提下,关闭文件
UdpServer(uint16_t localport = gdefaultport)
: _sockfd(gsockfd), _localport(localport), _isrunning(false) {}
~UdpServer() {
// 判断 _sockfd 是否是一个有效的套接字文件描述符
// 有效的文件描述符(如套接字、打开的文件等)是非负整数(>= 0)
if (_sockfd > -1)
::close(_sockfd);
}
禁止拷贝类
为了防止Server类被拷贝,此处可以设计一个防止拷贝和赋值的类,并让Server类继承
class nocopy{
public:
nocopy(){}
~nocopy(){}
nocopy(const nocopy&) = delete; // 禁止拷贝构造函数
const nocopy& operator=(const nocopy&) = delete; // 禁止拷贝赋值运算符
};
初始化函数 - InitServer() - 套接字描述符
- 初始化函数创建socket套接字,并将套接字进行绑定;
- 用
socket
创建一个 IPv4 的 UDP 套接字 - 创建成功的话,会返回一个非负整数的套接字描述符
- 且这个描述符,不应该是 0,1,2(标准输入、标准输出、标准错误)
void InitServer() {
// 创建套接字
// socket(int domain, int type, int protocol)
// 返回一个新的套接字文件描述符,或者在出错时返回-1
// 参数domain:协议族,AF_INET,表示IPv4协议族
// 参数type:套接字类型,SOCK_DGRAM,表示UDP套接字
// 参数protocol:协议,0,表示默认协议
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
// exit(SOCKET_ERR) 表示程序运行失败,并返回指定的错误码
exit(SOCKET_ERR);
}
LOG(LogLevel::DEBUG) << "socket success, sockfd is: " << _sockfd;
}
开始函数 - Start() 这部分后面在实现,现在先为空,看看目前为止是否正确
和预想中的一样,最终套接字描述符是3
初始化函数 - InitServer() - 套接字绑定
即将
套接字描述符
与网络序列的端口号和IP
进行绑定
- 首先我们需要将当前的 端口号 和 ip 变成网络字节序
struct sockaddr_in local;
// 将local全部置零,以便后面设置
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; // IPv4协议族
local.sin_port = htons(_localport); // 端口号,网络字节序
local.sin_addr.s_addr = htonl(INADDR_ANY); // 本地IP地址,网络字节序
- 然后再使用
bind
将套接字描述符绑定到本地地址
void InitServer() {
// 1. 创建套接字
// socket(int domain, int type, int protocol)
// 返回一个新的套接字文件描述符,或者在出错时返回-1
// 参数domain:协议族,AF_INET,表示IPv4协议族
// 参数type:套接字类型,SOCK_DGRAM,表示UDP套接字
// 参数protocol:协议,0,表示默认协议
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
// exit(SOCKET_ERR) 表示程序运行失败,并返回指定的错误码
exit(SOCKET_ERR);
}
LOG(LogLevel::DEBUG) << "socket success, sockfd is: " << _sockfd;
// 2. bind
// sockaddr_in
struct sockaddr_in local;
// 将local全部置零,以便后面设置
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; // IPv4协议族
local.sin_port = htons(_localport); // 端口号,网络字节序
local.sin_addr.s_addr = htonl(INADDR_ANY); // 本地IP地址,网络字节序
// 将套接字绑定到本地地址
// bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen)
// 绑定一个套接字到一个地址,使得套接字可以接收来自该地址的数据报
// 参数sockfd:套接字文件描述符
// 参数addr:指向sockaddr_in结构体的指针,表示要绑定的地址
// 参数addrlen:地址长度,即sizeof(sockaddr_in)
// 返回0表示成功,-1表示出错
int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if (n < 0) {
LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
exit(BIND_ERR);
}
LOG(LogLevel::DEBUG) << "bind success\n";
}
开始函数 - Start() - 接收消息
我们需要利用
recvfrom()
从套接字中接收数据
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
-
参数
sockfd
:已经创建并绑定的套接字的文件描述符。buf
:指向用于存储接收到的数据的缓冲区的指针。len
:缓冲区的大小,以字节为单位。flags
:接收操作的标志(此处设置为0即可),用于修改recvfrom的行为。常用的标志包括MSG_DONTWAIT(非阻塞模式)和MSG_WAITALL(阻塞模式,直到接收到指定大小的数据)。src_addr
:指向sockaddr结构体的指针,用于存储发送方的地址信息。addrlen
:指向整型的指针,用于指定src_addr结构体的大小,并在调用后被设置为新接收到的地址的实际大小。
-
返回值
- recvfrom
成功时
返回接收到的字节数 失败时
返回-1,并设置全局变量errno来指示错误的原因。
- recvfrom
这里接收客户端的地址信息是需要一个指向sockaddr结构体的指针,可以这样子封装一下
#define CONV(v) (struct sockaddr *)(v)
开始函数 - Start() - 发送消息
我们需要利用
recvfrom()
从套接字中接收数据
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
-
参数
sockfd
:套接字描述符,是通过 socket 函数创建的套接字文件描述符。buf
:指向要发送数据的缓冲区。len
:要发送数据的字节数。flags
:用于控制发送行为的标志位,通常设置为 0,但也可以使用以下选项之一或多个(使用按位或运算符 | 组合):- MSG_CONFIRM:请求确认消息数据已被发送(适用于某些特定协议)。
- MSG_DONTROUTE:绕过路由表,直接发送数据(仅适用于某些协议)。
- MSG_EOR:表示数据记录的结束(对于某些流协议可能有用)。
- MSG_MORE:指示后续将发送更多数据(对于某些协议,可能会优化发送)。
- MSG_NOSIGNAL:防止发送过程中产生 SIGPIPE 信号(如果连接已经关闭)。
dest_addr
:指向目标地址的指针,通常是一个 struct sockaddr_in(用于 IPv4)或 struct sockaddr_in6(用于 IPv6)结构体。addrlen
:目标地址的长度,通常是 sizeof(struct sockaddr_in) 或 sizeof(struct sockaddr_in6)。
-
返回值
成功时
,返回发送的字节数。失败时
,返回 -1,并设置 errno 以指示错误类型。
开始函数 - Start()
- 作为接收的
peer
,传过来的时候是网络字节序,我们需要转换为主机字节序后,才能答应出来
void Start() {
_isrunning = true;
while (true) {
char inbuffer[1024]; // 接收缓冲区
struct sockaddr_in peer; // 接收客户端地址
socklen_t peerlen = sizeof(peer); // 计算接收的客户端地址长度
// 接收数据报
// recvfrom(int sockfd, void* buf, size_t len, int flags, struct
// sockaddr* src_addr, socklen_t* addrlen)
// 从套接字接收数据,并存入buf指向的缓冲区中,返回实际接收的字节数
// 参数sockfd:套接字文件描述符
// 参数buf:指向接收缓冲区的指针
// 参数len:接收缓冲区的长度
// 参数flags:接收标志,一般设为0
// 参数src_addr:指向客户端地址的指针,若不为NULL,函数返回时,该指针指向客户端的地址,是网络字节序
// 参数addrlen:客户端地址长度的指针,若不为NULL,函数返回时,该指针指向实际的客户端地址长度
ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0,
CONV(&peer), &peerlen);
if (n > 0) {
// 接收到的peer是网络字节序,需要转换成主机字节序
uint16_t clientport =
::ntohs(peer.sin_port); // 客户端端口号,网络字节序
std::string clientip =
inet_ntoa(peer.sin_addr); // 客户端IP地址,字符串形式
inbuffer[n] = 0; // 字符串结尾
std::string clientinfo =
clientip + ":" + std::to_string(clientport) + " # " + inbuffer;
LOG(LogLevel::DEBUG) << clientinfo;
std::string echo_string = "server echo: ";
echo_string += inbuffer;
// 发送数据报
// sendto(int sockfd, const void* buf, size_t len, int flags, const
// struct sockaddr* dest_addr, socklen_t addrlen)
// 发送数据报,buf指向发送缓冲区,len为发送缓冲区的长度
// 参数sockfd:套接字文件描述符
// 参数buf:指向发送缓冲区的指针
// 参数len:发送缓冲区的长度
// 参数flags:发送标志,一般设为0
// 参数dest_addr:指向目的地址的指针,表示要发送到的地址
// 参数addrlen:目的地址长度,即sizeof(sockaddr_in)
::sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0,
CONV(&peer), peerlen);
}
}
}
InetAddr 类
- 针对上面需要将网络字节序转换为主机字节序的这个需求,我们可以构造一个
InetAddr
类,封装相应的方法
class InetAddr
{
private:
void PortNet2Host()
{
_port = ::ntohs(_net_addr.sin_port);
}
void IpNet2Host()
{
char ipbuffer[64];
const char *ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
(void)ip;
}
public:
InetAddr(){}
InetAddr(const struct sockaddr_in &addr) : _net_addr(addr)
{
PortNet2Host();
IpNet2Host();
}
std::string Ip() { return _ip; }
uint16_t Port() { return _port; }
~InetAddr(){}
private:
struct sockaddr_in _net_addr;
std::string _ip;
uint16_t _port;
};
因此 start
这部分代码可以这样改一下
// 接收到的peer是网络字节序,需要转换成主机字节序
// uint16_t clientport = ::ntohs(peer.sin_port); // 客户端端口号,网络字节序
// std::string clientip = inet_ntoa(peer.sin_addr); // 客户端IP地址,字符串形式
InetAddr cli(peer);
inbuffer[n] = 0; // 字符串结尾
// std::string clientinfo = clientip + ":" + std::to_string(clientport) + " # " + inbuffer;
std::string clientinfo = cli.Ip() + ":" + std::to_string(cli.Port()) + " # " + inbuffer;
🏳️🌈二、客户端
我们向服务器发送消息需要知道服务器的端口和IP,就比如你要买一个东西,总得知道那东西是啥
2.1 主要流程
- 读取接收端IP和端口
- 创建套接字
- 设置接收端信息
- 发消息和接收消息
- 关闭套接字
2.2 实现
1、client 需要bind它自己的IP和端口,但是client 不需要 “显示” bind它自己的IP和端口
2、client 在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口
#include "UdpClient.hpp"
int main(int argc, char* argv[]){
if(argc != 3){
std::cerr << argv[0] << " serverip server" << std::endl;
Die(USAGE_ERR);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1. 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0){
std::cerr << "create socket error" << std::endl;
Die(SOCKET_ERR);
}
// 1. 填充 server 信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = ::htons(serverport);
server.sin_addr.s_addr = ::inet_addr(serverip.c_str());
// 2. 发送数据
while(true){
std::cout << "Please Enter# ";
std::string msg;
std::getline(std::cin, msg);
// client 必须自己的ip和端口。但是客户端,不需要显示调用bind
// 客户端首次 sendto 消息的时候,由OS自动bind
// 1. 如何理解 client 自动随机bind端口号? 一个端口号,只能读一个进程bind
// 2. 如何理解 server 要显示地bind? 必须稳定!必须是众所周知且不能轻易改变的
int n = ::sendto(sockfd, msg.c_str(), msg.size(), 0, CONV(&server), sizeof(server));
(void)n;
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
n = ::recvfrom(sockfd, buffer,sizeof(buffer) - 1, 0, CONV(&temp), &len);
if(n > 0){
buffer[n] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
👥总结
本篇博文对 【Linux网络】简单UDP协议编程代码 做了一个较为详细的介绍,不知道对你有没有帮助呢
觉得博主写得还不错的三连支持下吧!会继续努力的~