1. 程式人生 > >【網路程式設計】TCP網路程式設計中connect()、listen()和accept()三者之間的關係

【網路程式設計】TCP網路程式設計中connect()、listen()和accept()三者之間的關係

舉個簡單的例子(以下程式碼只是示範性的,用於說明不同套接字的作用,實際的函式會需要更多的引數):
/* 建立用於監聽和接受客戶端連線請求的套接字 */
server_sock = socket();
/* 繫結監聽的IP地址和埠 */
bind(server_sock);
/* 開始監聽 */
listen(server_sock);
/**
* 等待客戶端連線請求,在沒有客戶端連線請求到來之前,
* 程式會一直阻塞在這個函式裡。
*/
client_sock = accept(server_sock);
/**
* 已經接受客戶端連線請求,accept()函式建立並返回了一個
* 新的套接字client_sock,用於與客戶端通訊。
* 如果不再需要接受其他客戶端的連線請求,可以關閉監聽
* 套接字了。
*/
close(server_sock);
/* 傳送資料到客戶端 */
send(client_sock, data);
/* 從客戶端接收資料 */
recv(client_sock, data);
/* 通訊結束,關閉與客戶端通訊的套接字 */
close(client_sock); 

 

版權宣告:本部落格文章,大多是本人整理編寫,或在網路中收集,轉載請註明出處! https://blog.csdn.net/tennysonsky/article/details/45621341

基於 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...\n",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\n");
        
        sleep(20);    //延時 20秒
        
        printf("listen client @port=%d...\n",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------\n", ++i);
            printf("client ip=%s,port=%d\n", 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\n",recv_buf);
                break;
            }
            
            close(connfd);     //關閉已連線套接字
            //printf("client closed!\n");
        }
        close(sockfd);         //關閉監聽套接字
        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>
     
    void test_connect()
    {
        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);
        }
        
        printf("err_log ========= %d\n", err_log);
        
        char send_buf[100]="this is for test";
        send(sockfd, send_buf, strlen(send_buf), 0);   // 向伺服器傳送資訊
        
        system("netstat -an | grep 8000");  // 檢視連線狀態
        
        //close(sockfd);
    }
     
    int main(int argc, char *argv[])
    {
        pid_t pid;
        pid = fork();
        
        if(0 == pid){
     
            test_connect();        // 1
            
            pid_t pid = fork();
            if(0 == pid){
                test_connect();    // 2
            
            }else if(pid > 0){
                test_connect();    // 3
            }
            
        }else if(pid > 0){
            
            test_connect();    // 4
            
            pid_t pid = fork();
            if(0 == pid){
                test_connect();    // 5
            
            }else if(pid > 0){
                test_connect();    // 6
            }
        
        }
     
        while(1);
        
        return 0;
    }


同樣是先執行伺服器,在執行客戶端,伺服器 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),浪費資源,寫太小也不好,延時建立連線。


測試程式碼下載請點此處。
---------------------
作者:Mike__Jiang
來源:CSDN
原文:https://blog.csdn.net/tennysonsky/article/details/45621341
版權宣告:本文為博主原創文章,轉載請附上博文連結!