• TCP/IP网络编程之套接字的多种可选项


    套接字可选项进而I/O缓冲大小

    我们进行套接字编程时往往只关注数据通信,而忽略了套接字具有的不同特性。但是,理解这些特性并根据实际需要进行更改也十分重要。之前我们写的程序在创建好套接字后都是未经特别操作就直接使用,此时通过默认的套接字特性进行数据通信。之前的示例比较简单,无需特别操作套接字特性,但有时的确需要更改,表1-1列出一部分套接字可选项

    表1-1   可设置套接字的多种可选选项 
    协议层 选项名 说明 读取 设置
    SOL_SOCKET          SO_SNDBUF  发送缓冲区大小  O  O
     SO_RCVBUF  接收缓冲区大小  O  O
     SO_REUSEADDR  是否启用地址再分配,主要原理是操作关闭套接字的Time-wait时间等待的开启和关闭  O  O
     SO_KEEPALIVE  开启套接字保活机制  O  O
     SO_BROADCAST  允许或禁止发送广播数据  O  O
     SO_DONTROUTE  打开或关闭路由查找功能  O  O
     SO_OOBINLINE  该数据字节并不放入套接字接收缓冲区,而是被放入该连接的一个独立的单字节带外缓冲区  O  O
     SO_ERROR  获得套接字错误  O  X
     SO_TYPE  获得套接字类型(这个只能获取,不能设置)  O  X
     IPPROTO_IP      IP_TOS  设定该字段的值,以区分不同服务的优先级  O
     IP_TTL  设置主机发送数据包的生存时间
     IP_MULTICAST_TTL  生存时间(Time To Live),组播传送距离
     IP_MULTICAST_LOOP  禁止组播数据回送
     IP_MULTICAST_IF  取默认接口或默认设置
     IPPROTO_TCP    TCP_KEEPALIVE  TCP保活机制开启下,设置保活包空闲发送时间间隔  O
     TCP_NODELAY  不使用Nagle算法  O O
     TCP_MAXSEG  TCP最大数据段的大小

    从表1-1可以看出,套接字可选项是分层的。IPPROTO_IP层可选项是IP协议相关事项,IPPROTO_TCP层可选项是TCP协议相关的事项,SOL_SOCKET层是套接字相关的通用可选项。

    getsockopt和setsockopt

    我们几乎可以针对表1-1中的所有可选项进行读取(Get)和设置(Set),可选项的读取和设置通过如下两个函数完成:

    #include <sys/socket.h>
    int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);//成功时返回0,失败时返回-1
    

      

    • sock:用于查看选项套接字文件描述符
    • level:要查看的可选项的协议层
    • optname:要查看的可选项名
    • optval:保存查看结果的缓冲地址值
    • optlen:向第四个参数optval传递的缓冲大小,调用函数后,该变量中保存通过第四个参数返回的可选项信息的字节数

    上述函数用于读取套接字可选项,并不难,接下来介绍更改可选项时调用的函数

    #include <sys/socket.h>
    int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);//成功时返回0,失败时返回-1
    

      

    • sock:用于更改可选项的套接字文件描述符
    • level:要更改的可选项的协议层
    • optname:要更改的可选项名
    • optval:保存要更改的选项信息的缓冲地址值
    • optlen:向第四个参数optval传递的可选项信息的字节数

    接下来介绍这些函数的调用方法,我们先介绍getsockopt函数的调用方法,setsockopt函数的调用方法将在其他的示例中给出。下面示例用协议层为SOL_SOCKET、名为SO_TYPE的可选项查看套接字类型(TCP或UDP)

    sock_type.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/socket.h>
    void error_handling(char *message);
    
    int main(int argc, char *argv[])
    {
        int tcp_sock, udp_sock;
        int sock_type;
        socklen_t optlen;
        int state;
    
        optlen = sizeof(sock_type);
        tcp_sock = socket(PF_INET, SOCK_STREAM, 0);
        udp_sock = socket(PF_INET, SOCK_DGRAM, 0);
        printf("SOCK_STREAM: %d 
    ", SOCK_STREAM);
        printf("SOCK_DGRAM: %d 
    ", SOCK_DGRAM);
    
        state = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void *)&sock_type, &optlen);
        if (state)
            error_handling("getsockopt() error!");
        printf("Socket type one: %d 
    ", sock_type);
    
        state = getsockopt(udp_sock, SOL_SOCKET, SO_TYPE, (void *)&sock_type, &optlen);
        if (state)
            error_handling("getsockopt() error!");
        printf("Socket type two: %d 
    ", sock_type);
        return 0;
    }
    
    void error_handling(char *message)
    {
        fputs(message, stderr);
        fputc('
    ', stderr);
        exit(1);
    }
    

        

    • 第15、16行:分别生成TCP、UDP套接字
    • 第17、18行:输出创建TCP、UDP套接字时传入的SOCK_STREAM、SOCK_DGRAM
    • 第20、25行:获取套接字类型信息,如果是TCP套接字,将获得SOCK_STREAM常数值1;如果是UDP套接字,则获得SOCK_DGRAM的常数值2

    编译sock_type.c并运行

    # gcc sock_type.c -o sock_type
    # ./sock_type 
    SOCK_STREAM: 1 
    SOCK_DGRAM: 2 
    Socket type one: 1 
    Socket type two: 2 
    

      

    上述示例给出了调用getsockopt函数查看套接字信息的方法,另外,用于验证套接字类型的SO_TYPE是典型的只读可选项,即套接字类型只能在创建时决定,以后不能再更改

    SO_SNDBUF和SO_RCVBUF

    前面介绍过,创建套接字将同时生成I/O缓冲,SO_RCVBUF是输入缓冲大小相关可选项,SO_SNDBUF是输出缓冲大小相关可选项。用这两个可选项可以读取和修改当前I/O缓冲大小。通过下面的示例读取创建套接字时默认的I/O缓冲大大小

    get_buf.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/socket.h>
    void error_handling(char *message);
    
    int main(int argc, char *argv[])
    {
        int sock;
        int snd_buf, rcv_buf, state;
        socklen_t len;
    
        sock = socket(PF_INET, SOCK_STREAM, 0);
        len = sizeof(snd_buf);
        state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, &len);
        if (state)
            error_handling("getsockopt() error");
    
        len = sizeof(rcv_buf);
        state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, &len);
        if (state)
            error_handling("getsockopt() error");
    
        printf("Input buffer size: %d 
    ", rcv_buf);
        printf("Outupt buffer size: %d 
    ", snd_buf);
        return 0;
    }
    
    void error_handling(char *message)
    {
        fputs(message, stderr);
        fputc('
    ', stderr);
        exit(1);
    }
    

      

    编译get_buf.c并运行

    # gcc get_buf.c -o get_buf
    # ./get_buf 
    Input buffer size: 87380 
    Outupt buffer size: 16384 
    

      

    这是我系统的运行结果,不同系统可能默认的输入缓冲和输出缓冲有所差异,接下来,我们通过程序修改I/O缓冲大小

    set_buf.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/socket.h>
    void error_handling(char *message);
    
    int main(int argc, char *argv[])
    {
        int sock;
        int snd_buf = 1024 * 3, rcv_buf = 1024 * 3;
        int state;
        socklen_t len;
    
        sock = socket(PF_INET, SOCK_STREAM, 0);
        state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, sizeof(rcv_buf));
        if (state)
            error_handling("setsockopt() error!");
    
        state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, sizeof(snd_buf));
        if (state)
            error_handling("setsockopt() error!");
    
        len = sizeof(snd_buf);
        state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, &len);
        if (state)
            error_handling("getsockopt() error!");
    
        len = sizeof(rcv_buf);
        state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, &len);
        if (state)
            error_handling("getsockopt() error!");
    
        printf("Input buffer size: %d 
    ", rcv_buf);
        printf("Output buffer size: %d 
    ", snd_buf);
        return 0;
    }
    
    void error_handling(char *message)
    {
        fputs(message, stderr);
        fputc('
    ', stderr);
        exit(1);
    }
    

      

    • 第15、19行:I/O缓冲大小更改为3M字节
    • 第24、29行:为了验证I/O缓冲的更改,读取缓冲大小

    编译set_buf.c并运行

    # gcc set_buf.c -o set_buf
    # ./set_buf 
    Input buffer size: 6144 
    Output buffer size: 6144
    

      

    输出结果和我们预想的完全不同,但也算合理,缓冲大小的设置需谨慎,因此不会完全按照我们的要求进行,只是通过setsockopt函数向系统传递我们的要求。如果把输出缓冲设置为0并如实反映这种设置,TCP协议将如何进行?如果要实现流控制和错误发生时的重传机制,至少要有一些缓冲空间吧?上述示例虽没有完全按照我们的要求设置缓冲大小,但也大致反映出可以通过setsockopt函数设置缓冲大小

    SO_REUSEADDR

    学习SO_REUSEADDR之前,应先理解好Time-wait状态,我们看完下面的示例在了解后面的内容

    reuseadr_eserver.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    
    #define TRUE 1
    #define FALSE 0
    void error_handling(char *message);
    
    int main(int argc, char *argv[])
    {
        int serv_sock, clnt_sock;
        char message[30];
        int option, str_len;
        socklen_t optlen, clnt_adr_sz;
        struct sockaddr_in serv_adr, clnt_adr;
    
        if (argc != 2)
        {
            printf("Usage : %s <port>
    ", argv[0]);
            exit(1);
        }
    
        serv_sock = socket(PF_INET, SOCK_STREAM, 0);
        if (serv_sock == -1)
            error_handling("socket() error");
        /*
    	optlen=sizeof(option);
    	option=TRUE;	
    	setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    	*/
    
        memset(&serv_adr, 0, sizeof(serv_adr));
        serv_adr.sin_family = AF_INET;
        serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
        serv_adr.sin_port = htons(atoi(argv[1]));
    
        if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)))
            error_handling("bind() error ");
    
        if (listen(serv_sock, 5) == -1)
            error_handling("listen error");
        clnt_adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);
    
        while ((str_len = read(clnt_sock, message, sizeof(message))) != 0)
        {
            write(clnt_sock, message, str_len);
            write(1, message, str_len);
        }
        close(clnt_sock);
        return 0;
    }
    
    void error_handling(char *message)
    {
        fputs(message, stderr);
        fputc('
    ', stderr);
        exit(1);
    }
    

      

    此示例是之前已实现多次的回声服务端,可以结合TCP/IP网络编程之基于TCP的服务端/客户端(一)这一章中的回声客户端运行。下面运行该示例,第30到32行应保持注释状态,可通过在客户端控制台输入Q或CTRL+C终止程序。也就是说,让客户端先通知服务端终止程序,在客户端控制台输入Q消息时调用close函数,向服务端发送FIN消息并经过四次握手过程。当然,输入CTRL+C时也会向服务端发送FIN消息。强制终止程序时,由操作系统关闭文件及套接字,此过程相当于调用close函数,也会向服务端发送FIN消息

    服务端和客户端在已建立连接的状态下,向服务端控制台输入CTRL+C,即强制关闭服务端,这主要模拟了服务端向客户端发送FIN消息。但如果以这种方式终止程序,那么服务端重新运行将产生问题,如果用同一端口号重新运行服务端,将输出"bind() error"消息,并无法再次运行,需要等到两三分钟后才可重新运行服务端

    上述两种终止运行的方式唯一的区别在于是谁先输出FIN消息,但结果迥然不同,原因何在呢?

    Time-wait状态

    这里需要对四次握手有很好的理解,如果还有疑问请看TCP/IP网络编程之基于TCP的服务端/客户端(二)这一章

    图1-1   Time-wait状态下的套接字

    图1-1中主机A是服务端,因为主机A向主机B发送FIN消息,故可以想象服务端在控制台输入CTRL+C。但问题是,套接字经过四次握手过程后并非立即消除,而是要经过一段时间的Time-wait状态。当然,只有先断开连接(即先发送FIN消息)的主机才经过Time-wait状态。因此,若服务端先断开连接,则无法立即重新运行。套接字处在Time-wait过程时,相应端口是正在使用的状态

    刚才说过,先断开连接的主机的套接字,都会经过一段时间的Time-wait状态,因此,客户端或者服务端都有可能经历Time-wait状态,要看是谁先断开连接。但是客户端的套接字即便处在Time-wait状态也不要紧,因为客户端套接字的端口号是任意指定的,与服务端不同,客户端每次运行程序都动态分配端口号,因此无需太在意客户端的Time-wait状态

    那么到底为什么会有Time-wait状态呢?图1-1中假设主机A向主机B传输ACK消息(SEQ 5001、ACK 7502)后立即消除套接字,但最后这条ACK消息在传递途中丢失,未能传给主机B。这时会发生什么?主机B会认为之前自己发送的FIN消息(SEQ 7501、ACK 5001)未能抵达主机A,继而试图重传。但此时主机A已是完全终止的状态,主机B永远无法收到从主机A最后传来的ACK消息。相反,若主机A的套接字处于Time-wait状态,则会向主机B重传最后的ACK消息,主机B也可以正常终止。基于这些考虑,先传输FIN消息的主机应经过Time-wait过程

    地址再分配

    Time-wait看似很重要,但不一定讨人喜欢,考虑一下系统发生故障从而紧急停止的情况,这时候需要尽快重启服务端以提供服务,但因Time-wait状态而必须等待几分钟。因此,Time-wait并非只有优点,而且有些情况下可能引起更大的问题。图1-2演示了四次握手不得不延长Time-wait过程的情况

    图1-2   重启Time-wait计时器

    图1-2所示,在主机A的四次握手过程中,如果最后数据丢失,主机B会认为主机A未能收到自己发送的FIN消息,因此重传。此时,收到FIN消息的主机A将重启Time-wait计时器。因此,如果网络状况不理想,Time-wait状态将持续

    解决方案就是在套接字的可选项中更改SO_REUSEADDR的状态,适当调整该参数,可将Time-wait状态下的套接字端口重新分配给新的套接字。SO_REUSEADDR的默认值为0(假),这就意味着无法分配Time-wait状态下的套接字端口号,因此需要将这个值改成1(真)。具体做法已在示例reuseadr_eserver.c中给出,就是那段被注释的代码 

    optlen=sizeof(option);
    option=TRUE;	
    setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
    

      

    TCP_NODELAY

    Nagle算法是为防止因数据包过多而发生网络过载,该算法应用于TCP层,非常简单,其使用与否会导致图1-3所示的差异

    图1-3   Nagle算法

    图1-3展示了通过Nagle算法发送字符串“Nagle”和未使用Nagle算法的差别,可以得到一条结论:只有收到前一条数据的ACK消息时,Nagle算法才发送下一条数据。TCP套接字默认使用Nagle算法交换数据,因此最大限度地进行缓冲,直到收到ACK消息。图1-3左侧正是这种情况,为了发送“Nagle”字符串,将其传递到输出缓冲,这时头字符“N”之前没有其他数据(没有需接收的ACK),因此立即传输。之后开始等待字符“N”的ACK消息,等待过程中,剩下的“agle”填入输出缓冲。接下来,收到字符“N”的ACK消息后,将输出缓冲的“agle”装入一个数据包发送。也就是说,共需传递四个数据包以输出一个字符串

    接下来分析未使用Nagle算法时发送字符串“Nagle”的过程,假设字符“N”到“e”依序传输到输出缓冲,此时的发送过程与ACK接收与否无关,因此数据到达数据缓冲后立即被发送出去,从图1-3右侧可以看到,发送字符串“Nagle”共需十个数据包。由此可知,不使用Nagle算法将对网络流量产生负面影响。即使只传输一个字节,其头信息都有可能几十个字节,因此,为了提高网络传输效率,必须使用Nagle算法

    Nagle算法并不是什么时候都适用,根据传输数据的特性,网络流量未受太大影响时,不使用Nagle算法要比使用它时传输速度快,最典型的是“传输大文件数据”。将文件数据传入输出缓冲不会花太多时间,因此即便不使用Nagle算法,也会在装满输出缓冲时传输数据包,这不仅不会增加数据包的数量,反而会在无需等待ACK的前提下连续传输,因此可以大大提高传输速度

    一般情况下,不适用Nagle算法可以提高传输速度,但如果无条件放弃使用Nagle算法,就会增加过多的网络流量,反而会影响传输。因此,未准确判断数据特性时不应禁用Nagle算法

    禁用Nagle算法

    刚才说过的“大数据文件”应禁用Nagle算法,换言之,如果有必要,就应禁用Nagle算法。Nagle算法使用与否在于网络流量上差别不大,使用Nagle算法的传输速度更慢。禁用方法很简单,从下面代码可以看出,只需将套接字可选项TCP_NODELAY改为1(真)即可

    int opt_val = 1;
    setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, sizeof(opt_val));
    

      

    通过TCP_NODELAY的值查看Nagle算法的设置状态

    int opt_val;
    socklen_t opt_len;
    opt_len = sizeof(opt_val);
    getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, opt_len);
    

      

    如果正在使用Nagle算法,opt_val变量中会保存0,如果已禁用Nagle算法,则保存1

  • 相关阅读:
    Mysql数据备份命令
    git命令大全
    git 安装及普通命令
    git学习资料
    php 魔术方法 和 魔术常量
    react事件处理(绑定)
    java父类、子类构造函数调用过程
    java字符串String的intern()方法,举例说明
    java关键字static使用的“坑”(准备、初始化)
    java虚拟机
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/9688490.html
Copyright © 2020-2023  润新知