基于UDP协议的网络服务器的模拟实现

目录

服务端类UdpServer的模拟实现

服务端类UdpServer的成员变量

服务端类UdpServer的构造函数、初始化函数initServer、析构函数

服务端类UdpServer的start函数

服务端类UdpServer的整体代码(即udp_server.h文件的整体代码)

基于服务端类UdpServer模拟实现的服务端

udp_server.cc文件的整体代码

客户端的模拟实现

udp_client.cc文件的整体代码

基于UDP协议的网络服务器的测试

在本地中测试

在网络中测试

bind绑定INADDR_ANY后的服务端(即udp_server.cc文件的整体代码)

bind绑定INADDR_ANY后的服务端类UdpServer的整体代码(即udp_server.h文件的整体代码)


服务端类UdpServer的模拟实现

服务端类UdpServer的成员变量

(tips:如果对以下内容感到疑惑,建议结合<<套接字socket编程的基础知识点>>一文阅读)

  • 一个服务器是需要绑定ip和端口号的,不然其他机器找不到该服务器,所以成员中肯定是有_ip和_port的。

  • 在网络中,发信息需要一个通信通道,这个通道为【当前进程--->sockfd指向的文件的文件缓冲区(即分配给该文件的某块内存)--->内核缓冲区--->网卡--->网络--->对方的网卡--->对方的内核缓冲区--->对方的sockfd指向的文件的文件缓冲区--->对方的进程】;接收信息需要一个通信通道,这个通道为【对方进程--->对方进程的sockfd指向的文件的文件缓冲区(即分配给该文件的某块内存)--->对方的内核缓冲区--->对方的网卡--->网络--->当前主机的网卡--->当前主机的内核缓冲区--->当前进程的sockfd指向的文件的文件缓冲区--->当前的进程】。根据前面的理论,可以看出这里在网络中,服务端进程和客户端进程通信肯定是需要通过socket文件的,需要该文件作为通信通道中的一环,所以服务端中肯定是需要一个指向该socket文件的文件描述符_sock的。

根据上面的理论,服务端类UdpServer的成员变量如下。

#include<iostream>
using namespace std;
#include<string.h>//提供bzero函数
#include<memory>//提供智能指针的库
#include<cstdlib>//提供atoi函数、exit函数
#include <unistd.h>//提供close
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include<sys/types.h>//系统库,提供socket编程需要的相关的接口
#include<sys/socket.h>//系统库,提供socket编程需要的相关的接口
#include<netinet/in.h>//提供sockaddr_in结构体
#include<arpa/inet.h>//提供sockaddr_in结构体class UdpServer
{
public:private://一个服务器是需要ip和端口号的,不然其他机器找不到该服务器string _ip;uint16_t _port;//port表示端口,uint16_t是C语言中stdint.h头文件中定义的一种数据类型,它占据16个二进制位,范围从0到65535。它是无符号整数类型,即只能表示非负整数,没有符号位。int _sock;//_sock是调用socket函数创建套接字时返回的值,本质就是文件描述符fd
};

服务端类UdpServer的构造函数、初始化函数initServer、析构函数

(tips:如果对以下内容感到疑惑,建议结合<<套接字socket编程的基础知识点>>一文阅读)

思路如下:

  • 构造函数的思路:就是把传给构造函数的参数赋值给UdpServer类的成员变量,没啥可说的。
  • 初始化函数的思路:就是先调用socket函数创建套接字socket文件(原因是需要socket文件作为通信通道的一环),然后调用bind函数将【当前进程】和【某个ip地址和某个端口port】进行绑定(原因在下面代码的注释中已经说的很明白了)。说一下,下面代码的注释中不光说明了需要将【当前进程】和【某个ip地址和某个端口port】进行绑定的原因,还说了为什么要先将【ip和port】从主机字节序转化成网络字节序,再将【当前进程】和【这些转化成了网络字节序的ip和port】进行绑定。
  • 析构函数的思路:在<<套接字socket编程的基础知识点>>一文中讲解socket函数的部分说过,socket函数会在内核上创建一个struct file文件,并把该文件的文件描述符返回,所以析构函数需要把【在初始化函数中因为调用了socket函数而“打开”的文件描述符】给close了。
  • 剩余的说明都在下面代码的注释中了,请结合代码思考。

结合上面的理论,服务端类UdpServer的构造函数、初始化函数initServer、析构函数的代码如下。

