zoukankan      html  css  js  c++  java
  • 【转载】socket 的 connect、listen、accept 和全连接队列、半连接队列的原理

    转自:http://blog.csdn.net/tennysonsky/article/details/45621341

        写在前面:

        1. accept 只是从全连接队列拿出一个已经建立好的socket,如果队列为空,则阻塞。

        2. connect 过程为三次握手过程,是由内核完成的,connect只是通知内核:我要发起连接了。所以下图中的connect指向的是服务器的listen函数和accept函数之间

        3. listen 函数不阻塞,仅仅告知内核,将socket变成被动连接的监听套接字,并在listen之前可以进行一些socket选项参数的设置,例如是否重用等。

        4. backlog参数表示连接队列的长度, 在Linux 2.2版本 以后,该参数仅表示已完成队列的大小,不包含未完成连接的大小,由系统参数 net/ipv4/tcp_max_syn_backlog限制该队列长度的最大值,因此方案是:通过系统参数设置半连接队列大小,通过应用参数设置全连接大小。 不同系统,该参数的含义不同,可参考http://veithen.github.io/2014/01/01/how-tcp-backlog-works-in-linux.html的说明

        5. 半连接队列长度是指服务器处于SYN_RCVD状态的套接字个数,半连接到全连接的通过设置重传次数来判断是否超时(tcp_synack_retries,客户端的超时对应为tcp_syn_retries ,重传时间间隔一般为3s,6s,12s,...)

        6. 全连接队列长度是指处于ESTABLISHED状态的套接字个数,即上面第4点说明的backlog的长度(特指Linux 2.2版本后), 这个设置是个参考值, 不是精确值. 如果大于/proc/sys/net/core/somaxconn, 则取somaxconn的值, 该值表示已连接队列最大值

    转自:http://www.jianshu.com/p/fe2228a77429

    已完成队列满后
      通常未完成队列的长度大于已完成队列.
      已完成队列满后, 当服务器收到来自客户端的ACK包时
      如果 /proc/sys/net/ipv4/tcp_abort_on_overflow 设为 1, 直接回RST包,结束连接.
      否则忽视ACK包.
      内核有定时器管理未完成队列,对于由于网络原因没收到ACK包或是收到ACK包后被忽视的SYN_RCVD连接重发SYN+ACK包, 最多重发次数由/proc/sys/net/ipv4/tcp_synack_retries 设定.

      backlog 即上述已完成队列的大小, 这个设置是个参考值,不是精确值. 内核会做些调整, 大于/proc/sys/net/core/somaxconn, 则取somaxconn的值

    未完成队列满后
      如果启用syncookies (net.ipv4.tcp_syncookies = 1),新的连接不进入未完成队列,不受影响.
      否则,服务器不在接受新的连接.

    SYN 洪水攻击(syn flood attack)
      通过伪造IP向服务器发送SYN包,塞满服务器的未完成队列,服务器发送SYN+ACK包 没回复,反复SYN+ACK包,使服务器不可用.

      启用syncookies 是简单有效的抵御措施.
      启用syncookies,仅未完成队列满后才生效.

    以下是转载文章内容, 下文的backlog说明不一定是错误的,因为它并没有明确系统环境

    基于 TCP 的网络编程开发分为服务器端和客户端两部分,常见的核心步骤和流程如下:

    connect()函数

    对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接(三次握手详情,请看《浅谈 TCP 三次握手》),最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)。

    通常的情况,客户端的 connect() 函数默认会一直阻塞,直到三次握手成功或超时失败才返回(正常的情况,这个过程很快完成)。

    listen()函数

    对于服务器,它是被动连接的。举一个生活中的例子,通常的情况下,移动的客服(相当于服务器)是等待着客户(相当于客户端)电话的到来。而这个过程,需要调用listen()函数。

    #include<sys/socket.h>  
    int listen(int sockfd, int backlog);  

    listen() 函数的主要作用就是将套接字( sockfd )变成被动的连接监听套接字(被动等待客户端的连接),至于参数 backlog 的作用是设置内核中连接队列的长度(这个长度有什么用,后面做详细的解释),TCP 三次握手也不是由这个函数完成,listen()的作用仅仅告诉内核一些信息。

    这里需要注意的是,listen()函数不会阻塞,它主要做的事情为,将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。

    这样的话,当有一个客户端主动连接(connect()),Linux 内核就自动完成TCP 三次握手,将建立好的链接自动存储到队列中,如此重复。

    所以,只要 TCP 服务器调用了 listen(),客户端就可以通过 connect() 和服务器建立连接,而这个连接的过程是由内核完成

    下面为测试的服务器和客户端代码,运行程序时,要先运行服务器,再运行客户端:

    服务器:

    #include <stdio.h>  
    #include <stdlib.h>  
    #include <string.h>                         
    #include <unistd.h>  
    #include <sys/socket.h>  
    #include <netinet/in.h>  
    #include <arpa/inet.h>                  
    int main(int argc, char *argv[])  
    {  
        unsigned short port = 8000;   
      
        int sockfd;  
        sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字  
        if(sockfd < 0)  
        {  
            perror("socket");  
            exit(-1);  
        }  
          
        struct sockaddr_in my_addr;  
        bzero(&my_addr, sizeof(my_addr));          
        my_addr.sin_family = AF_INET;  
        my_addr.sin_port   = htons(port);  
        my_addr.sin_addr.s_addr = htonl(INADDR_ANY);  
          
        int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));  
        if( err_log != 0)  
        {  
            perror("binding");  
            close(sockfd);        
            exit(-1);  
        }  
          
        err_log = listen(sockfd, 10);  
        if(err_log != 0)  
        {  
            perror("listen");  
            close(sockfd);        
            exit(-1);  
        }     
          
        printf("listen client @port=%d...
    ",port);  
          
        sleep(10);  // 延时10s  
      
        system("netstat -an | grep 8000");  // 查看连接状态  
          
        return 0;  
    }  

    客户端:

    #include <stdio.h>  
    #include <unistd.h>  
    #include <string.h>  
    #include <stdlib.h>  
    #include <arpa/inet.h>  
    #include <sys/socket.h>  
    #include <netinet/in.h>  
    int main(int argc, char *argv[])  
    {  
        unsigned short port = 8000;             // 服务器的端口号  
        char *server_ip = "10.221.20.12";       // 服务器ip地址  
      
        int sockfd;  
        sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字  
        if(sockfd < 0)  
        {  
            perror("socket");  
            exit(-1);  
        }  
          
        struct sockaddr_in server_addr;  
        bzero(&server_addr,sizeof(server_addr)); // 初始化服务器地址  
        server_addr.sin_family = AF_INET;  
        server_addr.sin_port = htons(port);  
        inet_pton(AF_INET, server_ip, &server_addr.sin_addr);  
          
        int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));      // 主动连接服务器  
        if(err_log != 0)  
        {  
            perror("connect");  
            close(sockfd);  
            exit(-1);  
        }  
          
        system("netstat -an | grep 8000");  // 查看连接状态  
          
        while(1);  
      
        return 0;  
    }  

    运行程序时,要先运行服务器,再运行客户端,运行结果如下:

    三次握手的连接队列

    这里详细的介绍一下 listen() 函数的第二个参数( backlog)的作用:告诉内核连接队列的长度。

    为了更好的理解 backlog 参数,我们必须认识到内核为任何一个给定的监听套接口维护两个队列:

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


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

     

    当来自客户的 SYN 到达时,TCP 在未完成连接队列中创建一个新项,然后响应以三次握手的第二个分节:服务器的 SYN 响应,其中稍带对客户 SYN 的 ACK(即SYN+ACK),这一项一直保留在未完成连接队列中,直到三次握手的第三个分节(客户对服务器 SYN 的 ACK )到达或者该项超时为止(曾经源自Berkeley的实现为这些未完成连接的项设置的超时值为75秒)。

    如果三次握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。

    backlog 参数历史上被定义为上面两个队列的大小之和,大多数实现默认值为 5,当服务器把这个完成连接队列的某个连接取走后,这个队列的位置又空出一个,这样来回实现动态平衡,但在高并发 web 服务器中此值显然不够。

    accept()函数

    accept()函数功能是,从处于 established 状态的连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。

    如果,服务器不能及时调用 accept() 取走队列中已完成的连接,队列满掉后会怎样呢?UNP(《unix网络编程》)告诉我们,服务器的连接队列满掉后,服务器不会对再对建立新连接的syn进行应答,所以客户端的 connect 就会返回 ETIMEDOUT。但实际上Linux的并不是这样的!

    下面为测试代码,服务器 listen() 函数只指定队列长度为 2,客户端有 6 个不同的套接字主动连接服务器,同时,保证客户端的 6 个 connect()函数都先调用完毕,服务器的 accpet() 才开始调用。

    服务器:

    #include <stdio.h>  
    #include <stdlib.h>  
    #include <string.h>                         
    #include <unistd.h>  
    #include <sys/socket.h>  
    #include <netinet/in.h>  
    #include <arpa/inet.h>      
                  
    int main(int argc, char *argv[])  
    {  
        unsigned short port = 8000;           
          
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);     
        if(sockfd < 0)  
        {  
            perror("socket");  
            exit(-1);  
        }  
          
        struct sockaddr_in my_addr;  
        bzero(&my_addr, sizeof(my_addr));          
        my_addr.sin_family = AF_INET;  
        my_addr.sin_port   = htons(port);  
        my_addr.sin_addr.s_addr = htonl(INADDR_ANY);  
          
        int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));  
        if( err_log != 0)  
        {  
            perror("binding");  
            close(sockfd);        
            exit(-1);  
        }  
          
        err_log = listen(sockfd, 2);    // 等待队列为2  
        if(err_log != 0)  
        {  
            perror("listen");  
            close(sockfd);        
            exit(-1);  
        }     
        printf("after listen
    ");  
          
        sleep(20);  //延时 20秒  
          
        printf("listen client @port=%d...
    ",port);  
      
        int i = 0;  
          
        while(1)  
        {     
          
            struct sockaddr_in client_addr;          
            char cli_ip[INET_ADDRSTRLEN] = "";       
            socklen_t cliaddr_len = sizeof(client_addr);      
              
            int connfd;  
            connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);         
            if(connfd < 0)  
            {  
                perror("accept");  
                continue;  
            }  
      
            inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);  
            printf("-----------%d------
    ", ++i);  
            printf("client ip=%s,port=%d
    ", cli_ip,ntohs(client_addr.sin_port));  
              
            char recv_buf[512] = {0};  
            while( recv(connfd, recv_buf, sizeof(recv_buf), 0) > 0 )  
            {  
                printf("recv data ==%s
    ",recv_buf);  
                break;  
            }  
              
            close(connfd);     //关闭已连接套接字  
            //printf("client closed!
    ");  
        }  
        close(sockfd);         //关闭监听套接字  
        return 0;  
    }  

    客户端:

     1 #include <stdio.h>  
     2 #include <unistd.h>  
     3 #include <string.h>  
     4 #include <stdlib.h>  
     5 #include <arpa/inet.h>  
     6 #include <sys/socket.h>  
     7 #include <netinet/in.h>  
     8   
     9 void test_connect()  
    10 {  
    11     unsigned short port = 8000;             // 服务器的端口号  
    12     char *server_ip = "10.221.20.12";       // 服务器ip地址  
    13       
    14     int sockfd;  
    15     sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字  
    16     if(sockfd < 0)  
    17     {  
    18         perror("socket");  
    19         exit(-1);  
    20     }  
    21       
    22     struct sockaddr_in server_addr;  
    23     bzero(&server_addr,sizeof(server_addr)); // 初始化服务器地址  
    24     server_addr.sin_family = AF_INET;  
    25     server_addr.sin_port = htons(port);  
    26     inet_pton(AF_INET, server_ip, &server_addr.sin_addr);  
    27       
    28     int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));      // 主动连接服务器  
    29     if(err_log != 0)  
    30     {  
    31         perror("connect");  
    32         close(sockfd);  
    33         exit(-1);  
    34     }  
    35       
    36     printf("err_log ========= %d
    ", err_log);  
    37       
    38     char send_buf[100]="this is for test";  
    39     send(sockfd, send_buf, strlen(send_buf), 0);   // 向服务器发送信息  
    40       
    41     system("netstat -an | grep 8000");  // 查看连接状态  
    42       
    43     //close(sockfd);  
    44 }  
    45   
    46 int main(int argc, char *argv[])  
    47 {  
    48     pid_t pid;  
    49     pid = fork();  
    50       
    51     if(0 == pid){  
    52   
    53         test_connect();     // 1  
    54           
    55         pid_t pid = fork();  
    56         if(0 == pid){  
    57             test_connect(); // 2  
    58           
    59         }else if(pid > 0){  
    60             test_connect(); // 3  
    61         }  
    62           
    63     }else if(pid > 0){  
    64           
    65         test_connect(); // 4  
    66           
    67         pid_t pid = fork();  
    68         if(0 == pid){  
    69             test_connect(); // 5  
    70           
    71         }else if(pid > 0){  
    72             test_connect(); // 6  
    73         }  
    74       
    75     }  
    76   
    77     while(1);  
    78       
    79     return 0;  
    80 }  

    同样是先运行服务器,在运行客户端,服务器 accept()函数前延时了 20 秒, 保证了客户端的 connect() 全部调用完毕后再调用 accept(),运行结果如下:

    服务器运行效果图:

    客户端运行效果图:

    按照 UNP 的说法,连接队列满后(这里设置长度为 2,发了 6 个连接),以后再调用 connect() 应该统统超时失败,但实际上测试结果是:有的 connect()立刻成功返回了,有的经过明显延迟后成功返回了。对于服务器 accpet() 函数也是这样的结果:有的立马成功返回,有的延迟后成功返回。

    对于上面服务器的代码,我们把lisen()的第二个参数改为 0 的数,重新运行程序,发现:

    客户端 connect() 全部返回连接成功(有些会延时):

    服务器 accpet() 函数却不能把连接队列的所有连接都取出来:

    对于上面服务器的代码,我们把lisen()的第二个参数改为大于 6 的数(如 10),重新运行程序,发现,客户端 connect() 立马返回连接成功, 服务器 accpet() 函数也立马返回成功。

    TCP 的连接队列满后,Linux 不会如书中所说的拒绝连接,只是有些会延时连接,而且accept()未必能把已经建立好的连接全部取出来(如:当队列的长度指定为 0 ),写程序时服务器的 listen() 的第二个参数最好还是根据需要填写,写太大不好(具体可以看cat /proc/sys/net/core/somaxconn,默认最大值限制是 128),浪费资源,写太小也不好,延时建立连接。

  • 相关阅读:
    [C#]mouse_event模拟点击时坐标参数无效?!
    体验boost::spirit
    喜讯,公司换宽屏液晶显示器了
    [疑难杂症]扩展AxWebBrowser的问题???
    VS 2005 BUG: 新增JScript文件编码问题引起乱码?
    在JavaScript中实现命名空间
    [C#]实现序列号生成器
    基于Web的仿WF工作流设计器
    分享:基于UDP协议实现可靠的数据传输
    远程控制之屏幕截取 小结
  • 原文地址:https://www.cnblogs.com/pengyusong/p/6434788.html
Copyright © 2011-2022 走看看