問題引出
好幾個讀者私信說在騰訊面試過程中,被面試官問到了一個問題:“一個tcp服務端和一個tcp客戶端,客戶端和服務端建立連接後,服務端一直sleep,然後客戶端一直發送數據會是什麼現象”。
要回答這個問題,需要我們清楚tcp協議的特點和tcp發送數據的大體過程。
tcp發送數據過程
恐怕接觸過網絡的同學都知道tcp是面向連接的可靠傳輸協議,意味着客戶端發送的數據服務端是一定能夠收到的,那麼對於上面的問題就不可能存在數據的丟棄。下面我們分析一下tcp的傳輸過程。
如圖所示,tcp數據包的傳輸過程主要有如下幾個步驟:
- • 1.應用程序調用write系列函數發送數據 ,數據首先由應用程序緩衝區複製到發送端的內核中的 套接字發送緩衝區,然後write成功返回;需要特別注意的是write成功返回只是說明數據成功的由應用進程緩衝區複製到了套接字發送緩衝區,並不代表數據發送到了對端主機。
- • 2.內核協議棧將套接字發送緩衝區中的數據發送到對端主機,這個過程不受應用程序控制,而是發送端內核協議棧完成;
- • 3.數據到達接收端主機的套接字接收緩衝區,注意這個接收過程也不受應用程序控制,而是由接收端內核協議棧完成;
- • 4.數據由套接字接收緩衝區複製到接收端應用程序緩衝區,注意這個過程是由類似read等函數來完成。
清楚了tcp的傳輸過程,現在我們分情況來討論上面的問題。
相關視頻推薦
阻塞方式的情況
write系列函數的工作方式默認是阻塞方式:調用write函數時,內核從應用進程的緩衝區到套接字的發送緩衝區複製數據。如果其發送緩衝區中沒有空間,進程將進入睡眠,直到有空間為止。
因此,阻塞方式下,如果服務端一直sleep不接收數據,而客戶端一直write,也就是只能執行上述過程中的前三步,這樣最後接收端的套接字接收緩衝區和發送端套接字發送緩衝區都被填滿,這樣write就無法繼續將數據從應用程序複製到發送端的套接字發送緩衝區了,從而發送端進程進入睡眠。可以用下面的程序驗證。
tcpClient.c是客戶端代碼用來發送數據,客戶端每次write成功一次,將計數器count加1,同時輸出本次write成功的字節數。count保存客戶端write成功的次數。
#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));
//與服務器端進行通信
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是服務端程序,服務端並不接收數據。
#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 //定義偵聽隊列長度
#define buflen 1024
int listenfd,connfd;
int main(int argc,char *argv[])
{
struct sockaddr_in server_addr; //存儲服務器端socket地址結構
struct sockaddr_in client_addr; //存儲客戶端 socket地址結構
pid_t pid;
listenfd = socket(AF_INET,SOCK_STREAM,0);
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET; //協議族
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);//子進程不接收數據,sleep 1000秒
exit(0);
}
else
{
close(connfd);
}
}
}
首先編譯運行服務端,然後啟動客戶端,運行結果如下:
可以看到客戶端write成功377次後就陷入了阻塞,注意這個時候不能說明發送端的套接字發送緩衝區一定是滿的,只能說明套接字發送緩衝區的可用空間小於write請求寫的自己數——1024。
非阻塞方式的情況
下面看一下非阻塞套接字情況下,write的工作方式:對於一個非阻塞的TCP套接字,如果發送緩衝區中根本沒用空間,輸出函數將立即返回一個EWOULDBLOCK錯誤。如果發送緩衝區中有一些空間,返回值將是內核能夠複製到該緩衝區的字節數。這個字節數也成為“不足計數”。
這樣就可以知道非阻塞情況下服務端一直sleep,客戶端一直write數據的效果了:開始客戶端write成功,隨着客戶端write,接收端的套接字接收緩衝區和發送端的套接字發送緩衝區會被填滿。當發送端的套接字發送緩衝區的可用空間小於write請求寫的字節數時,write立即返回-1,並將errno置為EWOULDBLOCK。
可以用下面的程序驗證,其中,服務端程序代碼和上面例子一樣,我們只看客戶端非阻塞模式代碼:
#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); //將已連接的套接字設置為非阻塞模式
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);
}
首先編譯運行服務端,然後啟動客戶端,運行結果如下圖所示。
編輯
可以看到客戶端成功write 185次後就發生套接字發送緩衝區空間不足,從而返回EWOULDBLOCK錯誤。我們注意到每次write同樣的字節數(1024)阻塞模式下能write成功377次,為什麼非阻塞情況下要少呢?
這是因為阻塞模式下一直write到接收端的套接字接收緩衝區和發送端的套接字發送緩衝區都滿的情況才會阻塞。而非阻塞模式情況下有可能是發送端發送過程的第二步較慢,造成發送端的套接字發送緩衝區很快寫滿,而接收端的套接字接收緩衝區還沒有滿,這樣write就會僅僅因為發送端的套接字發送緩衝區滿而返回錯誤。
原文地址:https://mp.weixin.qq.com/s/rpNTjTUt19Bbyx6IWm2-ig