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

Posted beiluowuzheng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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

 

以上是关于TCP/IP网络编程之套接字的多种可选项的主要内容,如果未能解决你的问题,请参考以下文章

TCP/IP网络编程之套接字类型与协议设置

计算机网络—网络原理之TCP/IP协议

Java网络编程基础-- 基于TCP/IP的Socket编程

TCP/IP网络编程系列之三(初级)

HTTP 之 套接字

TCP/IP网络编程系列之二(初级)