网络编程那些事:listen 监听

2,547 阅读5分钟

  我们知道在网络编程中,listen接口是用来建立socket监听的,其参数只有两个。那么这个接口真的简单吗?它的第二个参数是什么?三次握手中的半连接、完全连接在listen监听中充当着什么角色?本文将一探究竟。

函数原型

  int listen(int sockfd, int backlog);

  当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。根据TCP状态转换图,调用listen导致套接字从CLOSED状态转换成LISTEN状态。

  sockfd:成功创建的TCP套接字

  backlog:定义内核监听队列的最大长度。APUE中指出,backlog只是一个提示,具体的数值实际上由系统决定。在内核版本2.2之前的Linux中,backlog参数是指所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket的上限。但自内核版本2.2之后,它只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。backlog参数的典型值是5(4.2BSD支持的最大值)。

监听队列

  内核为任何一个给定的监听套接字维护两个队列:

  1)、未完成连接队列,每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程。这些套接字处于SYN_RCVD状态。

  2)、已完成连接队列,每个已完成TCP三次握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。

案例分析

  下面将通过一个实例进行分析,服务端代码如下:

#include#include
#include
#include
#include
#include
#include
#include
#include

static bool stop = false;
/*SIGTERM 信号的处理函数,触发时结束主进程中的循环*/
static void handle_term(int sig)
{
    stop = true;
}

int main(int argc, char* argv[])
{
    signal(SIGTERM, handle_term);

    if(argc <= 3)
    {
        printf("usage: %s ip_address port_number backlog\n", basename(argv[0]));
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi(argv[2]);
    int backlog = atoi(argv[3]);

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);

    ret = listen(sock, backlog);
    assert( ret != -1);

    /*循环等待连接,直到有SIGTERM信号将它中断*/
    while(!stop)
    {
        sleep(1);
    }
    /*关闭socket*/
    close(sock);
    return 0;
}

  客户端端代码如下:

#include#include
#include
#include
#include
#include
#include
#include
#include

static bool stop = false;
/*SIGTERM 信号的处理函数,触发时结束主进程中的循环*/
static void handle_term(int sig)
{
    stop = true;
}

int main(int argc, char* argv[])
{
    signal(SIGTERM, handle_term);

    if(argc <= 3)
    {
        printf("usage: %s ip_address port_number count\n", basename(argv[0]));
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi(argv[2]);
    int count= atoi(argv[3]);
    
    struct sockaddr_in server_address;
    bzero(&server_address, sizeof(server_address));
    server_address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &server_address.sin_addr);
    server_address.sin_port = htons(port);

    int sockfd[count];
    for(int i = 0; i < count; i++)
    {
        sockfd[i] = socket(AF_INET, SOCK_STREAM,0);
        assert(sockfd[i] >= 0);
        if(connect(sockfd[i],(struct sockaddr*)&server_address,sizeof(server_address)) < 0)
        {
            printf("connection failed\n");
        }
    }

    /*循环等待连接,直到有SIGTERM信号将它中断*/
    while(!stop)
    {
        sleep(1);
    }
    for(int j = 0; j < count; j++)
    {
        close(sockfd[j]);
    }

    return 0;
}

  服务端程序(名为listen)接收3个参数:IP地址、端口号和backlog值。我们在Linux服务器上(2.6.32内核)运行:

  $./listen 127.0.0.1 6666 5

  客户端程序(名为connect)接收3个参数:IP地址、端口和count连接数。我们在Linux另外一个终端上运行(建立9个连接):

  $./connect localhost 6666 9

  通过netstat命令查看listen监听队列内容

  $netstat -nt|grep 6666

  其中处于LISTEN状态的为建立监听的服务端程序,在监听队里中,处于ESTABLISHED状态的连接有6个(backlog值加1),其他的连接都处于SYN_RCVD状态。我们改变服务端与客户端程序的第3个参数并重新运行之,能发现同样的规律,即完成连接最多有(backlog+1)个。在不同的系统上,运行结果会有些差别,不过监听队列中已完成连接的上限通常比backlog值略大。

