当前位置: 首页 > news >正文

Linux中的UDP编程接口基本使用

UDP编程接口基本使用

本篇介绍

在前面网络基础部分已经介绍了网络的基本工作模式,有了这些理论基础之后,下面先从UDP编程开始从操作部分深入网络

在本篇中,主要考虑下面的内容:

  1. 创建并封装服务端:了解创建服务端的基本步骤
  2. 创建并封装客户端,测试客户端和服务端通信:了解创建客户端的基本步骤和二者通信
  3. 测试云服务器与本地进行通信:从本地通信到真正实现网络通信

根据上面的内容,本次设计的服务器功能就是接受客户端发送的信息并向客户端返回服务端收到的信息

创建并封装服务端

创建服务器类

因为需要对服务器进行封装,所以首先创建服务器类的基本框架,本次设计的服务器一旦启动就不再关闭,除非手动关闭,所以可以提供两个接口:

  1. start:启动服务器接口
  2. stop:停止服务器接口

所以基本框架如下:

class UdpServer
{
public:UdpServer(){}// 启动服务器void start(){}// 停止服务器void stop(){}~UdpServer(){}private:
};

创建服务器套接字

既然要创建服务器,首先就是对服务器的相关信息进行设置。首先需要创建socket文件描述,可以使用socket接口,该接口原型如下:

int socket(int domain, int type, int protocol);

该接口的第一个参数表示网络协议家族,可以选择的选择选项有很多,其中包括AF_UNIXAF_INET,因为本次是网络通信,所以该参数选择AF_INET,第二个参数表示协议类型,在网络通信部分分为两种:TCP和UDP,对应的值分别为SOCK_STREAMSOCK_DGRAM,因为本次是UDP,所以选择SOCK_DGRM

根据Linux操作手册的描述:

  1. SOCK_STREAM:Provides sequenced, reliable, two-way, connection-based byte streams(提供序列化的、可靠的、双工的、面向有连接的字节流)
  2. SOCK_DGRAM:Supports datagrams (connectionless, unreliable messages of a fixed maximum length)(支持数据包,即无连接、不可靠的固定长度信息)

全双工、半双工和单工是描述通信双方在数据传输时的交互模式,具体对比如下:

  1. 全双工(Full Duplex):双方可以同时互相发送和接收数据,就像电话通话两端都能同时说话和听对方
  2. 半双工(Half Duplex):双方均可发送和接收数据,但同一时间只能有一方传输。例如,对讲机通信时,一旦你在讲话,另一方必须等待直到你停止后才能回应
  3. 单工(Simplex):数据只能单向传输,通信只有一端发送,而另一端只接收。例如,广播视频信号中,电视台只能发送信号,观众只能接收信号

第三个参数表示指定采用的具体协议。通常传入0表示让系统自动选择适合domaintype参数的默认协议

该接口返回值为一个新套接字的文件描述符,否则返回-1并设置错误码

根据这个接口的描述可以知道当前服务器类需要一个成员_socketfd用于接收socket的返回值,代码如下:

