• Linux:Socket套接字编程 | UDP



    全文约 9520 字,预计阅读时长: 27分钟


    端口号

    • 一个IP地址,标识一个网络中的一台计算机;一个端口号标识该计算机上的一个进程
    • 端口号(port)是传输层协议的内容:
      • 端口号是一个2字节16位的整数;
      • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
      • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
      • 一个端口号只能被一个进程占用.
    • 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定。
      • 比如一个人在不同的场景,有工号,学号等。
      • 虽然场景号码名字不一样,但都标识唯一的一个人。
    • 传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号目的端口号。就是在描述 “数据是谁发的, 要发给谁”。

    网络字节序

    • 网络数据流同样有大端小端之分。
      • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
      • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。数据的高位放在低地址上。
      • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
    • 为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
      • 计算机网络——网络字节序 | ip地址转换函数
      • h 表示host,n 表示network,l 表示32位长整数,s 表示16位短整数。
      • 例如 htonl 表示将32位的长整数,从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
      • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
    #include 
    
    //将主机字节序转换为网络字节序
     unit32_t htonl (unit32_t hostlong);
     unit16_t htons (unit16_t hostshort);
     //将网络字节序转换为主机字节序
     unit32_t ntohl (unit32_t netlong);
     unit16_t ntohs (unit16_t netshort);
    
     说明:h -----host;n----network ;s------short;l----longhtons()--"Host to Network Short"
    htonl()--"Host to Network Long"
    ntohs()--"Network to Host Short"
    ntohl()--"Network to Host Long"
    ————————————————
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    套接字

      一个IP地址加一个端口号可以标识网络中某个主机上一个进程,因此网络也是进程间通信的一种,网络就是那份公共资源。操作系统提供了socket API的这样一层抽象的网络编程接口,有对应的属性和方法来完成两个不同主机的进程间的通信。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。

      套接字是一套用C语言写成的应用程序开发库,它首先是一个库。主要作用就是实现进程间通信和网络编程,因此在网络应用开发中被广泛使用。套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序与网络中的其他应用程序进行通信。**网络套接字是IP地址与端口的组合。**实际开发中使用的套接字可以分为三类:流套接字(TCP套接字(SOCK-STREAM))数据报套接字(SOCK_DGRAM)原始套接字(SOCK-RAW)

      数据报套接字(SOCK_DGRAM)提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User Datagram Protocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。

      流套接字(SOCK_STREAM)用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了TCP(The Transmission Control Protocol)协议传输控制协议

      原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接。


    socket编程接口

    • socket 常见API
    // 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
    int socket(int domain, int type, int protocol);
    // 绑定端口号 (TCP/UDP, 服务器) 
    int bind(int socket, const struct sockaddr *address,
     socklen_t address_len);
    // 开始监听socket (TCP, 服务器)
    int listen(int socket, int backlog);
    // 接收请求 (TCP, 服务器)
    int accept(int socket, struct sockaddr* address,
     socklen_t* address_len);
    // 建立连接 (TCP, 客户端)
    int connect(int sockfd, const struct sockaddr *addr,
     socklen_t addrlen);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    struct sockaddr 这个结构体是linux的网络编程接口中用来表示IP地址的标准结构体,bind、connect等函数中都需要这个结构体,这个结构体是兼容IPV4和IPV6的。

    • sockaddr结构:
      • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址。
      • IPv4、IPv6地址类型分别定义为常数AF_INETAF_INET6。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
      • socket API 都用struct sockaddr *类型表示,在使用的时候需要强制转化成struct sockaddr *,这样的好处是程序的通用性,可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
        在这里插入图片描述
        sockaddr_in结构体:有些内容需要自己填充。
    truct sockaddr_in {
      short int sin_family; /* Address family */
      unsigned short int sin_port; /* Port number */
      struct in_addr sin_addr; /* Internet address */
      unsigned char sin_zero[8]; /* Same size as struct sockaddr */
      };
      sin_family:指代协议族,在socket编程中只能是AF_INET
      sin_port:存储端口号(使用网络字节顺序)
      sin_addr:存储IP地址,使用in_addr这个数据结构
      sin_zero:是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。
       in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    :所有的接口都是struct sockaddr类型,如何区分对于具体的传入使用哪个结构体?

    答案是:提取第一个字段进行if判断,如果为AF_INET则为struct sockaddr_in。而且每个结构的大小不同,所以还需要传入用户层实际结构体的长度进行处理。如此一来就达到同一套接口,传入参数的不同,进行不同的函数操作,达到了静态多态——函数重载。

    :为什么在数据结构 struct sockaddr_in 中, sin_addrsin_port 需要转换为网络字节顺序,而sin_family 需不需要呢?

    答案是: sin_addrsin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须要 是网络字节顺序。但是 sin_family 域只是被内核 (kernel) 使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时, sin_family 没有发送到网络上,它们可以是本机字节顺序。


    数据报套接字:UDP

      数据报套接字使用UDP( User Datagram Protocol)协议进行数据的传输,提供一种无连接的服务,该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。实现起来比较简单。

      UDP服务端:利用系统提供的socket接口创建套接字,返回一个文件描述符;然后将主机IP和端口号绑定至特定的网络文件;接着因为服务器是一个软件 程序,周而复始一直在运行,永远不退出,所以死循环接受数据,处理数据,返回数据;最后关闭套接字文件。

      UDP客户端:创建套接字网络文件;不需要绑定,因为客户端不是必须哪一个端口,只要有一个端口就行,由操作系统自动分配;然后发送数据,接受服务端返回的数据;最后关闭套接字避免文件描述符泄露。为什么服务器端需要绑定?服务器是向外提供服务的,需要尽可能地将自己的IP暴露出去,端口号一般隐藏;这两者必须是稳定的,尤其是端口号,本服务进程独有。


    创建和绑定

    • 创建套接字文件描述符int socket(int domain, int type, int protocol);
      • domainAF_INET IPv4 Internet protocols,使用哪种格式的IP协议。
      • type:使用传输层的哪种协议。TCP: SOCK_STREAMUDP:SOCK_DGRAM
      • protocol:默认为0。协议指定与套接字一起使用的特定协议,通常只有一个协议支持给定协议族中的特定套接字类型,在这种情况下,协议可以指定为0。
      • 返回值:文件描述符
     #include         
     #include 
     int sock = socket(AF_INET, SOCK_DGRAM, 0); //用户数据报 ipv4 udp
    
    • 1
    • 2
    • 3
    • 绑定就是将本主机相关的ip,端口,协议家族等信息写入到特定的fd标定的文件中:
    • int bind(int socket, const struct sockaddr *address, socklen_t address_len);
      • socket:创建的文件描述符
      • const struct sockaddr *address:输入型参数;IPV4格式的需要传入 struct sockaddr_in,然后强转;
        • sockaddr_in:里面有些内容需要自己填充。里面的端口号要转网络字节序,IP地址也要转。
        • 注意: 云服务器你要bind的时候的,一般不能直接绑定明确的IP,非常推荐使用INADDR_ANY, bind所有你的机器上面的ip。
      • socklen_t address_len:输入性参数;上面该结构体的大小。
      • 返回值:成功为0;失败-1。
    • 一个IP本质,可以使用4个字节进行保存[0-255].[0-255].[0-255].[0-255],“42.192.83.143”,是点分十进制字符串风格的IP。
    #define PORT 8081
     //该结构是OS给你提供的一个结构体,用户层定义的,local是属于main函数内的一个临时变量
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET; //使用的IP类型
        local.sin_port = htons(PORT); //后续网络端口,会以源端口的方式,发送给对面
        local.sin_addr.s_addr = htonl(INADDR_ANY); 
        if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        { 
            std::cerr << "bind error" << std::endl;
            return 3;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    数据的接受和发送。

    • 接受数据:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
      • sockfd:对应操作的套接字文件描述符,表示从该文件描述符索引的文件当中读取数据
      • buf:读取数据的存放位位置
      • len:期望读取数据的字节数
      • flags:读取的方式,一般设置为0,表示阻塞读取。
      • src_addr输入输出型参数;保存对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
      • addrlen输入输出型参数;上述结构体的长度。
      • 返回值:读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。
     char message[1024];
        for( ; ; ){
            memset(message, 0, sizeof(message));
            
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            
            ssize_t s = recvfrom(sock, message, sizeof(message)-1, 0, (struct sockaddr*)&peer, &len);
            if(s > 0)
            {
            	//处理.....
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 发送数据:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
      • sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中
      • buf:待写入数据的存放位置
      • len:期望写入数据的字节数
      • flags:写入的方式,一般设置为0,表示阻塞写入
      • dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等
      • addrlen:传入dest_addr结构体的长度
      • 返回值:入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。
    ...
    if(s > 0)
      {
         //处理.....
         message[s]='\0';
         std::cout<<"cilent: "<<message<<std::endl;
         std::string echo_mess = message;
         echo_mess += " _server_";
         sendto(sock_fd, echo_mess.c_str(),echo_mess.size(), 0, (struct sockaddr*)&peer,len);   	
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    UDP服务端

    • 综上的一个完整的服务端流程:使用 命令行 main 函数传参的方式完成端口号的绑定。
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void Usage(std::string proc)
    {
        std::cerr << "Usage: " << "\n\t" << proc << " local_port" << std::endl;
    }
    // ./udp_server port
    int main(int argc, char *argv[])
    {
        if(argc != 2){
            Usage(argv[0]);
            return 1;
        }
        int sock = socket(AF_INET, SOCK_DGRAM, 0); //用户数据报
        if(sock < 0){
            std::cerr << "socket error" << std::endl;
            return 2;
        }
        std::cout <<"sock: " << sock << std::endl;
    
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(atoi(argv[1])); 
        local.sin_addr.s_addr = htonl(INADDR_ANY); 
    
        if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            std::cerr << "bind error" << std::endl;
            return 3;
        }
    
        //服务器是一个软件 程序,周而复始一直在运行,永远不退出
        char message[1024];
        for( ; ; )
        {
            memset(message, 0, sizeof(message));
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t s = recvfrom(sock, message, sizeof(message)-1, 0, (struct sockaddr*)&peer, &len);
            if(s > 0)  //对客户端的数据简单的原样返回。
            {
                message[s] = '\0';
                std::cout << "client# " << message << std::endl;
                std::string echo_message = message;
                echo_message += "_server_";
                echo_message += std::to_string((long long)time(nullptr));
                sendto(sock_fd, echo_mess.c_str(),echo_mess.size(), 0, (struct sockaddr*)&peer,len);
            }
        }
        close(sock);
        return 0;
    }
    [saul@VM-12-7-centos tt815]$ ./udp_server 8081
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • makefile 文件:
    .PHONY:all
    all:udp_client udp_server
    
    udp_client:udp_client.cc
    	g++ -o $@ $^ -std=c++11 -static
    udp_server:udp_server.cc
    	g++ -o $@ $^ -std=c++11
    
    .PHONY:clean
    clean:
    	rm -f udp_client udp_server
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    UDP客户端

    • 向服务器发送数据,接受数据,不需绑定。
    • 以命令行参数的方式传入 服务器的IP地址和 端口号。
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    // ./udp_client 127.0.0.1 8081
    void Usage(std::string proc) //提示使用方式不对
    {
        std::cerr << "Usage: " << "\n\t" << proc << " desc_ip desc_port" << std::endl;
    }
    int main(int argc, char *argv[])
    {
        if( argc != 3 ){
            Usage(argv[0]);
            return 1;
        }
    
        int sock = socket(AF_INET, SOCK_DGRAM, 0);
        if(sock < 0){
            std::cerr << "socket error" << std::endl;
            return 2;
        }
        char buffer[1024];
    
        struct sockaddr_in desc;
        memset(&desc, 0, sizeof(desc));
        desc.sin_family = AF_INET;
        desc.sin_port = htons(atoi(argv[2]));
        desc.sin_addr.s_addr = inet_addr(argv[1]);
    
        for( ; ; ){
            std::cout << "please enter ";
            fflush(stdout);
            buffer[0] = 0;
            ssize_t size = read(0, buffer, sizeof(buffer)-1); //从标准输入读取数据
            if(size > 0){
                buffer[size - 1] = 0;
                sendto(sock, buffer, strlen(buffer), 0, (struct sockaddr*)&desc/*发送到哪里??*/, sizeof(desc)/*长度*/);
    
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                ssize_t s = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len); //peer, len,暂时不用
                if(s > 0){
                    buffer[s] = 0;
                    std::cout << "echo# " << buffer << std::endl;
                }
            }
        }
        close(sock);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    测试:编译后,先运行服务器端,用 netstat //查看网络状态 -nlup 查看UDP进程;再运行客户端,ctrl c结束进程。
    客户端填IP时,地址用:127.0.0.1测试,本地环回IP。


    UDP服务端:处理数据改进

    • 客户端发送命令,服务端执行,返回执行结果。
    • 方案一:字符串分割解析,进程程序替换
    .....
    if(s > 0)
    {        
       char *command[64] = {0};
       command[0] = strtok(message, " ");
       int i = 1;
       while(command[i] = strtok(NULL, " "))
       {
      	 i++;
       }
    
       if(fork() == 0)
       {//child
         execvp(command[0], command);
         std::cerr << "client message# " << command << std::endl;
         exit(4);
       }
       sendto(sock, echo_message.c_str(), echo_message.size(), 0, (struct sockaddr*)&peer, len);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 方案二:FILE *popen(const char *command, const char *type);
      • command:可以执行任何命令;后台自动创建子进程去执行命令,通过文件指针的方式返回,文件中保存的是执行结果。
      • char* type:打开文件的方式。
    ....
    if(s > 0){
                //command,命令行命令 , ls -a -l -i
                FILE *in = popen(message, "r");
                if(in == nullptr)
                {
                    continue;
                }
    
                std::string echo_message;
                char line[128];
                while(fgets(line, sizeof(line), in))
                {
                    echo_message += line;
                }
                sendto(sock, echo_message.c_str(), echo_message.size(), 0, (struct sockaddr*)&peer, len);
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    寄语

    • 一般都是固定的套路,区别在于对接受到的数据进行怎样的处理。
  • 相关阅读:
    SpringMVC 学习(二)Hello SpringMVC
    mysql备份与恢复
    sed 原地替换文件时遇到的趣事
    .9图片详解
    自学网络编程第一天- HTTP基础:基础原理、Request请求、Response应答
    Java回顾-Collection-Set-HashSet/LinkedHashSet/TreeSet的对比
    app测试定位方式
    软件设计师考试---标题、判定表、页式存储管理器、快速原型模型、三层C/S结构、耦合类型,
    数据技术前沿趋势、TiDB 产品方向、真实场景 Demo… 丨PingCAP DevCon 2022 产品技术论坛预览
    Fast semi-supervised learning with anchor graph for large
  • 原文地址:https://blog.csdn.net/WTFamer/article/details/126342443