半连接状态socket上限

  我们先来了解下syncookies功能,它控制着系统内核ipv4参数修改是否生效。如果syncookies是启动的,那么ipv4内核参数修改无效。syncookies是在内核编译的时候设置的,我们通过cat查看是否启动:

  $ cat /proc/sys/net/ipv4/tcp_syncookies

  通过echo可禁用syncookies:

  $echo 0 > /proc/sys/net/ipv4/tcp_syncookies

  接下来,我们通过cat查看内核半连接状态的socket上限是多少:

  $cat /proc/sys/net/ipv4/tcp_max_syn_backlog

  通过echo修改该内核半连接socket上限为2

  $echo 2 > /proc/sys/net/ipv4/tcp_max_syn_backlog

  此时我们运行服务端程序

  $./listen 127.0.0.1 6666 5

  打开另外一个终端运行客户端程序

  $./connect localhost 6666 10

  通过netstat命令查看listen监听队列内容

  $netstat -nt|grep 6666

  此时处于半连接(SYN_RECV)的套接字共有3个(tcp_max_syn_backlog + 1),处于已完成连接(ESTABLISHED)的套接字共6个(backlog + 1)。由于此时队列已满,TCP将忽略剩余的一个客户分片,也就是不发送RST。客户TCP将重发SYN,直到TCP返回ECONNREFUSED(尝试连接失败)错误信息。

完全连接状态socket上限

  通过cat查看内核连接状态的socket上限

  $cat /proc/sys/net/core/somaxconn

  通过echo修改该内核完全连接socket上限为2

  $echo 2 > /proc/sys/net/core/somaxconn

  运行服务器程序

  $./listen 127.0.0.1 6666 5

  打开另外一个终端运行客户端程序

  $./connect localhost 6666 10  #建立10个连接

  通过netstat命令查看listen监听队列内容

  $netstat -nt|grep 6666

  我们不难发现处于半连接状态的连接共3个(tcp_max_syn_backlog + 1),处于已完成状态的连接也共3个(tcp_max_syn_backlog + 1 ),此时大家或许会有些迷惑,backlog与系统内核参数somaxconn到底有什么关系?完全连接状态socket上限究竟由谁控制?那么让我们通过listen的Linux内核源码(linux-2.6.32)剖析来揭开这层神秘的面纱。

listen源码剖析

  listen的源码入口位于socket.c,具体代码如下:

  AF_INEF协议族(af_inet.c)的listen实现函数为inet_listen,代码如下:

  接下来进入inet_csk_listen_start,代码如下:

  通过源码剖析,我们可看出在listen第二个参数backlog不超过系统限制的最大值somaxconn时,内核直接使用其作为已完成连接队列的最大长度。如果超过了,那么系统将采用somaxconn作为已完成连接队列的最大长度。

  1)、listen建立监听后,会创建两个队列:3次握手完成连接队列(ENSTABLISHED),3次握手半连接队列(SYN_RCVD)。

  2)、当完成连接队列满时,TCP客户端连接不会丢弃,而会堆积在半连接队列中,这会加剧半连接队列的增长。如果半连接队列满了,那么TCP客户端网络分组将会被丢弃。 

  3)、半连接队列的上限由/proc/sys/net/ipv4/tcp_max_syn_backlog控制。

  4)、在listen第二个参数backlog小于/proc/sys/net/core/somaxconn时,完成连接队列的最大值由backlog控制,否则由/proc/sys/net/core/somaxconn控制。

  5)、我们可以通过修改/proc/sys/net/ipv4/tcp_syncookies参数决定对内核参数修改是否生效。

  一个看似简单的listen接口,想要真正掌握也不简单。需要我们编写代码进行试验、分析TCP连接状态图、调优内核参数、剖析Linux内核源码...

  总之,源码之前了无秘密。多读、多写、多调试。

参考资料

《UNIX环境高级编程》

《UNIX网络编程卷1》

《Linux环境编程》

《Linux高性能服务器编程》