class UdpServer
{
public:UdpServer(): _socketfd(-1){// 创建服务器套接字_socketfd = socket(AF_INET, SOCK_DGRAM, 0);}// ...private:int _socketfd; // 套接字文件描述符
};

在创建服务器套接字失败时可以考虑使用日志系统显示相关的信息,一旦服务器创建异常,说明此时服务器无法正常创建,可以直接退出函数,为了保证可读性,可以将错误码定义为宏,代码如下:

// 错误码枚举类
enum class errorNumber
{ServerSocketFail = 1, // 创建套接字失败
};class UdpServer
{
public:UdpServer(): _socketfd(-1){// 创建服务器套接字_socketfd = socket(AF_INET, SOCK_DGRAM, 0);if (_socketfd < 0){LOG(LogLevel::FATAL) << "服务器启动异常:" << strerror(errno);exit(static_cast<int>(errorNumber::ServerSocketFail));}LOG(LogLevel::INFO) << "服务器启动成功:" << _socketfd;}// ...private:int _socketfd; // 套接字文件描述符
};

绑定服务器IP地址和端口

前面的过程只是创建了一个可以写入的位置,socket接口可以类比文件部分的open接口,在网络部分接下来的步骤并不是写入,而应该是绑定端口和IP地址,确保其他计算机可以找到当前服务器和具体进程。在Linux中,绑定可以使用bind接口,该接口原型如下:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

该接口的第一个参数表示需要绑定的套接字对应的文件描述符,第二个参数表示套接字结构,第三个参数表示套接字结构的大小

如果绑定成功,该接口返回0,否则返回-1并设置错误码

对于第一个参数和第三个参数来说,二者作用和含义明显,此处不作过多介绍,下面就第二个参数详细介绍:

在[Socket编程基础]部分提到sockaddr可以理解为sockaddr_in结构和sockaddr_un的父类,而因为本次创建的是网络通信,所以要使用的结构就是sockaddr_in,既然参数部分是sockaddr结构而不是sockaddr_in,那么在传递实参时就需要进行强制类型转换

那么,既然需要用户传递sockaddr_in结构,那么这个结构中就存在一些属性需要用户去设置。在[Socket编程基础]部分的示意图已经了解到sockaddr_in有下面的几种成员:

  1. 16位地址类型:用于区分当前是何种类型的通信,对应的成员名是sin_family
  2. 16位端口号:对应的成员名是sin_port
  3. 32位IP地址:对应的成员名是sin_addr
  4. 8字节填充

因为第四个成员可以不需要考虑,只是用于占位,所以可以忽略,下面就前面三种类型进行详细介绍:

首先是16为地址类型,其类型是sa_family_t。在底层,该类型是一个宏:

struct sockaddr_in
{__SOCKADDR_COMMON (sin_);// ...
};#define	__SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##familytypedef unsigned short int sa_family_t;

这个宏利用到了[C语言的##运算符],其含义是将sa_prefix##替换为宏传递的值,因为在__SOCKADDR_COMMON (sin_)设置sa_prefix对应的sin_,所以拼接后为sin_family,因为sa_family_t代表的是unsigned short int,所以sin_family就是一个unsigned short int的值

因为当前是网络通信,所以对应值就是AF_INET,但是需要注意,这里传递的AF_INET和前面在socket接口传递的AF_INET含义不同,在使用socket时,指定AF_INET表明了socket所使用的协议族,它决定了内部数据结构和通信规则,而在调用bind时,指定AF_INET是因为需要通过这个成员来正确解析后续的地址信息

第二个成员是16位端口号,对于端口来说,其类型是in_port_t。在底层,对应源码如下:

struct sockaddr_in
{// ...in_port_t sin_port;			/* Port number.  */// ...
};typedef uint16_t in_port_t;typedef __uint16_t uint16_t;typedef unsigned short int __uint16_t;

所以,本质in_port_t也是unsigned short int,但是因为使用unsigned short int__uint16_t都不够简单,所以直接使用uint16_t

需要注意的是,在[Socket编程基础]提到过网络字节流时使用的都是大端,所以如果当前服务器是小端存储,那么就需要转换,否则就不需要转换。这里有两种处理方式:

  1. 判断当前设备是否是大端,如果是就直接写端口号,否则就需要对端口号进行小端到大端的转化,具体判断方式参考[进制转换与类型在内存的存储方式]
  2. 不论是大端还是小端都进行转换,如果是大端就不变,否则就变成大端

本次考虑第二种处理方式,系统提供了相关的接口处理大小端转换问题,如下:

uint16_t htons(uint16_t hostshort);

最后考虑第三个成员:IP地址,其类型是一个结构体:struct in_addr,其原型如下:

struct sockaddr_in
{// ...struct in_addr sin_addr;		/* Internet address.  */// ...
};typedef uint32_t in_addr_t;
struct in_addr
{in_addr_t s_addr;
};typedef __uint32_t uint32_t;
typedef unsigned int __uint32_t;

实际上就是一个结构体包含了一个unsigned int类型的成员,所以在底层,IP地址是一个无符号整数,在设置IP地址时,需要具体指定到sin_addr

在底层,第四个成员如下:

struct sockaddr_in
{/* Pad to size of `struct sockaddr'.  */unsigned char sin_zero[sizeof (struct sockaddr)- __SOCKADDR_COMMON_SIZE- sizeof (in_port_t)- sizeof (struct in_addr)];
};

以上就是bind接口的第二个参数的详细介绍,下面根据上面的介绍对指定的套接字进行绑定:

因为需要创建端口号和IP地址,所以需要增加两个成员,其中端口号使用的类型是uint16_t,而IP地址是字符串类型,之所以使用字符串是为了在使用时更方便,但是如果使用字符串,字符串使用的格式是点分十进制,而需要的是一个无符号整型的整数,此时就需要进行转换,并且还需要将IP地址也转换为大端字节序,这个问题对应地解决方案是inet_addr接口,其原型如下:

in_addr_t inet_addr(const char *cp);

这个接口可以将指定的点分十进制字符串格式的IP地址转换为in_addr_t类型,并且转换为大端字节序

接着对创建的端口号成员和IP地址成员进行初始化,本次可以考虑给定一个默认的值:8080127.0.0.1,也可以让用户在创建服务器时自行设定

在构造函数内部,首先就是创建一个struct sockaddr_in结构的对象,接着就是根据前面的提示对结构体成员进行填充,因为结构体对象一旦创建就不能再通过整体赋值的方式初始化,只能通过对每一个成员单独赋值初始化。但是填充具体值之前,建议将struct sockaddr_in进行清空操作(即全部初始化为0),下面有两种方法:

  1. 使用memset接口进行清空
  2. 使用bzero接口进行清空

本次考虑使用bzero接口进行,其原型如下:

#include <strings.h>void bzero(void *s, size_t n);

这个接口和memset接口的效果相同

清空后就是对每个成员进行赋值初始化

初始化struct sockaddr_in对象后,就可以对指定套接字的对应文件描述符进行绑定。同样,考虑使用日志系统显示相关信息

综上,代码如下:

// 错误码枚举类
enum class ErrorNumber
{// ...BindSocketFail,       // 绑定失败
};// 默认端口和IP地址
const std::string default_ip = "127.0.0.1";
const uint16_t default_port = 8080;class UdpServer
{
public:UdpServer(const std::string &ip = default_ip, uint16_t port = default_port): // ... , _ip(ip), _port(port){// ...// 绑定端口号和IP地址struct sockaddr_in saddrIn;saddrIn.sin_family = AF_INET;saddrIn.sin_port = htons(_port);saddrIn.sin_addr.s_addr = inet_addr(_ip.c_str());// 使用reinterpret_cast强制类型转换int ret = bind(_socketfd, reinterpret_cast<const sockaddr *>(&saddrIn), sizeof(sockaddr_in));if (ret < 0){LOG(LogLevel::FATAL) << "Bind error" << strerror(errno);exit(static_cast<int>(ErrorNumber::BindSocketFail));}LOG(LogLevel::INFO) << "Bind success";}// ...private:// ...uint16_t _port;  // 端口号std::string _ip; // 点分十进制IP地址
};

至此,服务器创建完成,总结一下创建服务器一共分为两步:

  1. 创建服务器套接字对应的文件描述符
  2. 根据套接字对应的文件描述符进行协议家族、端口号和IP地址进行绑定

启动服务器

启动服务器就需要用到一个变量标记当前服务器是否已经启动,所以需要一个成员_isRunning,该变量初始化为false,如果当前服务器并没有启动,就可以启动服务器,否则就不需要启动

所谓的启动服务器就是让服务器执行指定的任务,本次服务端就是负责接收信息并回复客户端发送的信息

因为服务器一般情况下一旦启动就不会再关闭,为了模拟这种情况,考虑服务器启动后就是一个死循环,在这个循环内部就是服务器执行任务的逻辑,所以基本结构如下:

// 启动服务器
void start()
{if (!_isRunning){_isRunning = true;while (true){}}
}

接下来就是考虑服务器执行的任务:接收客户端的消息并返回客户端的消息。对于这个任务可以拆为两个任务:

  1. 接收客户端消息
  2. 返回客户端接收到的消息

首先考虑接收客户端消息,接收客户端消息可以使用recvfrom接口,其原型如下:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

如果仔细观察上面的接口可以发现其和之前文件部分的read接口很类似,该接口的第一个参数表示用于接收的套接字对应的文件描述符,第二个参数表示缓冲区,用于存储接收到的数据,第三个参数表示缓冲区的大小,第四个参数表示是否是一个标记位,传递0表示使用默认的阻塞模式和行为,第五个参数表示客户端的套接字结构,第六个参数表示客户端套接字结构的大小

对于前四个参数都很好理解,关键是第五个参数,为什么服务器端接收还需要知道客户端套接字结构,具体来说,为什么服务器端接收需要知道客户端的端口和IP地址。最简单的解释就是因为UDP是无连接协议,服务器没有固定的连接信息,所以每次收到数据包时,需要知道数据包的来源(客户端的IP和端口),以便在需要回复数据时能正确定位到发送方,另外获取客户端信息有助于日志记录、错误排查以及实时监控,这样可以更准确地定位是哪个客户端发来了数据以及可能出现的问题所在

但是,需要注意,后两个参数是输出型参数,也就是说,第四个参数和第五个参数的值并不需要用户指定

该接口返回读取到的字节数,否则返回-1

根据这个接口的介绍,可以设计第一个任务的逻辑如下,同样考虑结合日志显示相应的信息:

建议接收数据时留下一个位置用于存放\0

// 启动服务器
void start()
{if (!_isRunning){_isRunning = true;while (true){// 1. 接收客户端信息char buffer[1024] = {0};struct sockaddr_in peer;socklen_t length = sizeof(peer);// sizeof(buffer) - 1留下一个位置存放\0ssize_t ret = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, reinterpret_cast<struct sockaddr *>(&peer), &length);if (ret > 0){struct sockaddr_in temp = static_cast<struct sockaddr_in>(peer);// 1.1 打印信息LOG(LogLevel::INFO) << buffer;}}}
}

为了知道是哪一个客户端发送的消息,可以考虑打印出客户端消息的同时打印出客户端的端口和IP地址,此处需要注意:

首先,因为recvfrom接口的第四个参数类型是struct sockaddr,这个结构中默认不带有端口和IP地址,所以还需要转换回网络套接字结构对象

接着,因为网络使用的是大端,而一般的客户机都是小端,所以考虑将收到的端口和IP地址转换回小端字节序,对应地可以使用接口:

// 将大端字节序端口转换为小端字节序
uint16_t ntohs(uint16_t netshort);
// 将IP地址转换为小端字节序并按照点分十进制存储到一个静态空间,注意此处返回的不是字符串
char *inet_ntoa(struct in_addr in);

对于「将IP地址转换为小端字节序并按照点分十进制存储」可以使用接口inet_ntop,该接口原型如下:

 const char *inet_ntop(int af, const void * src, char *dst, socklen_t size);

该接口的第一个参数表示协议族,网络通信传递AF_INET,第二个参数传递struct in_addr类型变量的地址,第三个参数传递一个用于存储结果的空间地址,第四个参数传递第三个参数的大小

因为这个接口可以在函数的内部创建一个临时空间作为接口的第三个实参,如果是多线程情况下就不会出现多个线程访问同一块空间的问题

该接口返回一个IP地址字符串

示例代码如下:

void test()
{struct sockaddr_in local;char ipbuffer[64];const char *ip = ::inet_ntop(AF_INET, &local.sin_addr, ipbuffer, sizeof(ipbuffer));
}

结合这两个接口完善信息的打印:

// ...
struct sockaddr_in temp = reinterpret_cast<struct sockaddr_in>(peer);// 1.1 打印信息
LOG(LogLevel::INFO) << "Client: " << inet_ntoa(temp.sin_addr) << ":" << ntohs(temp.sin_port) << " send: " << buffer;
// ...

至此,服务器可以显示从客户端接收的消息,第一个任务完成,接下来处理第二个任务,服务器向客户端回复收到的信息。既然是回复,那么肯定涉及到发送信息,此时就可以使用sendto接口,其原型如下:

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

如果仔细观察上面的接口可以发现其和之前文件部分的write接口很类似,前四个参数和recvfrom一样,不再赘述,下面主要介绍第五个参数:

第五个参数表示目标网络套接字结构,既然是发送,肯定需要知道对方的端口和IP地址,所以第五个参数需要使用者自己创建对象并填充对应值

第六个参数和recvfrom一样

该接口返回成功发送的字节数,否则返回-1

因为在前面已经获取到了客户端的端口和IP地址,只需要直接赋值就可以正确设置客户端的网络套接字结构,所以基本逻辑如下:

// 2. 回应客户端
struct sockaddr_in peer_temp;
peer_temp.sin_family = AF_INET;
peer_temp.sin_addr.s_addr = temp.sin_addr.s_addr;
peer_temp.sin_port = temp.sin_port;ssize_t n = sendto(_socketfd, buffer, sizeof(buffer), 0, reinterpret_cast<const struct sockaddr *>(&peer_temp), sizeof(peer_temp));if (n > 0)
{// 2.1 打印回复信息LOG(LogLevel::INFO) << "Server received: "<< buffer<< ", and send to: "<< inet_ntoa(temp.sin_addr) << ":"<< ntohs(temp.sin_port);
}

停止服务器

停止服务器的逻辑很简单,只需要判断_isRunning是否为true,如果为true就调用文件部分提到的close接口关闭_socketfd并将_isRunning设置为false即可,为了保证对象销毁时可以自动释放,考虑在析构函数中调用停止服务器的接口:

=== “停止服务器接口”

// 停止服务器
void stop()
{if (_isRunning)close(_socketfd);
}

=== “析构函数”

~UdpServer()
{stop();
}

创建并封装客户端

创建客户端类

对客户端进行封装首先需要大致的框架,因为客户端主要是向服务器发送内容,所以主要任务就是发送信息,也就是启动客户端,对应地客户端也可以有停止客户端的接口,所以基本框架如下:

class UdpClient
{
public:UdpClient(){}// 启动客户端void start(){}// 结束客户端void stop(){}~UdpClient(){}private:
};

创建客户端套接字

创建客户端套接字的方式和服务端,此处不再赘述,代码如下:

enum class ErrorNumber
{ClientSocketFail = 1, // 创建套接字失败
};class UdpClient
{
public:UdpClient(): _socketfd(-1){_socketfd = socket(AF_INET, SOCK_DGRAM, 0);if (_socketfd < 0){LOG(LogLevel::FATAL) << "Client initiate error: " << strerror(errno);exit(static_cast<int>(ErrorNumber::ClientSocketFail));}LOG(LogLevel::INFO) << "Client initiated: " << _socketfd;}// ...private:int _socketfd; // 套接字文件描述符
};

*绑定客户端IP地址和端口

实际上,客户端并不需要绑定IP地址和端口,如果客户端由程序员绑定,那么假设有两个公司上线的客户端使用的端口是一样的,就会出现一个软件先打开之后可以正常收到服务器发送的数据,但是另外一个软件的服务器就无法正确发送信息到对应的软件上,即一个端口只能对应一个进程,但是一个进程可以有多个端口

那么,客户端难道不需要端口吗?并不是,如果客户端没有端口,那么服务器只能通过IP地址找到具体客户端设备,但是找不到对应地进程,既然如此,客户端的端口怎么确定?实际上这个端口由操作系统自行分配

那么服务器端又为什么需要程序员手动绑定端口号?因为服务器端口号如果是随机的,而软件中请求服务器的端口号是固定的,那么一个软件可能在某一天可以正常收到服务器发送的数据,但是下一次因为服务器端口号是变化的,就无法正常收到信息

综上所述,服务器端需要程序员手动绑定IP地址和端口号,而客户端不需要程序员手动绑定IP地址和端口号,由操作系统自行分配并绑定

启动客户端

启动客户端和启动服务端的设计思路基本一致,基本框架如下:

// 启动客户端
void start()
{if (!_isRunning){_isRunning = true;while (true){}}
}

因为客户端的任务是向服务器端发送数据,所以需要知道服务端的IP地址和端口号,同样可以给定一个默认IP地址和端口,也可以由用户自行设置:

class UdpClient
{
public:UdpClient(const std::string ip = default_ip, uint16_t port = default_port): //..., _ip(ip), _port(port){// ...}// ...private:// ...std::string _ip; // 服务器IP地址uint16_t _port;  // 服务器端口号// ...
};

下面就是设计客户端的任务,本次设计客户端的任务为:向服务器发送信息并回显服务器回复的信息,同样,将这个任务拆为两个任务如下:

  1. 向服务器发送信息
  2. 回显服务器回复的信息

首先设计第一个任务,既然是向服务器发送信息,那么就要使用到sendto接口,这个接口在前面已经介绍过,此处不再赘述,代码如下:

// 1. 向服务器发送数据
struct sockaddr_in local;
bzero(&local, 0);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str());// 1.1 读取输入信息
std::string message;
getline(std::cin, message);// 1.2 发送数据
ssize_t ret = sendto(_socketfd, message.c_str(), message.size(), 0, reinterpret_cast<const struct sockaddr *>(&local), sizeof(local));if (ret < 0)LOG(LogLevel::WARNING) << "客户端未发送成功";

接着设计第二个任务,回显服务器信息本质就是接收服务器的信息并显示,所以需要使用recvfrom接口,同样,这个接口在前面已经介绍过,此处不再赘述,代码如下:

// 2. 回显服务器的信息
struct sockaddr_in temp;
socklen_t length = sizeof(temp);
char buffer[1024] = {0};
ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, reinterpret_cast<struct sockaddr *>(&temp), &length);if (n > 0)LOG(LogLevel::INFO) << "收到服务器信息:" << buffer;

停止客户端

思路与服务器端一致,代码如下:

=== “停止客户端接口”

// 结束客户端
void stop()
{if (_isRunning)close(_socketfd);
}

=== “析构函数”

~UdpClient()
{stop();
}

测试

测试步骤:

  1. 先启动服务端,再启动客户端
  2. 客户端向服务器端发送信息

测试目标:

  1. 客户端可以正常向服务器端发送信息
  2. 服务端可以正常显示客户端信息并正常向客户端返回客户端发送的信息
  3. 客户端可以正常显示服务端回复的信息

测试代码如下:

=== “客户端”

#include "udp_client.hpp"#include <memory>using namespace UdpClientModule;
using namespace LogSystemModule;int main(int argc, char *argv[])
{std::shared_ptr<UdpClient> client;if (argc == 1){// 创建客户端对象——使用默认端口和IP地址client = std::make_shared<UdpClient>();}else if (argc == 3){// 获取到用户输入的端口和IP地址std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);// 创建客户端对象——用户自定义端口和IP地址client = std::make_shared<UdpClient>(ip, port);}else{LOG(LogLevel::ERROR) << "错误使用,正确使用为:" << argv[0] << " IP地址 端口号(或者二者都不存在)";exit(3);}// 启动客户端client->start();return 0;
}

=== “服务端”

#include "udp_server.hpp"
#include <memory>using namespace UdpServerModule;int main()
{// 创建UdpServerModule对象std::shared_ptr<UdpServer> udp_server = std::make_shared<UdpServer>();udp_server->start();return 0;
}

本次设计的客户端支持用户从命令行输入端口和IP地址,否则就直接使用默认,下面是一种结果:

在这里插入图片描述

测试云服务器与本地进行通信

因为此时需要确保服务端运行在云服务器的公网IP上,否则客户端无法找到服务端,服务端测试代码修改如下:

#include "udp_server.hpp"
#include <memory>using namespace UdpServerModule;int main(int argc, char *argv[])
{// 创建UdpServerModule对象std::shared_ptr<UdpServer> udp_server;if (argc == 1){udp_server = std::make_shared<UdpServer>();}else if (argc == 3){std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);udp_server = std::make_shared<UdpServer>(ip, port);}udp_server->start();return 0;
}

测试云服务器与本地进行通信最直接的步骤如下:

  1. 将服务端程序拷贝到云服务器
  2. 本地作为客户端,通过云服务器的公网IP地址连接云服务器的服务端
  3. 客户端向云服务器发送信息

根据上面的步骤依次进行:

将服务端程序拷贝到云服务器:

在这里插入图片描述

因为传输到云服务器的文件默认是没有可执行权限的,所以需要使用chmod指令设置可执行权限:

在这里插入图片描述

接着,指定IP地址为云服务器公网IP地址,端口为8080,运行云服务器的服务端:

在这里插入图片描述

从上图可以看到,虽然创建套接字成功,但是绑定失败。之所以出现这个问题是因为云服务器的公网IP地址是不允许用户自行绑定的,但是如果是虚拟机就可以进行绑定IP地址(非127.0.0.1地址)

那么有没有什么办法解决呢?,但是在实现解决方案之前先了解下面的知识:

前面的代码在启动时都为服务器设置启动IP地址,但是思考一个问题,启动服务器真的需要指定IP地址吗?并不需要,那如果不需要指定服务器端IP地址,客户端怎么找到服务器呢?回到这个问题之前先解释为什么启动服务器不需要指定IP地址。实际上,之所以不需要IP地址是因为一台服务器可能有多个IP地址,此时如果服务器固定IP地址,那么此时就会出现服务器只能接收传送到固定IP地址的信息,就算服务器有很多IP地址也只有一个IP地址可以使用,很明显这个效果并不符合UDP协议的特点,因为UDP协议是面向无连接的,既然都不需要连接,为什么还需要指定IP地址,所以启动服务器不需要指定IP地址。有了这个概念之后,再解释没有指定服务器端IP地址,客户端怎么找到服务器,实际上只需要客户端启动的时候指定已经知道的服务器IP地址和端口号即可,对应的服务器只需要设置好端口号即可完成通信

有了上面的概念,对服务器端代码修改如下:

=== “服务器端封装代码”

 // 默认端口和IP地址// const std::string default_ip = "127.0.0.1"; 去除// ...class UdpServer{public:UdpServer(uint16_t port = default_port /* const std::string &ip = default_ip 服务器端不需要指定IP地址 */): _socketfd(-1), _port(port), _isRunning(false) /* , _ip(ip) */{// ...// 绑定端口号和IP地址// ...// saddrIn.sin_addr.s_addr = inet_addr(_ip.c_str());// 服务器端IP地址设置为任意saddrIn.sin_addr.s_addr = INADDR_ANY;// ...}// ...private:// ...// std::string _ip; // 点分十进制IP地址——去除// ...};

=== “主函数”

#include "udp_server.hpp"
#include <memory>using namespace UdpServerModule;int main(int argc, char *argv[])
{// 创建UdpServerModule对象std::shared_ptr<UdpServer> udp_server;if (argc == 1){udp_server = std::make_shared<UdpServer>();}else if (argc == 2){// std::string ip = argv[1]; 去除uint16_t port = std::stoi(argv[1]);udp_server = std::make_shared<UdpServer>(port);}else{LOG(LogLevel::ERROR) << "错误使用,正确使用:" << argv[0] << " 端口(或者不写)";exit(4);}udp_server->start();return 0;
}

在上面的服务器端封装代码中,因为不需要指定IP地址,在绑定时,对于struct sockaddr_in中的IP地址字段设置为INADDR_ANY表示0.0.0.0,即任意IP地址

在底层实际上就是0:

#define	INADDR_ANY		((in_addr_t) 0x00000000)

现在再按照前面提到的三步进行云服务器与本地进行通信:

将服务端程序拷贝到云服务器:

前面两步不变,只演示最后一步:

在这里插入图片描述

可以看到服务器已经正常启动了

本地作为客户端,通过云服务器的公网IP地址连接云服务器的服务端:

启动本地的客户端:

客户端向云服务器发送信息

如果客户端可以正常发送信息并且回显服务器回复的信息,服务器可以正常显示来自客户端的信息并且正常回复客户端收到的信息,那么说明连接成功:

在这里插入图片描述

从上图中可以看到,客户端和服务端已经可以正常通信,至此,从前面的本地通信测试完成了网络通信

需要注意,如果使用修改后的代码可以实现本地通信,但是使用修改后的代码无法实现网络通信(例如客户端正确指定云服务器IP地址和端口号并正常启动,服务器也是正常启动,但是客户端发送消息服务端并没有反应),此时可能是云服务器的安全组问题,如果云服务器的安全组并没有允许其他设备通过UDP协议向当前云服务器发送信息,那么就会出现客户端发送消息服务端没有反应的情况。对于这种情况可以在云服务的安全组配置中允许UDP协议和对应的端口

部分细节优化

在前面对服务端和客户端进行封装时,可以发现有些代码其实是可以进行合并的,还有一部分代码是可以进行封装的,下面就这些代码进行优化

合并重复代码

在出现错误时,服务端和客户端的退出码应该是一致的,此时可以对错误码枚举类进行抽取放到一个公共的文件中:

// 文件errors.hpp中
#pragma once// 错误码枚举类
enum class ErrorNumber
{SocketFail = 1, // 创建套接字失败(供服务端和客户端使用)BindSocketFail, // 绑定失败(供服务端使用)
};

再在服务端封装文件和客户端封装文件中引入该文件并修改指定位置的代码即可,不再演示

封装struct sockaddr_in

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>namespace SockAddrInModule
{class SockAddrIn{private:// 大端转换为本地端口值void NetPort2Local(){_port = ntohs(_s_addr_in.sin_port);}// 将IP地址转换为小端字节序并按照点分十进制存储void NetIP2Local(){char buffer[1024] = {0};const char *ip = inet_ntop(AF_INET, &_s_addr_in.sin_addr, buffer, sizeof(buffer));_ip = buffer;}public:// 无参构造SockAddrIn(){}// 根据指定的sockaddr_in对象进行构造SockAddrIn(const struct sockaddr_in &s): _s_addr_in(s){// 转换为本地小端用于使用NetIP2Local();NetPort2Local();}// 根据具体端口构造SockAddrIn(uint16_t port): _port(port){// 内部通过传入的端口对sockaddr_in对象进行初始化_s_addr_in.sin_family = AF_INET;_s_addr_in.sin_port = htons(_port);_s_addr_in.sin_addr.s_addr = INADDR_ANY;}// 根据端口和IP地址构造SockAddrIn(uint16_t port, std::string ip): _ip(ip), _port(port){_s_addr_in.sin_family = AF_INET;_s_addr_in.sin_port = htons(_port);_s_addr_in.sin_addr.s_addr = inet_addr(_ip.c_str());}// 重载&struct sockaddr *operator&(){return reinterpret_cast<struct sockaddr *>(&_s_addr_in);}// 获取struct sockaddr_in对象长度socklen_t getLength(){return sizeof(_s_addr_in);}// 返回IP地址std::string getIp(){return _ip;}// 返回端口号uint16_t getPort(){return _port;}~SockAddrIn(){}private:struct sockaddr_in _s_addr_in;std::string _ip;uint16_t _port;};
}

根据上面的封装修改服务端和客户端:

=== “服务端”

class UdpServer
{
public:UdpServer(uint16_t port = default_port): // ..., _sa_in(port)// ...{// ...// 绑定端口号和IP地址// struct sockaddr_in saddrIn;// saddrIn.sin_family = AF_INET;// saddrIn.sin_port = htons(_port);// // saddrIn.sin_addr.s_addr = inet_addr(_ip.c_str());// // 服务器端IP地址设置为任意// saddrIn.sin_addr.s_addr = INADDR_ANY;// 使用reinterpret_cast强制类型转换// int ret = bind(_socketfd, reinterpret_cast<const sockaddr *>(&saddrIn), sizeof(sockaddr_in));int ret = bind(_socketfd, &_sa_in, _sa_in.getLength());// ...}// 启动服务器void start(){if (!_isRunning){_isRunning = true;while (true){// 1. 接收客户端信息// ...struct sockaddr_in peer;// ...if (ret > 0){// 1.1 打印信息// struct sockaddr_in temp = peer;// LOG(LogLevel::INFO) << "Client: "//                     << inet_ntoa(temp.sin_addr) << ":"//                     << ntohs(temp.sin_port)//                     << " send: " << buffer;SockAddrIn temp(peer);LOG(LogLevel::INFO) << "Client: "<< temp.getIp() << ":"<< temp.getPort()<< " send: " << buffer;// 2. 回应客户端// ssize_t n = sendto(_socketfd, buffer, sizeof(buffer), 0, reinterpret_cast<const struct sockaddr *>(&temp), sizeof(temp));ssize_t n = sendto(_socketfd, buffer, sizeof(buffer), 0, &temp, temp.getLength());if (n > 0){// 2.1 打印回复信息// LOG(LogLevel::INFO) << "Server received: "//                     << buffer//                     << ", and send to: "//                     << inet_ntoa(temp.sin_addr) << ":"//                     << ntohs(temp.sin_port);LOG(LogLevel::INFO) << "Server received: "<< buffer<< ", and send to: "<< temp.getIp() << ":"<< temp.getPort();}}}}}// ...private:// ...// uint16_t _port; // 端口号// // std::string _ip; // 点分十进制IP地址——去除SockAddrIn _sa_in;// ...
};

=== “客户端”

 class UdpClient{public:UdpClient(const std::string ip = default_ip, uint16_t port = default_port): // ..., _sa_in(port, ip){// ...}// 启动客户端void start(){if (!_isRunning){_isRunning = true;while (true){// 1. 向服务器发送数据// struct sockaddr_in local;// bzero(&local, 0);// local.sin_family = AF_INET;// local.sin_port = htons(_port);// local.sin_addr.s_addr = inet_addr(_ip.c_str());// ...// 1.2 发送数据// ssize_t ret = sendto(_socketfd, message.c_str(), message.size(), 0, &local, sizeof(local));ssize_t ret = sendto(_socketfd, message.c_str(), message.size(), 0, &_sa_in, _sa_in.getLength());// ...}}}// ...private:// ...// std::string _ip; // 服务器IP地址// uint16_t _port;  // 服务器端口号SockAddrIn _sa_in;// ...};

相关文章:

Linux中的UDP编程接口基本使用

UDP编程接口基本使用 本篇介绍 在前面网络基础部分已经介绍了网络的基本工作模式&#xff0c;有了这些理论基础之后&#xff0c;下面先从UDP编程开始从操作部分深入网络 在本篇中&#xff0c;主要考虑下面的内容&#xff1a; 创建并封装服务端&#xff1a;了解创建服务端的…...

RAG项目实战:金融问答系统

需求痛点 私有知识很多&#xff0c;如何让大模型只选择跟问题有关的知识进行参考呢&#xff1f; 需求分析 是否可以使用关键词匹配呢&#xff1f;以前的搜索主要使用关键词匹配&#xff0c;这个要求太高了&#xff0c;需要提前抽取准备好关键词&#xff0c;有点像以前SEO的工…...

大白话React第十一章React 相关的高级特性以及在实际项目中的应用优化

假设我们已经对 React 前端框架的性能和可扩展性评估有了一定了解&#xff0c;接下来的阶段可以深入学习 React 相关的高级特性以及在实际项目中的应用优化&#xff0c;以下是详细介绍及代码示例&#xff1a; 1. React 高级特性的深入学习 1.1 React 并发模式&#xff08;Con…...

虚拟机Linux操作(持续更新ing)

虚拟机操作(持续更新ing) 虚拟机基本操作(Linux) # Linux # 立刻关机 poweroff # 立刻关机&#xff0c;可以选择数字或者具体时间 shutdown -h now # 立刻重启&#xff0c;可以选择数字或者具体时间 shutdown -r now # 立刻重启 reboot # cd 切换目录,下面用根目录举例 cd /…...

【开源-线程池(Thread Pool)项目对比】

一些实现**线程池&#xff08;Thread Pool&#xff09;**功能的开源项目的对比分析。 线程池功能的开源项目 项目名称语言优点缺点适用场景开源代码链接ThreadPoolC简单易用&#xff0c;代码简洁&#xff1b;适合快速原型开发。功能较为基础&#xff0c;不支持动态调整线程数…...

JMeter 实战项目脚本录制最佳实践(含 BadBoy 录制方式)

JMeter 实战项目脚本录制最佳实践&#xff08;含 BadBoy 录制方式&#xff09; 一、项目背景 在软件测试过程中&#xff0c;使用 JMeter 进行性能测试和功能测试是常见的操作。本实战项目将详细介绍如何使用 JMeter 自带工具以及 BadBoy 进行脚本录制&#xff0c;并完善脚本以…...

Jackson注解实战:@JsonInclude的妙用

在日常的Java开发中&#xff0c;我们经常需要将Java对象序列化为JSON格式&#xff0c;以便进行数据传输或存储。然而&#xff0c;有时候我们并不希望在JSON中包含某些空值或不必要的字段&#xff0c;这不仅会增加数据的冗余性&#xff0c;还可能对后续的处理造成困扰。Jackson库…...

CAN总线通信协议学习1——物理层

首先来看看CAN是怎么产生的&#xff1a;简单理解&#xff0c;CAN就是一种“拥有特别连接方式”的数据传输的总线&#xff0c;其有特定的一些规则。 &#xff08;注&#xff1a;资料及图片来源于知乎博主TOMOCAT。&#xff09; CAN总线的结构 查阅参考文献&#xff0c;OSI标准…...

Vim 常用快捷键大全:跳转、编辑、查找替换全解析

摘要&#xff1a; Vim 是一款非常强大的文本编辑器&#xff0c;许多程序员和系统管理员都离不开它。 本文详细介绍了 Vim 编辑器中的常用快捷键和命令&#xff0c;从基本模式、光标移动、编辑操作到查找替换&#xff0c;再到文件保存等常用操作&#xff0c;帮助你快速上手并提…...

【Python 数据结构 2.时间复杂度和空间复杂度】

Life is a journey —— 25.2.28 一、引例&#xff1a;穷举法 1.单层循环 所谓穷举法&#xff0c;就是我们通常所说的枚举&#xff0c;就是把所有情况都遍历了的意思。 例&#xff1a;给定n&#xff08;n ≤ 1000&#xff09;个元素ai&#xff0c;求其中奇数有多少个 判断一…...

【Qt QML】QML鼠标事件(MouseArea)

QML鼠标事件全面解析 一、MouseArea基础概念 在 QML 中,鼠标事件是处理用户与界面元素交互的重要部分。QML 提供了多种方式来处理鼠标事件,MouseArea 是 QML 中用于处理鼠标事件的核心元素,它可以覆盖在其他元素之上,捕获鼠标操作并触发相应的信号。 1、基本用法 import …...

LeetCode 202. 快乐数 java题解

https://leetcode.cn/problems/happy-number/description/ 哈希表 class Solution {public boolean isHappy(int n) {if(n1) return true;HashSet<Integer> setnew HashSet<>();while(n!1&&!(set.contains(n))){//没找到结果&#xff1b;没有重复出现过se…...

《认知·策略·跃迁:新能源汽车工程师的深度学习系统构建指南》

--- ## 前言&#xff1a;为什么传统学习法正在杀死你的竞争力&#xff1f; 在新能源汽车领域&#xff0c;我们正经历着每18个月知识体系更新迭代的指数级变革。当磷酸铁锂电池能量密度刚突破200Wh/kg时&#xff0c;固态电池已进入量产倒计时&#xff1b;当自动驾驶还在L2级徘…...

PHP环境安装达梦数据库驱动实操

PHP环境安装达梦数据库驱动实操 一、环境准备 达梦数据库安装 从达梦官网下载对应系统版本的DM8开发版或企业版&#xff0c;完成安装并确保数据库服务正常运行。安装后需记录数据库的安装路径&#xff08;如Windows默认路径为D:\dmdbms&#xff0c;Linux为/dm/server&#xff0…...

Electron + Vite + React + TypeScript 跨平台开发实践指南

Electron Vite React TypeScript 跨平台开发全栈实践指南 开发环境的搭建(node.js&#xff0c;npm的安装)请参见我的文章 2025Q1 核心组件版本矩阵 组件版本关键改进特性Electron30.0.0原生ESM支持、V8引擎性能优化30%Vite6.0.0多核编译加速、SSR增强模式React21.0.0并发…...

Java---入门基础篇(下)---方法与数组

前言 本篇文章主要讲解有关方法与数组的知识点 ,是基础篇的一部分 , 而在下一篇文章我会讲解类和对象的知识点 入门基础篇上的链接给大家放在下面啦 ! Java---入门基础篇(上)-CSDN博客 感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb; 欢迎各位大佬指点…...

【分布式理论11】分布式协同之分布式事务(一个应用操作多个资源):从刚性事务到柔性事务的演进

文章目录 一. 什么是分布式事务&#xff1f;二. 分布式事务的挑战三. 事务的ACID特性四. CAP理论与BASE理论1. CAP理论1.1. 三大特性1.2. 三者不能兼得 2. BASE理论 五. 分布式事务解决方案1. 两阶段提交&#xff08;2PC&#xff09;2. TCC&#xff08;Try-Confirm-Cancel&…...

【文献阅读】Collective Decision for Open Set Recognition

基本信息 文献名称&#xff1a;Collective Decision for Open Set Recognition 出版期刊&#xff1a;IEEE TRANSACTIONS ON KNOWLEDGE AND DATA ENGINEERING 发表日期&#xff1a;04 March 2020 作者&#xff1a;Chuanxing Geng and Songcan Chen 摘要 在开集识别&#xff0…...

Gorm中的First()、Create()、Update()、Delete()的错误处理

一. First() result : tx.Model(&models.Attachment{}).Where("home ? AND home_id ?", attachment.Home, attachment.HomeID).First(&existingAttachment)如果没有查询到数据&#xff0c;result.Error的值是什么&#xff1f; 在使用 GORM&#xff08;…...

【心得】一文梳理高频面试题 HTTP 1.0/HTTP 1.1/HTTP 2.0/HTTP 3.0的区别并附加记忆方法

面试时很容易遇到的一个问题—— HTTP 1.0/HTTP 1.1/HTTP 2.0/HTTP 3.0的区别&#xff0c;其实这四个版本的发展实际上是一环扣一环的&#xff0c;是逐步完善的&#xff0c;本文希望帮助读者梳理清楚各个版本之间的区别&#xff0c;并且给出当前各个版本的应用情况&#xff0c;…...

uniapp 对接腾讯云IM群组成员管理(增删改查)

UniApp 实战&#xff1a;腾讯云IM群组成员管理&#xff08;增删改查&#xff09; 一、前言 在社交类App开发中&#xff0c;群组成员管理是核心功能之一。本文将基于UniApp框架&#xff0c;结合腾讯云IM SDK&#xff0c;详细讲解如何实现群组成员的增删改查全流程。 权限校验…...

AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

基于大模型的 UI 自动化系统

基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...

Docker 运行 Kafka 带 SASL 认证教程

Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明&#xff1a;server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...

WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)

一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解&#xff0c;适合用作学习或写简历项目背景说明。 &#x1f9e0; 一、概念简介&#xff1a;Solidity 合约开发 Solidity 是一种专门为 以太坊&#xff08;Ethereum&#xff09;平台编写智能合约的高级编…...

Rapidio门铃消息FIFO溢出机制

关于RapidIO门铃消息FIFO的溢出机制及其与中断抖动的关系&#xff0c;以下是深入解析&#xff1a; 门铃FIFO溢出的本质 在RapidIO系统中&#xff0c;门铃消息FIFO是硬件控制器内部的缓冲区&#xff0c;用于临时存储接收到的门铃消息&#xff08;Doorbell Message&#xff09;。…...

10-Oracle 23 ai Vector Search 概述和参数

一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI&#xff0c;使用客户端或是内部自己搭建集成大模型的终端&#xff0c;加速与大型语言模型&#xff08;LLM&#xff09;的结合&#xff0c;同时使用检索增强生成&#xff08;Retrieval Augmented Generation &#…...

【生成模型】视频生成论文调研

工作清单 上游应用方向&#xff1a;控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...

Python基于历史模拟方法实现投资组合风险管理的VaR与ES模型项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档&#xff09;&#xff0c;如需数据代码文档可以直接到文章最后关注获取。 1.项目背景 在金融市场日益复杂和波动加剧的背景下&#xff0c;风险管理成为金融机构和个人投资者关注的核心议题之一。VaR&…...

Kafka入门-生产者

生产者 生产者发送流程&#xff1a; 延迟时间为0ms时&#xff0c;也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于&#xff1a;异步发送不需要等待结果&#xff0c;同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...