基于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的成员变量如下。

cpp 复制代码
#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、析构函数的代码如下。

cpp 复制代码
#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));//该函数用于把从某个地址开始以及向后的若干字节上的值置为0
        local.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函数的代码如下。

cpp 复制代码
#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类型的ip
                printf("[%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文件的代码。

cpp 复制代码
#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));//该函数用于把从某个地址开始以及向后的若干字节上的值置为0
        local.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类型的ip
                printf("[%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的代码是在上文中模拟实现出来的)

cpp 复制代码
#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从主机字节序转化成网络字节序。需要进行这些操作的原因在讲解客户端时都已经说过了,这里不再赘述。

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

cpp 复制代码
#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后的服务端的代码如下。

cpp 复制代码
#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的整体代码如下。

cpp 复制代码
#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));//该函数用于把从某个地址开始以及向后的若干字节上的值置为0
        local.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类型的ip
                printf("[%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】即可连接成功,就可以正常通信了,这就是一个简易版本的网络服务器了。

相关推荐
会员源码网9 分钟前
理财源码开发:单语言深耕还是多语言融合?看完这篇不踩坑
网络·个人开发
米羊1211 小时前
已有安全措施确认(上)
大数据·网络
Fcy6481 小时前
Linux下 进程(一)(冯诺依曼体系、操作系统、进程基本概念与基本操作)
linux·运维·服务器·进程
袁袁袁袁满1 小时前
Linux怎么查看最新下载的文件
linux·运维·服务器
主机哥哥2 小时前
阿里云OpenClaw部署全攻略,五种方案助你快速部署!
服务器·阿里云·负载均衡
ManThink Technology2 小时前
如何使用EBHelper 简化EdgeBus的代码编写?
java·前端·网络
珠海西格电力科技3 小时前
微电网能量平衡理论的实现条件在不同场景下有哪些差异?
运维·服务器·网络·人工智能·云计算·智慧城市
QT.qtqtqtqtqt3 小时前
未授权访问漏洞
网络·安全·web安全
释怀不想释怀3 小时前
Linux环境变量
linux·运维·服务器
zzzsde3 小时前
【Linux】进程(4):进程优先级&&调度队列
linux·运维·服务器