問題:「一個tcp服務端和一個tcp客戶端,客戶端和服務端創建鏈接後,服務端一直sleep,而後客戶端一直髮送數據會是什麼現象」。php
回答這個問題前咱們先想想tcp的特徵和tcp發送數據的大致過程:服務器
首先,tcp是有連接的可靠傳輸協議,所謂可靠也就是說保證客戶端發送的數據服務端都可以收到,而且是按序收到。那麼對於上面的問題就不可能存在數據的丟棄。那麼客戶端一直髮送數據愈來愈多怎麼辦?下面咱們分析一下tcp的傳輸過程。socket
圖1tcp
如圖1所示當發送數據時:函數
(1) 數據首先由應用程序緩衝區複製到發送端的套接字發送緩衝區(位於內核),注意這個過程是用相似write功能的函數完成的。有的人一般看到write成功就覺得數據發送到了對端主機,其實這是錯誤的,write成功僅僅表示數據成功的由應用進程緩衝區複製到了套接字發送緩衝區。ui
(2) 而後內核協議棧將套接字發送緩衝區中的數據發送到對端主機,注意這個過程不受應用程序控制,而是發送端內核協議棧完成,其中包括使用滑動窗口、用賽控制等功能。spa
(3) 數據到達接收端主機的套接字接收緩衝區,注意這個接收過程也不受應用程序控制,而是由接收端內核協議棧完成,其中包括髮送ack確認等。.net
(4) 數據由套接字接收緩衝區複製到接收端應用程序緩衝區,注意這個過程是由相似read等函數來完成。unix
1.1 阻塞方式的狀況
知道了這個過程,咱們在看一下在默認狀況下(套接字爲阻塞方式)write等函數的工做方式:輸出操做,包括write、writev、send、sendto和sendmsg共5個函數。對於一個TCP套接字,內核從應用進程的緩衝區到套接字的發送緩衝區複製數據。對於阻塞的套接字,若是其發送緩衝區中沒有空間,進程將被投入睡眠,直到有空間爲止。——UNPv1server
這樣咱們就能夠推測出告終果:阻塞方式下,若是服務端一直sleep不接收數據,而客戶端一直write,也就是隻能執行上述過程當中的前三步,這樣最終結果確定是接收端的套接字接收緩衝區和發送端套接字發送緩衝區都被填滿,這樣write就沒法繼續將數據從應用程序複製到發送端的套接字發送緩衝區了,從而使進程進入睡眠。
驗證例子以下。
客戶端代碼:
l tcpClient.c
點擊(此處)摺疊或打開
- #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 9999
- #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,'a',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);
- }
客戶端每次write成功一次,將計數器count加1,同時輸出本次write成功的字節數。count保存客戶端write成功的次數。
服務端代碼:
l 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 9999 //定義通訊端口
- #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);
- }
- }
- }
首先編譯運行服務端,而後啓動客戶端,運行結果如圖2所示。
圖2
能夠看到客戶端write成功377次後就陷入了阻塞,注意這個時候不能說明發送端的套接字發送緩衝區一點是滿的,只能說明套接字發送緩衝區的可用空間小於write請求寫的本身數——1024。
補充:當服務端sleep到1000後,會關閉當前鏈接,此時客戶端處於阻塞中的write會返回錯誤,效果如圖3。
圖3
1.2 非阻塞方式的狀況
下面看一下非阻塞套接字狀況下,write的工做方式:對於一個非阻塞的TCP套接字,若是發送緩衝區中根本沒用空間,輸出函數將當即返回一個EWOULDBLOCK錯誤。若是發送緩衝區中有一些空間,返回值將是內核可以複製到該緩衝區的字節數。這個字節數也成爲「不足計數」。——UNPv1
這樣就能夠知道非阻塞狀況下服務端一直sleep,客戶端一直write數據的效果了:開始客戶端write成功,隨着客戶端write,接收端的套接字接收緩衝區和發送端的套接字發送緩衝區會被填滿。當發送端的套接字發送緩衝區的可用空間小於write請求寫的字節數時,write當即返回-1,並將errno置爲EWOULDBLOCK。
驗證例子代碼以下。
l 服務端同阻塞狀況(略)。
客戶端(非阻塞模式):
l tcpClientNonBlock.c
點擊(此處)摺疊或打開
- #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 9999
- #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);
- }
首先編譯運行服務端,而後啓動客戶端,運行結果以下圖4所示。
圖4
能夠看到客戶端成功write 185次後就發生套接字發送緩衝區空間不足,從而返回EWOULDBLOCK錯誤。咱們注意到每次write一樣的字節數(1024)阻塞模式下能write成功377次,爲何非阻塞狀況下要少呢?這是由於阻塞模式下一直write到接收端的套接字接收緩衝區和發送端的套接字發送緩衝區都滿的狀況纔會阻塞。而非阻塞模式狀況下有多是發送端發送過程的第二步較慢,形成發送端的套接字發送緩衝區很快寫滿,而接收端的套接字接收緩衝區尚未滿,這樣write就會僅僅由於發送端的套接字發送緩衝區滿而返回錯誤(準確的說的套接字發送緩衝區的可用空間小於write請求寫的字節數)。對比一下377正好是185的二倍左右,因此能夠推測因爲發送過程第二步的延遲,極可能發送端的套接字發送緩衝區已經滿了,而接收端的套接字接收緩衝區仍是空的。
1.3 UDP狀況補充
對於UDP套接字不存在真正的發送緩衝區。內核只是複製應用進程數據並把它沿協議棧向下傳送,漸次冠以UDP首部和IP首部。所以對於一個阻塞的UDP套接字(默認設置),輸出函數調用將不會由於與TCP套接字同樣的緣由而阻塞,不過有可能會由於其餘緣由而阻塞。——UNPv1
轉載地址:
http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=28541347&id=4730278
http://ticktick.blog.51cto.com/823160/779866
感謝博主的分享!