TCP网络编程中connect()、listen()和accept()三者之间的关系 ( 很是重要!!)

https://blog.csdn.net/tennysonsky/article/details/45621341web

 

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

链接详情:

 

connect()函数

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

 

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

 

listen()函数

对于服务器,它是被动链接的。举一个生活中的例子,一般的状况下,移动的客服(至关于服务器)是等待着客户(至关于客户端)电话的到来。而这个过程,须要调用listen()函数。并发

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

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

 

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

 

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

 

因此,只要 TCP 服务器调用了 listen(),客户端就能够经过 connect() 和服务器创建链接,而这个链接的过程是由内核完成测试

 

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

服务器:

 
  1.  
    #include <stdio.h>
  2.  
    #include <stdlib.h>
  3.  
    #include <string.h>
  4.  
    #include <unistd.h>
  5.  
    #include <sys/socket.h>
  6.  
    #include <netinet/in.h>
  7.  
    #include <arpa/inet.h>
  8.  
    int main(int argc, char *argv[])
  9.  
    {
  10.  
    unsigned short port = 8000;
  11.  
     
  12.  
    int sockfd;
  13.  
    sockfd = socket(AF_INET, SOCK_STREAM, 0);// 建立通讯端点:套接字
  14.  
    if(sockfd < 0)
  15.  
    {
  16.  
    perror("socket");
  17.  
    exit(-1);
  18.  
    }
  19.  
     
  20.  
    struct sockaddr_in my_addr;
  21.  
    bzero(&my_addr, sizeof(my_addr));
  22.  
    my_addr.sin_family = AF_INET;
  23.  
    my_addr.sin_port = htons(port);
  24.  
    my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  25.  
     
  26.  
    int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
  27.  
    if( err_log != 0)
  28.  
    {
  29.  
    perror("binding");
  30.  
    close(sockfd);
  31.  
    exit(-1);
  32.  
    }
  33.  
     
  34.  
    err_log = listen(sockfd, 10);
  35.  
    if(err_log != 0)
  36.  
    {
  37.  
    perror("listen");
  38.  
    close(sockfd);
  39.  
    exit(-1);
  40.  
    }
  41.  
     
  42.  
    printf("listen client @port=%d...\n",port);
  43.  
     
  44.  
    sleep(10); // 延时10s
  45.  
     
  46.  
    system("netstat -an | grep 8000"); // 查看链接状态
  47.  
     
  48.  
    return 0;
  49.  
    }

 

客户端:

 
  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.  
    int main(int argc, char *argv[])
  9.  
    {
  10.  
    unsigned short port = 8000; // 服务器的端口号
  11.  
    char *server_ip = "10.221.20.12"; // 服务器ip地址
  12.  
     
  13.  
    int sockfd;
  14.  
    sockfd = socket(AF_INET, SOCK_STREAM, 0);// 建立通讯端点:套接字
  15.  
    if(sockfd < 0)
  16.  
    {
  17.  
    perror("socket");
  18.  
    exit(-1);
  19.  
    }
  20.  
     
  21.  
    struct sockaddr_in server_addr;
  22.  
    bzero(&server_addr,sizeof(server_addr)); // 初始化服务器地址
  23.  
    server_addr.sin_family = AF_INET;
  24.  
    server_addr.sin_port = htons(port);
  25.  
    inet_pton(AF_INET, server_ip, &server_addr.sin_addr);
  26.  
    //server_addr.sin_addr.s_addr=inet_addr(server_ip);
  27.  
    int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
  28.  
    // 主动链接服务器
  29.  
    if(err_log != 0)
  30.  
    {
  31.  
    perror("connect");
  32.  
    close(sockfd);
  33.  
    exit(-1);
  34.  
    }
  35.  
     
  36.  
    system("netstat -an | grep 8000"); // 查看链接状态
  37.  
     
  38.  
    while(1);
  39.  
     
  40.  
    return 0;
  41.  
    }

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

 

 

三次握手的链接队列

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

 

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

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


二、已完成链接队列(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() 才开始调用。

 

服务器:

 
  1.  
    #include <stdio.h>
  2.  
    #include <stdlib.h>
  3.  
    #include <string.h>
  4.  
    #include <unistd.h>
  5.  
    #include <sys/socket.h>
  6.  
    #include <netinet/in.h>
  7.  
    #include <arpa/inet.h>
  8.  
     
  9.  
    int main(int argc, char *argv[])
  10.  
    {
  11.  
    unsigned short port = 8000;
  12.  
     
  13.  
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  14.  
    if(sockfd < 0)
  15.  
    {
  16.  
    perror("socket");
  17.  
    exit(-1);
  18.  
    }
  19.  
     
  20.  
    struct sockaddr_in my_addr;
  21.  
    bzero(&my_addr, sizeof(my_addr));
  22.  
    my_addr.sin_family = AF_INET;
  23.  
    my_addr.sin_port = htons(port);
  24.  
    my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  25.  
     
  26.  
    int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
  27.  
    if( err_log != 0)
  28.  
    {
  29.  
    perror("binding");
  30.  
    close(sockfd);
  31.  
    exit(-1);
  32.  
    }
  33.  
     
  34.  
    err_log = listen(sockfd, 2); // 等待队列为2
  35.  
    if(err_log != 0)
  36.  
    {
  37.  
    perror("listen");
  38.  
    close(sockfd);
  39.  
    exit(-1);
  40.  
    }
  41.  
    printf("after listen\n");
  42.  
     
  43.  
    sleep(20); //延时 20秒
  44.  
     
  45.  
    printf("listen client @port=%d...\n",port);
  46.  
     
  47.  
    int i = 0;
  48.  
     
  49.  
    while(1)
  50.  
    {
  51.  
     
  52.  
    struct sockaddr_in client_addr;
  53.  
    char cli_ip[INET_ADDRSTRLEN] = "";
  54.  
    socklen_t cliaddr_len = sizeof(client_addr);
  55.  
     
  56.  
    int connfd;
  57.  
    connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);
  58.  
    if(connfd < 0)
  59.  
    {
  60.  
    perror("accept");
  61.  
    continue;
  62.  
    }
  63.  
     
  64.  
    inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
  65.  
    printf("-----------%d------\n", ++i);
  66.  
    printf("client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port));
  67.  
     
  68.  
    char recv_buf[512] = {0};
  69.  
    while( recv(connfd, recv_buf, sizeof(recv_buf), 0) > 0 )
  70.  
    {
  71.  
    printf("recv data ==%s\n",recv_buf);
  72.  
    break;
  73.  
    }
  74.  
     
  75.  
    close(connfd); //关闭已链接套接字
  76.  
    //printf("client closed!\n");
  77.  
    }
  78.  
    close(sockfd); //关闭监听套接字
  79.  
    return 0;
  80.  
    }

 

客户端:

 
  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\n", 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),浪费资源,写过小也很差,延时创建链接。

 

测试代码下载请点此处。

相关文章
相关标签/搜索