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),浪費資源,寫過小也很差,延時創建鏈接。

 

測試代碼下載請點此處。

相關文章
相關標籤/搜索