#include<iostream>
using namespace std;
#include<string.h>//提供bzero函数
#include<memory>//提供智能指针的库
#include<cstdlib>//提供atoi函数、exit函数
#include <unistd.h>//提供close
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include<sys/types.h>//系统库,提供socket编程需要的相关的接口
#include<sys/socket.h>//系统库,提供socket编程需要的相关的接口
#include<netinet/in.h>//提供sockaddr_in结构体
#include<arpa/inet.h>//提供sockaddr_in结构体class UdpServer
{
public:UdpServer(uint16_t port, string ip):_ip(ip),_port(port){}void initServer(){//首先创建套接字_sock = socket(AF_INET, SOCK_DGRAM, 0);if(_sock == -1){cout<<"创建套接字失败"<<endl;exit(1);}//bind,作用为将设置的ip和port和当前进程绑定,为什么要bind绑定呢?套接字sock文件用于通信,首先,如果想要网络通信,则必须通过网卡,所以你必须得指定从哪个网卡(ip)读//取数据送到socket文件,这就是bind ip的原因,数据读取完毕后,送到哪个端口(进程)呢?所以你必须指定一个端口号。//通信的另一方一定得知道我这一方的ip和port,这样它才能在接收信息时认出这是我发给它的,它才会进行信息的接收,那如何把我绑定的ip和port告知通信的另一方呢?在UDP通信模式下,调用sendto函//数发送信息时OS会自动把当前进程绑定的ip和port包含在信息中形成UDP数据包后发给对方,对方进程调用recvfrom函数接收信息时通过传给recvfrom函数的输出型参数就能知道我的ip和port了。说一下,sendto函数//发送信息时会把信息发送到网络中,然后另一端的进程会从网络中获取到这些信息。注意我们是需要在调用sendto函数,把当前进程的ip从字符序列转换成网络序列、需要将端口号从主机序//列(可能是大端、可能是小端)转化成网络序列(大端)的。因为sendto发送信息时,OS会自动把当前进程绑定的ip和port包含进发送的信息中形成UDP数据包的,而网络资源又是寸土寸金的,发送的数据越小越好,//所以在调用sendto发送数据前,是需要将ip从字符序列转换成网络序列、需要将端口号从主机序列(可能是大端、可能是小端)转化成网络序列(大端)的。注意因为sendto发送信息到网络时,是OS//自动把当前进程bind绑定的ip和port包含进发送的信息中形成UDP数据包,所以如果想要sendto在发送信息时ip和port是网络序列,那在当前进程bind绑定ip和port时,ip和port就应该是网络序列,所以在这里bind绑定//ip和port前,是应该把ip从字符序列转换成网络序列、应该把端口号port从主机序列(可能是大端、可能是小端)转化成网络序列(大端)的。说一下,bind绑定这些信息(即ip和port)时,是先把需//要绑定的信息全填充到一个sockaddr类的对象中,之后bind只需要绑定这个sockaddr类的对象即可,由于该结构体当中还有部分选填字段,因此我们最好在填充之前对该结构体变量里面的内容进行清空,//然后再将协议家族、端口号、IP地址等信息填充到该结构体变量当中。sockaddr_in local;        bzero(&local,sizeof(local));//该函数用于把从某个地址开始以及向后的若干字节上的值置为0local.sin_family = AF_INET;local.sin_port = htons(_port);//把port从主机序列转化成网络序列local.sin_addr.s_addr = inet_addr(_ip.c_str());//把_ip从string类转换成in_addr_t类(即uint32_t类),然后会自动将转化出的整数从主机字节序变为网络字节序if(bind(_sock, (sockaddr*)&local, sizeof(local)) < 0){cout<<"bind绑定失败"<<endl;exit(1);}}~UdpServer(){if(_sock >= 0)close(_sock);}
private://一个服务器是需要ip和端口号的,不然其他机器找不到该服务器string _ip;uint16_t _port;//port表示端口,uint16_t是C语言中stdint.h头文件中定义的一种数据类型,它占据16个二进制位,范围从0到65535。它是无符号整数类型,即只能表示非负整数,没有符号位。int _sock;//_sock是调用socket函数创建套接字时返回的值,本质就是文件描述符fd
};

服务端类UdpServer的start函数

(tips:如果对以下内容感到疑惑,建议结合<<套接字socket编程的基础知识点>>一文阅读)

对于我们当前模拟实现的服务器类UdpServer,如果想让服务器跑起来,则调用其成员函数的顺序是【UdpServer类的构造函数--->UdpServer类的初始化函数initServer--->UdpServer类的start函数--->UdpServer类的析构函数】,可以看到start函数是在初始化函数之后的,走完初始化函数后,已经完成了socket文件的创建、将【当前进程】和【转化成网络字节序后的某个ip地址和某个端口port】进行绑定,剩下的工作就是start函数需要完成的了,需要做的事情为:

  • 设置一个死循环,每次循环都要做的事情是【接收、分析、并处理客户端发过来的信息,以及处理完毕后向客户端发出反馈信息】。在接收客户端发来的信息时,需要创建一个sockaddr类的对象作为输出型参数,以拿到客户端的ip和port信息,这样当前进程(即服务端)才能在向客户端发送反馈信息时知道目的地在哪。在通过作为输出型参数的sockaddr类的对象获取客户端的ip和port信息时,因为这些信息是从网络中来的,所以此时就需要将这些信息从网络字节序转化成主机字节序。
  • 剩余的说明都在下面代码的注释中了,请结合代码思考。

结合上面的理论,服务端类UdpServer的start函数的代码如下。

#include<iostream>
using namespace std;
#include<string.h>//提供bzero函数
#include<memory>//提供智能指针的库
#include<cstdlib>//提供atoi函数、exit函数
#include <unistd.h>//提供close
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include<sys/types.h>//系统库,提供socket编程需要的相关的接口
#include<sys/socket.h>//系统库,提供socket编程需要的相关的接口
#include<netinet/in.h>//提供sockaddr_in结构体
#include<arpa/inet.h>//提供sockaddr_in结构体class UdpServer
{
public:void Start(){char c[1024];//当前Start函数是服务端调用的函数,用于获取、分析、处理客户端的信息并反馈给客户端,服务端读取到的客户端的信息就存放在数组c中sockaddr_in client_info;//纯输出型参数,作为参数传给recvfrom函数后即可获取客户端的ip和port(需要获取的原因是:当前Start函数是服务端调用的函数,用于获取、分析、处理客户端的信息并反馈给客户端,所以是需要客户端的ip和port的),注意因为这里是作为输出型参数,所以原则上不必调用bzero函数初始化。socklen_t client_info_len = sizeof(client_info);//输入输出型参数,作为参数传给recvfrom函数。输入时的值为:client_info对象所占的空间大小/输出时的值为:实际填充进client_info的数据的大小while(1){ssize_t size = recvfrom(_sock, (void*)c, sizeof(c), 0,(sockaddr*)&client_info, &client_info_len);//从客户端读取消息。函数需要的参数flag设置为0即可,不必关心其含义。recvfrom的返回值为实际读取到的字节个数。if (size > 0){//走到这里服务端已经读取到了客户端发过来的信息c[size] = 0;//因为在设计客户端与服务端通信时,设计的逻辑是双方都只发送C语言字符串,所以通信通道中的信息就全是C语言字符串,所以这里需要在读到的数据的末尾设置0,防止打印时出现乱码uint16_t port = ntohs(client_info.sin_port);//把从网络中来的port从网络字节序转化成主机字节序string ip = (inet_ntoa(client_info.sin_addr));//会自动把从网络中来的uint16_t整形的ip地址先从网络字节序转化成主机字节序,然后将主机字节序的整形ip转化string类型的ipprintf("[%s][%d]#:%s\n",ip.c_str(), port, c);}/*走到这里开始处理从客户端读到的信息,这需要经过一段的时间。*///把从客户端读取到的信息处理完毕后,向客户端发出反馈信息sendto(_sock, c, strlen(c), 0, (sockaddr*)&client_info, client_info_len);}}private://一个服务器是需要ip和端口号的,不然其他机器找不到该服务器string _ip;uint16_t _port;//port表示端口,uint16_t是C语言中stdint.h头文件中定义的一种数据类型,它占据16个二进制位,范围从0到65535。它是无符号整数类型,即只能表示非负整数,没有符号位。int _sock;//_sock是调用socket函数创建套接字时返回的值,本质就是文件描述符fd
};

服务端类UdpServer的整体代码(即udp_server.h文件的整体代码)

下面是整个udp_server.h文件的代码。

#include<iostream>
using namespace std;
#include<string.h>//提供bzero函数
#include<memory>//提供智能指针的库
#include<cstdlib>//提供atoi函数、exit函数
#include <unistd.h>//提供close
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include<sys/types.h>//系统库,提供socket编程需要的相关的接口
#include<sys/socket.h>//系统库,提供socket编程需要的相关的接口
#include<netinet/in.h>//提供sockaddr_in结构体
#include<arpa/inet.h>//提供sockaddr_in结构体class UdpServer
{
public:UdpServer(uint16_t port, string ip):_ip(ip),_port(port){}~UdpServer(){if(_sock >= 0)close(_sock);}void initServer(){//首先创建套接字_sock = socket(AF_INET, SOCK_DGRAM, 0);if(_sock == -1){cout<<"创建套接字失败"<<endl;exit(1);}//bind,作用为将设置的ip和port和当前进程绑定,为什么要bind绑定呢?套接字sock文件用于通信,首先,如果想要网络通信,则必须通过网卡,所以你必须得指定从哪个网卡(ip)读//取数据送到socket文件,这就是bind ip的原因,数据读取完毕后,送到哪个端口(进程)呢?所以你必须指定一个端口号。//通信的另一方一定得知道我这一方的ip和port,这样它才能在接收信息时认出这是我发给它的,它才会进行信息的接收,那如何把我绑定的ip和port告知通信的另一方呢?在UDP通信模式下,调用sendto函//数发送信息时OS会自动把当前进程绑定的ip和port包含在信息中形成UDP数据包后发给对方,对方进程调用recvfrom函数接收信息时通过传给recvfrom函数的输出型参数就能知道我的ip和port了。说一下,sendto函数//发送信息时会把信息发送到网络中,然后另一端的进程会从网络中获取到这些信息。注意我们是需要在调用sendto函数,把当前进程的ip从字符序列转换成网络序列、需要将端口号从主机序//列(可能是大端、可能是小端)转化成网络序列(大端)的。因为sendto发送信息时,OS会自动把当前进程绑定的ip和port包含进发送的信息中形成UDP数据包的,而网络资源又是寸土寸金的,发送的数据越小越好,//所以在调用sendto发送数据前,是需要将ip从字符序列转换成网络序列、需要将端口号从主机序列(可能是大端、可能是小端)转化成网络序列(大端)的。注意因为sendto发送信息到网络时,是OS//自动把当前进程bind绑定的ip和port包含进发送的信息中形成UDP数据包,所以如果想要sendto在发送信息时ip和port是网络序列,那在当前进程bind绑定ip和port时,ip和port就应该是网络序列,所以在这里bind绑定//ip和port前,是应该把ip从字符序列转换成网络序列、应该把端口号port从主机序列(可能是大端、可能是小端)转化成网络序列(大端)的。说一下,bind绑定这些信息(即ip和port)时,是先把需//要绑定的信息全填充到一个sockaddr类的对象中,之后bind只需要绑定这个sockaddr类的对象即可,由于该结构体当中还有部分选填字段,因此我们最好在填充之前对该结构体变量里面的内容进行清空,//然后再将协议家族、端口号、IP地址等信息填充到该结构体变量当中。sockaddr_in local;        bzero(&local,sizeof(local));//该函数用于把从某个地址开始以及向后的若干字节上的值置为0local.sin_family = AF_INET;local.sin_port = htons(_port);//把port从主机序列转化成网络序列local.sin_addr.s_addr = inet_addr(_ip.c_str());//把_ip从string类转换成in_addr_t类(即uint32_t类),然后会自动将转化出的整数从主机字节序变为网络字节序if(bind(_sock, (sockaddr*)&local, sizeof(local)) < 0){cout<<"bind绑定失败"<<endl;exit(1);}}void Start(){char c[1024];//当前Start函数是服务端调用的函数,用于获取、分析、处理客户端的信息并反馈给客户端,服务端读取到的客户端的信息就存放在数组c中sockaddr_in client_info;//纯输出型参数,作为参数传给recvfrom函数后即可获取客户端的ip和port(需要获取的原因是:当前Start函数是服务端调用的函数,用于获取、分析、处理客户端的信息并反馈给客户端,所以是需要客户端的ip和port的),注意因为这里是作为输出型参数,所以原则上不必调用bzero函数初始化。socklen_t client_info_len = sizeof(client_info);//输入输出型参数,作为参数传给recvfrom函数。输入时的值为:client_info对象所占的空间大小/输出时的值为:实际填充进client_info的数据的大小while(1){ssize_t size = recvfrom(_sock, (void*)c, sizeof(c), 0,(sockaddr*)&client_info, &client_info_len);//从客户端读取消息。函数需要的参数flag设置为0即可,不必关心其含义。recvfrom的返回值为实际读取到的字节个数。if (size > 0){//走到这里服务端已经读取到了客户端发过来的信息c[size] = 0;//因为在设计客户端与服务端通信时,设计的逻辑是双方都只发送C语言字符串,所以通信通道中的信息就全是C语言字符串,所以这里需要在读到的数据的末尾设置0,防止打印时出现乱码uint16_t port = ntohs(client_info.sin_port);//把从网络中来的port从网络字节序转化成主机字节序string ip = (inet_ntoa(client_info.sin_addr));//会自动把从网络中来的uint16_t整形的ip地址先从网络字节序转化成主机字节序,然后将主机字节序的整形ip转化string类型的ipprintf("[%s][%d]#:%s\n",ip.c_str(), port, c);}/*走到这里开始处理从客户端读到的信息,这需要经过一段的时间。*///把从客户端读取到的信息处理完毕后,向客户端发出反馈信息sendto(_sock, c, strlen(c), 0, (sockaddr*)&client_info, client_info_len);}}private://一个服务器是需要ip和端口号的,不然其他机器找不到该服务器string _ip;uint16_t _port;//port表示端口,uint16_t是C语言中stdint.h头文件中定义的一种数据类型,它占据16个二进制位,范围从0到65535。它是无符号整数类型,即只能表示非负整数,没有符号位。int _sock;//_sock是调用socket函数创建套接字时返回的值,本质就是文件描述符fd
};

基于服务端类UdpServer模拟实现的服务端

udp_server.cc文件的整体代码

udp_server.cc文件的整体代码如下。逻辑非常简单,从命令行中获取到传给服务端进程的ip和port后,通过它们构造出在上文中模拟实现出的UdpServer类的对象,然后通过该对象调用UdpServer类的成员函数initServer和Start,这样服务端进程就跑起来了,即服务器就跑起来了。(注意udp_server.h的代码是在上文中模拟实现出来的)

#include"udp_server.h"void usage(char* c)
{printf("Usage:%s ip port\n", c);
}//以后运行server进程的方式是输入命令:./udp_server ip port 
int main(int argc, char* argv[])
{if(argc != 3){usage(argv[0]);exit(1);}string ip = argv[1];uint16_t port = atoi(argv[2]);unique_ptr<UdpServer> up(new UdpServer(port, ip));up->initServer();up->Start();return 0;
}

客户端的模拟实现

udp_client.cc文件的整体代码

(tips:如果对以下内容感到疑惑,建议结合<<套接字socket编程的基础知识点>>一文阅读)

客户端的大逻辑就是设置一个循环,每次循环都要【向服务端进程发送信息,然后接收服务端的反馈信息】。发送信息前也和服务端一样,也需要调用socket函数创建套接字文件、也需要调用相关接口将对端的ip和port从主机字节序转化成网络字节序。需要进行这些操作的原因在讲解客户端时都已经说过了,这里不再赘述。

还有一些笔者对于客户端的实现的补充说明,这些内容都在下面代码的注释中,请结合代码进行思考。

#include<iostream>
using namespace std;
#include<string.h>//提供bzero函数
#include <unistd.h>//提供close函数
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include<sys/types.h>//系统库,提供socket编程需要的相关的接口
#include<sys/socket.h>//系统库,提供socket编程需要的相关的接口
#include<netinet/in.h>//提供sockaddr_in结构体
#include<arpa/inet.h>//提供sockaddr_in结构体void usage(char* c)
{printf("usage:%s ip port\n", c);
}int main(int argc, char* argv[])
{if (argc != 3){usage(argv[0]);exit(1);}//首先创建套接字文件,即创建通信通道文件int sock = socket(AF_INET, SOCK_DGRAM, 0);if(sock < 0){cout<<"创建套接字失败"<<endl;exit(1);}//套接字文件创建完毕后,客户端需不需要进行bind将设置的ip和port和当前进程绑定,让当前进程从对应网卡(ip)中输入和读取数据呢?答案:一定是需要的,//如果不进行bind,那OS就不知道你这个进程要从哪个网卡(ip)输入或者读取数据,并且OS也不知道通信的另一方发来的数据该由哪个进程(端口port)接收。但注意,//因为当前进程是客户端,而客户端是让各个地区的普通人使用的,所以客户端一般是不会显示bind的,即程序员一般编写客户端的代码时不进行bind的调用,因为如果程//序员bind了,那客户端一定是bind了一个固定的ip和端口,而每台机器的ip都不一样,固定的ip肯定是不行的;再说端口port,如果客户端bind了一个固定的port,因为//端口和进程是多对1的关系,每个端口最多被一个进程使用,所以如果此时有其他进程先运行并占用了这个port,那客户端这个程序就启动不了了,所以客户端一般是不需要//显示的bind绑定指定的ip和port的,而是让OS自动随机选择。那什么时候随机选择呢?在客户端第一次向服务端sendto发送信息的时候,OS就会自动把当前机器的ip和随机//分配的一个port给当前进程进行bind。//在bind以及sendto前,需要先将对端的ip从字符串变成网络序列,把对端的port从主机序列变成网络序列,其原因在服务端的代码注释中说明过了sockaddr_in server_info;bzero(&server_info, sizeof(server_info));server_info.sin_family = AF_INET;server_info.sin_port = htons(atoi(argv[2]));//把port从主机序列转化成网络序列server_info.sin_addr.s_addr = inet_addr(argv[1]);//把ip从string类转换成in_addr_t类(即uint32_t类),然后会自动将转化出的整数从主机字节序变为网络字节序char c[1024];//该缓冲区用于接收客户端的反馈信息string message;//message是客户端发给服务端的信息while(1){cout<<"请输入发给服务端的信息#";getline(cin, message);if(message == "end"){break;}sendto(sock, message.c_str(), sizeof(c), 0, (sockaddr*)&server_info, sizeof(server_info));//在当前情景中,因为当前进程是客户端,所以直接就能从命令行中获取到服务端的ip和port信息,但即使我们已经能获取服务端的ip和port信息了,这里客户端在调用recvfrom接收服务//端的信息时还是得传一个sockaddr_in类的临时temp对象作为输出型参数获取服务端的ip和port信息,因为调用recvfrom函数需要这样一个参数,不传就无法调用,同时也不能传nullptr进去,否则//会出现未知的错误。有人可能会说【这个函数这样设计不是多此一举吗?】,实际上并不是多次一举,因为不同情景下需求不同,在当前场景下,当前进程(即客户端进程)只作为客户端,除了需要接收//服务端进程的信息外不需要接收其他进程的信息,而如果当前进程(即客户端进程)还作为其他进程的服务端,接收其他进程的信息后需要向这些进程发出反馈信息,那么就需要这些进程的ip和port,此时//就需要通过传入recvfrom函数的作为输出型参数的sockaddr_in类对象获取这些进程的ip和port,所以该函数这样设计并不是多次一举。//根据上一段的理论,这里在recvfrom时我们就创建一个sockaddr_in temp作为函数的占位符。sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sock, c, sizeof(c), 0, (sockaddr*)&temp, &len);if (s > 0){c[s] = 0;//因为在设计客户端与服务端通信时,设计的逻辑是双方都只发送C语言字符串,所以通信通道中的信息就全是C语言字符串,所以这里需要在读到的数据的末尾设置0,防止打印时出现乱码printf("服务端返回给我(客户端)的信息为:%s\n", c);}}close(sock);return 0;
}

基于UDP协议的网络服务器的测试

在本地中测试

将上文中编写好的udp_server.cc和udp_client.cc文件编译好后,直接在两个ssh渠道中分别运行它们,在运行时要传入命令行参数:

  • 把本地环回地址(即ip地址127.0.0.1)和端口号8080设置成命令行参数传给服务端进程,让服务端进程和它们进行绑定bind。
  • 把本地环回地址(即ip地址127.0.0.1)和端口号8080也设置成命令行参数传给客户端进程,让客户端进程知道自己该向哪个主机的哪个进程发送信息(或者说让客户端进程知道服务端进程在哪)。

这样一来,客户端进程client和服务端进程server收发数据时就只在本地协议栈中进行数据流动,不会把我们的数据发送到网络中。

测试结果如下(左半部分是服务端、右半部分的上面是客户端,右半部分的下面是一个用于检验本机各端口的网络连接情况的指令)。可以看到结果是符合我们的预期的,客户端向服务端发送信息后,服务端能收到该信息,并能将信息处理后再给客户端发送反馈信息。

问题:如何证明服务端中收到的信息是客户端发过来的呢?

答案:通过上图红线连接的内容可以看出在服务端的shell界面中打印出的端口号52455就是客户端绑定的端口号(该端口号是客户端第一次向服务端发送信息时OS随机分配的),也就证明了在服务端中受到的信息就是客户端发过来的。

在网络中测试

现在我们已经通过了本地测试,接下来就需要进行网络测试了,那是不是直接让服务端绑定我的公网IP,此时这个服务端就能够被外网访问了呢?

理论上是这样的,但在实际bind绑定的过程中会出现问题,如下图所示,是无法bind的,其原因在<<套接字socket编程的基础知识点>>一文中已经说过了,详情请见该篇文章的内容。

问题:那当前的代码如何才能进行网络测试呢?

答案:让服务端进程bind绑定INADDR_ANY即可,其原因在<<套接字socket编程的基础知识点>>一文中也已经说过了,截图如下。

既然要让服务端进程bind绑定INADDR_ANY,那我们就要把上文中模拟实现的服务端类UdpServer(即udp_server.h文件)的代码和服务端(即udp_server.cc文件)的代码稍作修改。哪些地方需要修改呢?非常简单,如下:

  • 在udp_server.cc文件中,把左边红框处的代码改成右边红框处的代码即可。
  • 在udp_server.h文件中,把左边红框处的代码改成右边红框处的代码即可。

bind绑定INADDR_ANY后的服务端(即udp_server.cc文件的整体代码)

根据上面的理论进行修改后,bind绑定INADDR_ANY后的服务端的代码如下。

#include"udp_server.h"void usage(char* c)
{printf("Usage:%s port\n", c);
}//以后运行server进程的方式是输入命令:./udp_server port 
int main(int argc, char* argv[])
{if(argc != 2){usage(argv[0]);exit(1);}//string ip = argv[1];uint16_t port = atoi(argv[1]);unique_ptr<UdpServer> up(new UdpServer(port));up->initServer();up->Start();return 0;
}

bind绑定INADDR_ANY后的服务端类UdpServer的整体代码(即udp_server.h文件的整体代码)

根据上面的理论进行修改后,bind绑定INADDR_ANY后的服务端类UdpServer的整体代码如下。

#include<iostream>
using namespace std;
#include<string.h>//提供bzero函数
#include<memory>//提供智能指针的库
#include<cstdlib>//提供atoi函数、exit函数
#include <unistd.h>//提供close
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include<sys/types.h>//系统库,提供socket编程需要的相关的接口
#include<sys/socket.h>//系统库,提供socket编程需要的相关的接口
#include<netinet/in.h>//提供sockaddr_in结构体
#include<arpa/inet.h>//提供sockaddr_in结构体class UdpServer
{
public:UdpServer(uint16_t port, string ip = ""):_ip(ip),_port(port){}~UdpServer(){if(_sock >= 0)close(_sock);}void initServer(){//首先创建套接字_sock = socket(AF_INET, SOCK_DGRAM, 0);if(_sock == -1){cout<<"创建套接字失败"<<endl;exit(1);}//bind,作用为将设置的ip和port和当前进程绑定,为什么要bind绑定呢?套接字sock文件用于通信,首先,如果想要网络通信,则必须通过网卡,所以你必须得指定从哪个网卡(ip)读//取数据送到socket文件,这就是bind ip的原因,数据读取完毕后,送到哪个端口(进程)呢?所以你必须指定一个端口号。//通信的另一方一定得知道我这一方的ip和port,这样它才能在接收信息时认出这是我发给它的,它才会进行信息的接收,那如何把我绑定的ip和port告知通信的另一方呢?在UDP通信模式下,调用sendto函//数发送信息时OS会自动把当前进程绑定的ip和port包含在信息中形成UDP数据包后发给对方,对方进程调用recvfrom函数接收信息时通过传给recvfrom函数的输出型参数就能知道我的ip和port了。说一下,sendto函数//发送信息时会把信息发送到网络中,然后另一端的进程会从网络中获取到这些信息。注意我们是需要在调用sendto函数,把当前进程的ip从字符序列转换成网络序列、需要将端口号从主机序//列(可能是大端、可能是小端)转化成网络序列(大端)的。因为sendto发送信息时,OS会自动把当前进程绑定的ip和port包含进发送的信息中形成UDP数据包的,而网络资源又是寸土寸金的,发送的数据越小越好,//所以在调用sendto发送数据前,是需要将ip从字符序列转换成网络序列、需要将端口号从主机序列(可能是大端、可能是小端)转化成网络序列(大端)的。注意因为sendto发送信息到网络时,是OS//自动把当前进程bind绑定的ip和port包含进发送的信息中形成UDP数据包,所以如果想要sendto在发送信息时ip和port是网络序列,那在当前进程bind绑定ip和port时,ip和port就应该是网络序列,所以在这里bind绑定//ip和port前,是应该把ip从字符序列转换成网络序列、应该把端口号port从主机序列(可能是大端、可能是小端)转化成网络序列(大端)的。说一下,bind绑定这些信息(即ip和port)时,是先把需//要绑定的信息全填充到一个sockaddr类的对象中,之后bind只需要绑定这个sockaddr类的对象即可,由于该结构体当中还有部分选填字段,因此我们最好在填充之前对该结构体变量里面的内容进行清空,//然后再将协议家族、端口号、IP地址等信息填充到该结构体变量当中。sockaddr_in local;        bzero(&local,sizeof(local));//该函数用于把从某个地址开始以及向后的若干字节上的值置为0local.sin_family = AF_INET;local.sin_port = htons(_port);//把port从主机序列转化成网络序列local.sin_addr.s_addr = (_ip.empty() == true? INADDR_ANY : inet_addr(_ip.c_str()));//把_ip从string类转换成in_addr_t类(即uint32_t类),然后会自动将转化出的整数从主机字节序变为网络字节序if(bind(_sock, (sockaddr*)&local, sizeof(local)) < 0){cout<<"bind绑定失败"<<endl;exit(1);}}void Start(){char c[1024];//当前Start函数是服务端调用的函数,用于获取、分析、处理客户端的信息并反馈给客户端,服务端读取到的客户端的信息就存放在数组c中sockaddr_in client_info;//纯输出型参数,作为参数传给recvfrom函数后即可获取客户端的ip和port(需要获取的原因是:当前Start函数是服务端调用的函数,用于获取、分析、处理客户端的信息并反馈给客户端,所以是需要客户端的ip和port的),注意因为这里是作为输出型参数,所以原则上不必调用bzero函数初始化。socklen_t client_info_len = sizeof(client_info);//输入输出型参数,作为参数传给recvfrom函数。输入时的值为:client_info对象所占的空间大小/输出时的值为:实际填充进client_info的数据的大小while(1){ssize_t size = recvfrom(_sock, (void*)c, sizeof(c), 0,(sockaddr*)&client_info, &client_info_len);//从客户端读取消息。函数需要的参数flag设置为0即可,不必关心其含义。recvfrom的返回值为实际读取到的字节个数。if (size > 0){//走到这里服务端已经读取到了客户端发过来的信息c[size] = 0;//因为在设计客户端与服务端通信时,设计的逻辑是双方都只发送C语言字符串,所以通信通道中的信息就全是C语言字符串,所以这里需要在读到的数据的末尾设置0,防止打印时出现乱码uint16_t port = ntohs(client_info.sin_port);//把从网络中来的port从网络字节序转化成主机字节序string ip = (inet_ntoa(client_info.sin_addr));//会自动把从网络中来的uint16_t整形的ip地址先从网络字节序转化成主机字节序,然后将主机字节序的整形ip转化string类型的ipprintf("[%s][%d]#:%s\n",ip.c_str(), port, c);}/*走到这里开始处理从客户端读到的信息,这需要经过一段的时间。*///把从客户端读取到的信息处理完毕后,向客户端发出反馈信息sendto(_sock, c, strlen(c), 0, (sockaddr*)&client_info, client_info_len);}}private://一个服务器是需要ip和端口号的,不然其他机器找不到该服务器string _ip;uint16_t _port;//port表示端口,uint16_t是C语言中stdint.h头文件中定义的一种数据类型,它占据16个二进制位,范围从0到65535。它是无符号整数类型,即只能表示非负整数,没有符号位。int _sock;//_sock是调用socket函数创建套接字时返回的值,本质就是文件描述符fd
};

正式开始网络测试

将上文中修改过的udp_server.cc和自始至终都没被修改过的udp_client.cc文件编译好后,直接启动服务端,注意根据我们编码的逻辑,在启动服务端进程时已经不用把服务端进程需要bind绑定的ip设置成命令行参数了,只在命令行参数中传入服务端进程所需要bind绑定的端口号port即可,如下图右半部分所示。

在<<套接字socket编程的基础知识点>>一文中说过,当前进程bind绑定INADDR_ANY地址后,不光可以让其他主机上的进程访问当前进程,是还可以让本地(即本机)上的其他进程访问当前进程的。所以下图右半部分的上面,我们测试了本地的客户端进程向服务端进程发送信息,可以发现测试成功。

那其他主机上的进程如何向本机的服务端进程发送信息呢?说一下,因为我们客户端的代码(即udp_client.cc文件)是使用的Linux的系统接口编写的,所以通过该文件编译出的可执行程序也只能在Linux机器上跑,所以你得先输入sz指令将编译好的客户端的可执行文件从云服务器上下载到本机(Windows系统),如下图所示,指令的格式为【sz+客户端的可执行文件的文件名】,然后将该文件发给你的小伙伴,当你的朋友收到这个客户端的可执行程序后,可以通过rz命令或拖拽的方式将这个可执行程序上传到他的云服务器上,然后通过chmod命令给该文件加上可执行权限。因为此时你的服务端进程已经启动了(即在上图中就已经启动了),所以你的朋友在命令行中输入指令【./udp_client+服务端进程所在的云服务器的虚拟ip地址+8080】即可连接成功,就可以正常通信了,这就是一个简易版本的网络服务器了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/141583.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Tomcat中文路径目录

一、问题描述 linux环境下tomcat发布了包含中文名字的页面和文件&#xff0c;浏览器访问报404&#xff0c;非中文页面没有问题&#xff1b;本人为RP设计的原型图发布&#xff0c;其中包含了大量的中文文件和路径 二、解决步骤 第一步&#xff0c;设置tomcat&#xff0c;配置…

vue里使用elementui的级联选择器el-cascader进行懒加载的怎么实现数据回显?

需要实现的懒加载回显效果 比如&#xff1a;后端返回数据 广东省/广州市/天河区 &#xff1a;440000000000/440100000000/440106000000&#xff0c;需要我们自动展开到天河区的下一级&#xff0c;效果如下 代码实现 我的实现思路就是拿到 440000000000/440100000000/44010600…

Java版本企业工程项目管理系统平台源码(三控:进度组织、质量安全、预算资金成本、二平台:招采、设计管理)

工程项目管理软件&#xff08;工程项目管理系统&#xff09;对建设工程项目管理组织建设、项目策划决策、规划设计、施工建设到竣工交付、总结评估、运维运营&#xff0c;全过程、全方位的对项目进行综合管理 工程项目各模块及其功能点清单 一、系统管理 1、数据字典&#…

JavaScript 基础第三天笔记

JavaScript 基础第三天笔记 if 多分支语句和 switch的区别&#xff1a; 共同点 都能实现多分支选择&#xff0c; 多选1大部分情况下可以互换 区别&#xff1a; switch…case语句通常处理case为比较确定值的情况&#xff0c;而if…else…语句更加灵活&#xff0c;通常用于范围…

[每周一更]-(第64期):Dockerfile构造php定制化镜像

利用php官网镜像php:7.3-fpm&#xff0c;会存在部分插件缺失的情况&#xff0c;自行搭建可适用业务的镜像&#xff0c;才是真理 Dockerhub 上 PHP 官方基础镜像主要分为三个分支&#xff1a; cli: 没有开启 CGI 也就是说不能运行fpm。只可以运行命令行。fpm: 开启了CGI&#x…

PHP后台实现微信小程序登录

微信小程序官方给了十分详细的登陆时序图&#xff0c;当然为了安全着想&#xff0c;应该加上签名加密。 微信小程序端 1).调用wx.login获取 code 。 2).调用wx.getUserInfo获取签名所需的 rawData , signatrue , encryptData 。 3).发起请求将获取的数据发送的后台。 login: …

Spring学习笔记5 GoF之工厂模式

Spring学习笔记4 Bean的作用域_biubiubiu0706的博客-CSDN博客 出了GoF23种设计模式.还有javaee的设计模式(DAO模式,MVC模式) 设计模式:是一种可以被重复利用的解决方案 GoF23种设计模式可分为三大类: 创建型(5个):解决对象创建问题. 单例模式&#xff0c;工厂方法模式&#x…

Windows--Python永久换下载源

1.新建pip文件夹&#xff0c;注意路径 2.在上述文件中&#xff0c;新建文件pip.ini 3.pip.ini记事本打开&#xff0c;输入内容&#xff0c;保存完事。 [global] index-url https://pypi.douban.com/simple

​旅行季《乡村振兴战略下传统村落文化旅游设计》许少辉八一著作想象和世界一样宽广

​旅行季《乡村振兴战略下传统村落文化旅游设计》许少辉八一著作想象和世界一样宽广

计算机毕业设计 基于微信小程序的校园商铺系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

基于SpringBoot的甘肃非物质文化网站设计与实现

目录 前言 一、技术栈 二、系统功能介绍 用户信息管理 商品分类管理 申请信息管理 订单信息管理 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#x…

lv5 嵌入式开发-6 线程的取消和互斥

目录 1 线程通信 – 互斥 2 互斥锁初始化 – pthread_mutex_init 3 互斥锁销毁 pthread_mutex_destroy 4 申请锁 – pthread_mutex_lock 5 释放锁 – pthread_mutex_unlock 6 读写锁 7 死锁的避免 8 条件变量&#xff08;信号量&#xff09; 9 线程池概念和实现 9.1 …

bean的生命周期

配置生命周期 方法一:在配置文件中配置这些属性 方法二:在类上实现接口 生命周期的具体过程 补充 bean的销毁方法默认是不会触发的 必须手动关闭容器,在虚拟机退出之前(程序执行完虚拟机就退出了) configurablleApplicationContext中才有close这个方法

windows下实现mysql8的主从复制

1、下载mysql8的安装包 MySQL :: Download MySQL Community Server 2、放到指定目录进行解压&#xff0c;更改名称为mysql-8.1.0-winx64-master,并复制一份作为从数据库 3、在bin目录下创建一个my.ini文件 添加如下内容 [mysqld] basedir"D:/soft/mysql/mysql-8.1.0-win…

Linux 系统死机后挽救措施

一、背景 因我们日常使用Linux系统过程中&#xff0c;会不时遇到系统崩溃的事&#xff0c;但这时系统界面除了呈现一片告警字符外&#xff0c;无发执行任何其他操作&#xff0c;留给我们的要不重启&#xff0c;要不就是尴尬等待指令。那面对会这种情况&#xff0c;还到底有没有…

App Inventor 2 模拟sleep函数

App Inventor 2 原生没有 sleep 及相关函数&#xff0c;需要模拟实现&#xff0c;经过测试这里给出一个既简单又相对高效率的实现方案&#xff1a; 需要用到计时器组件&#xff1a; 实现代码如下&#xff1a; 代码原理非常简单&#xff0c;就是计算好要 sleep 到的时刻&#x…

PHP脚本导出MySQL数据库

背景&#xff1a;有时候需要同步数据库的表结构和部分数据&#xff0c;同步全表数据非常大&#xff0c;也不适合。还有一个种办法是使用数据库的dump命令执行备份&#xff0c;无法进入服务器&#xff1f;没有权限怎么办&#xff1f; 这里只要能访问服务器中的 information_sch…

http的get与post

get方法&#xff1a; 这个网址可以获取配置信息&#xff08;我把部分位置字符改了&#xff0c;现在打不开了&#xff0c;不然会被追责&#xff09; http://softapi.s103.cn/addons/Kmdsoft/Index/config?productwxdk&partner_id111122&osWindows&os_version11&am…

人机逻辑中的家族相似性与非家族相似性

维特根斯坦的家族相似性理论是他在《哲学研究》中提出的一个重要概念。他认为&#xff0c;语言游戏是一种人们使用语言的方式&#xff0c;不同的语言游戏之间可能存在相似性&#xff0c;就像一个家族的成员之间存在相似性一样。维特根斯坦认为&#xff0c;相似性不是通过一个共…

二维空间 点绕点旋转公式

记录一下 点绕点旋转公式的推导 点A绕点B逆时针旋转贝塔角度 点A绕点B顺时针旋转贝塔角度 贝塔<阿尔法 点A绕点B顺时针旋转贝塔角度 贝塔>阿尔法