1. 一道高頻騰訊面試題:tcp數(shù)據(jù)發(fā)送問題

        共 8086字,需瀏覽 17分鐘

         ·

        2022-07-17 17:20

        問題引出

        好幾個(gè)讀者私信說在騰訊面試過程中,被面試官問到了一個(gè)問題:“一個(gè)tcp服務(wù)端和一個(gè)tcp客戶端,客戶端和服務(wù)端建立連接后,服務(wù)端一直sleep,然后客戶端一直發(fā)送數(shù)據(jù)會(huì)是什么現(xiàn)象”。

        要回答這個(gè)問題,需要我們清楚tcp協(xié)議的特點(diǎn)和tcp發(fā)送數(shù)據(jù)的大體過程。

        tcp發(fā)送數(shù)據(jù)過程

        恐怕接觸過網(wǎng)絡(luò)的同學(xué)都知道tcp面向連接可靠傳輸協(xié)議,意味著客戶端發(fā)送的數(shù)據(jù)服務(wù)端是一定能夠收到的,那么對(duì)于上面的問題就不可能存在數(shù)據(jù)的丟棄。下面我們分析一下tcp的傳輸過程。

        如圖所示,tcp數(shù)據(jù)包傳輸過程主要有如下幾個(gè)步驟:

        • ? 1.應(yīng)用程序調(diào)用write系列函數(shù)發(fā)送數(shù)據(jù) ,數(shù)據(jù)首先應(yīng)用程序緩沖區(qū)復(fù)制到發(fā)送端的內(nèi)核中的 套接字發(fā)送緩沖區(qū),然后write成功返回;需要特別注意的是write成功返回只是說明數(shù)據(jù)成功的由應(yīng)用進(jìn)程緩沖區(qū)復(fù)制到了套接字發(fā)送緩沖區(qū),并不代表數(shù)據(jù)發(fā)送到了對(duì)端主機(jī)。

        • ? 2.內(nèi)核協(xié)議棧將套接字發(fā)送緩沖區(qū)中的數(shù)據(jù)發(fā)送到對(duì)端主機(jī),這個(gè)過程不受應(yīng)用程序控制,而是發(fā)送端內(nèi)核協(xié)議棧完成;

        • ? 3.數(shù)據(jù)到達(dá)接收端主機(jī)的套接字接收緩沖區(qū),注意這個(gè)接收過程也不受應(yīng)用程序控制,而是由接收端內(nèi)核協(xié)議棧完成;

        • ? 4.數(shù)據(jù)由套接字接收緩沖區(qū)復(fù)制到接收端應(yīng)用程序緩沖區(qū),注意這個(gè)過程是由類似read等函數(shù)來完成。

        清楚了tcp的傳輸過程,現(xiàn)在我們分情況來討論上面的問題。

        阻塞方式的情況

        write系列函數(shù)的工作方式默認(rèn)是阻塞方式:調(diào)用write函數(shù)時(shí),內(nèi)核從應(yīng)用進(jìn)程的緩沖區(qū)到套接字的發(fā)送緩沖區(qū)復(fù)制數(shù)據(jù)。如果其發(fā)送緩沖區(qū)中沒有空間,進(jìn)程將進(jìn)入睡眠,直到有空間為止。

        因此,阻塞方式下,如果服務(wù)端一直sleep不接收數(shù)據(jù),而客戶端一直write,也就是只能執(zhí)行上述過程中的前三步,這樣最后接收端的套接字接收緩沖區(qū)和發(fā)送端套接字發(fā)送緩沖區(qū)都被填滿,這樣write就無法繼續(xù)將數(shù)據(jù)從應(yīng)用程序復(fù)制到發(fā)送端的套接字發(fā)送緩沖區(qū)了,從而發(fā)送端進(jìn)程進(jìn)入睡眠。可以用下面的程序驗(yàn)證。

        tcpClient.c是客戶端代碼用來發(fā)送數(shù)據(jù),客戶端每次write成功一次,將計(jì)數(shù)器count加1,同時(shí)輸出本次write成功的字節(jié)數(shù)。count保存客戶端write成功的次數(shù)。

        #include <stdio.h>
        #include <string.h>
        #include <unistd.h>
        #include <sys/types.h>
        #include <sys/socket.h>
        #include <stdlib.h>
        #include <memory.h>
        #include <arpa/inet.h>
        #include <netinet/in.h>
        #define PORT 8888
        #define Buflen 1024
        int main(int argc,char *argv[])
        {
            struct sockaddr_in server_addr;
            int n,count=0;
            int sockfd;
            char sendline[Buflen];
            sockfd= socket(AF_INET,SOCK_STREAM,0);
            memset(&server_addr,0,sizeof(server_addr));
            server_addr.sin_family = AF_INET;
            server_addr.sin_port = htons(PORT);
            server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
            server_addr.sin_addr.s_addr = inet_addr(argv[1]);
            connect(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr));

            //與服務(wù)器端進(jìn)行通信
            memset(sendline,'x',sizeof(Buflen));

            while ( (n=write(sockfd,sendline,Buflen))>0 )
            {
                count++;
                printf("already write %d bytes -- %d\n",n,count);
            }

            if(n<0)
                perror("write error");
            close(sockfd);
        }

        下面的tcpServer.c是服務(wù)端程序,服務(wù)端并不接收數(shù)據(jù)。

        #include <stdio.h>
        #include <stdlib.h>
        #include <strings.h>
        #include <sys/types.h>
        #include <sys/socket.h>
        #include <memory.h>
        #include <unistd.h>
        #include <netinet/in.h>
        #include <arpa/inet.h>
        #include <string.h>
        #define PORT 8888 //定義通信端口
        #define BACKLOG 5 //定義偵聽隊(duì)列長度
        #define buflen 1024
        int listenfd,connfd;
        int main(int argc,char *argv[])
        {
            struct sockaddr_in server_addr//存儲(chǔ)服務(wù)器端socket地址結(jié)構(gòu)
            struct sockaddr_in client_addr//存儲(chǔ)客戶端 socket地址結(jié)構(gòu)
            pid_t pid;
            listenfd = socket(AF_INET,SOCK_STREAM,0);
            memset(&server_addr,0,sizeof(server_addr));
            server_addr.sin_family = AF_INET; //協(xié)議族
            server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //本地地址
            server_addr.sin_port = htons(PORT);
            bind(listenfd,(struct sockaddr *)&server_addr,sizeof(server_addr));
            listen(listenfd,BACKLOG);
            for(;;)
            {
                socklen_t addrlen = sizeof(client_addr);
                connfd = accept(listenfd,(struct sockaddr *)&client_addr,&addrlen);
                if(connfd<0)
                    perror("accept error");
                printf("receive connection\n");
                if((pid = fork()) == 0)
                {
                    close(listenfd);
                    sleep(1000);//子進(jìn)程不接收數(shù)據(jù),sleep 1000秒
                    exit(0);
                }
                else
                {
                    close(connfd);
                }
            }
        }

        首先編譯運(yùn)行服務(wù)端,然后啟動(dòng)客戶端,運(yùn)行結(jié)果如下:

        可以看到客戶端write成功377次后就陷入了阻塞,注意這個(gè)時(shí)候不能說明發(fā)送端的套接字發(fā)送緩沖區(qū)一定是滿的,只能說明套接字發(fā)送緩沖區(qū)的可用空間小于write請求寫的自己數(shù)——1024。

        非阻塞方式的情況

        下面看一下非阻塞套接字情況下,write的工作方式:對(duì)于一個(gè)非阻塞的TCP套接字,如果發(fā)送緩沖區(qū)中根本沒用空間,輸出函數(shù)將立即返回一個(gè)EWOULDBLOCK錯(cuò)誤。如果發(fā)送緩沖區(qū)中有一些空間,返回值將是內(nèi)核能夠復(fù)制到該緩沖區(qū)的字節(jié)數(shù)。這個(gè)字節(jié)數(shù)也成為“不足計(jì)數(shù)”。

        這樣就可以知道非阻塞情況下服務(wù)端一直sleep,客戶端一直write數(shù)據(jù)的效果了:開始客戶端write成功,隨著客戶端write,接收端的套接字接收緩沖區(qū)和發(fā)送端的套接字發(fā)送緩沖區(qū)會(huì)被填滿。當(dāng)發(fā)送端的套接字發(fā)送緩沖區(qū)的可用空間小于write請求寫的字節(jié)數(shù)時(shí),write立即返回-1,并將errno置為EWOULDBLOCK。

        可以用下面的程序驗(yàn)證,其中,服務(wù)端程序代碼和上面例子一樣,我們只看客戶端非阻塞模式代碼:

        #include <stdio.h>
        #include <string.h>
        #include <unistd.h>
        #include <sys/types.h>
        #include <sys/socket.h>
        #include <stdlib.h>
        #include <memory.h>
        #include <arpa/inet.h>
        #include <netinet/in.h>
        #include <fcntl.h>
        #include <errno.h>
        #define PORT 8888
        #define Buflen 1024
        int main(int argc,char *argv[])
        {
            struct sockaddr_in server_addr;
            int n,flags,count=0;
            int sockfd;
            char sendline[Buflen];
            sockfd= socket(AF_INET,SOCK_STREAM,0);
            memset(&server_addr,0,sizeof(server_addr));
            server_addr.sin_family = AF_INET;
            server_addr.sin_port = htons(PORT);
            server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
            server_addr.sin_addr.s_addr = inet_addr(argv[1]);
            connect(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr));
            flags=fcntl(sockfd,F_GETFL,0); //將已連接的套接字設(shè)置為非阻塞模式
            fcntl(sockfd,F_SETFL,flags|O_NONBLOCK);
            memset(sendline,'a',sizeof(Buflen));
            
            while ( (n=write(sockfd,sendline,Buflen))>0 )
           {
             count++;
             printf("already write %d bytes -- %d\n",n,count);
           }
            
           if(n<0)
          {
            if(errno!=EWOULDBLOCK)
               perror("write error");
            else
               printf("EWOULDBLOCK ERROR\n"); 
          }
           close(sockfd);
        }

        首先編譯運(yùn)行服務(wù)端,然后啟動(dòng)客戶端,運(yùn)行結(jié)果如下圖所示。

        可以看到客戶端成功write 185次后就發(fā)生套接字發(fā)送緩沖區(qū)空間不足,從而返回EWOULDBLOCK錯(cuò)誤。我們注意到每次write同樣的字節(jié)數(shù)(1024)阻塞模式下能write成功377次,為什么非阻塞情況下要少呢?

        這是因?yàn)樽枞J较乱恢眞rite到接收端的套接字接收緩沖區(qū)和發(fā)送端的套接字發(fā)送緩沖區(qū)都滿的情況才會(huì)阻塞。而非阻塞模式情況下有可能是發(fā)送端發(fā)送過程的第二步較慢,造成發(fā)送端的套接字發(fā)送緩沖區(qū)很快寫滿,而接收端的套接字接收緩沖區(qū)還沒有滿,這樣write就會(huì)僅僅因?yàn)榘l(fā)送端的套接字發(fā)送緩沖區(qū)滿而返回錯(cuò)誤。

        本文源碼地址:
        https://github.com/qinlizhong1/C/write

        本文示例代碼環(huán)境:
        操作系統(tǒng):macOs 12.1
        編譯器:gcc

        —  —

        歡迎關(guān)注原創(chuàng)技術(shù)號(hào)↓↓↓
        如有幫助,辛苦點(diǎn)贊和在看
        瀏覽 58
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 欧美np | 人人射人人 | 啊灬啊灬啊灬啊灬高潮在线看 | 女十八免费毛片视频 | 性猛交AAAA片免费看蜜桃视频 |