socket的阻塞模式和非阻塞模式
socket
的阻塞模式和非阻塞模式
無論是Windows還是Linux,預設建立socket
都是阻塞模式的
在Linux中,可以再建立socket
是直接將它設定為非阻塞模式
int socket (int __domain, int __type, int __protocol)
將__type
增加SOCK_NOBLOCK
不僅如此,在Linux上直接利用accept
函式返回的代表與客戶端通訊的socket
也提供了一個拓展函式accept4
,直接將accept4
返回的socket
設定為非阻塞的
send
和recv
函式在阻塞和非阻塞模式下的表現
send
和recv
函式並不是直接向網路上傳送資料和接收資料
send
函式是將應用層傳送緩衝區的資料拷貝到核心緩衝區中
recv
函式是將核心緩衝區的資料拷貝到應用緩衝區
可以用下面這張圖來描述:
通過上圖我們可以知道,不同的程式進行網路通訊時,傳送的一方會將核心緩衝區的資料通過網路傳輸給接收方的核心緩衝區。
在應用程式A與應用程式B建立TCP連線後,假設A不斷呼叫send
函式,會將資料不斷拷貝到對應的核心緩衝區,如果應用程式不呼叫recv
函式,那麼在應用程式B的核心緩衝區被填滿後,A的緩衝區也隨後被填滿,此時如果A繼續呼叫send
函式會有什麼後果呢?
- 當
socket
處於阻塞模式時,繼續呼叫send/recv
函式,程式會阻塞在send/recv
呼叫處 - 當
socket
send/recv
函式,會返回錯誤碼
-
socket
阻塞模式下send
函式的表現程式碼來自《C++伺服器開發精髓》
服務端程式碼:
#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <iostream> #include <string.h> int main(int argc, char* argv[]) { //1.建立一個偵聽socket int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd == -1) { std::cout << "create listen socket error." << std::endl; return -1; } //2.初始化伺服器地址 struct sockaddr_in bindaddr; bindaddr.sin_family = AF_INET; bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); bindaddr.sin_port = htons(3000); if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) { std::cout << "bind listen socket error." << std::endl; close(listenfd); return -1; } //3.啟動偵聽 if (listen(listenfd, SOMAXCONN) == -1) { std::cout << "listen error." << std::endl; close(listenfd); return -1; } while (true) { struct sockaddr_in clientaddr; socklen_t clientaddrlen = sizeof(clientaddr); //4. 接受客戶端連線 int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen); if (clientfd != -1) { //只接受連線,不呼叫recv收取任何資料 std:: cout << "accept a client connection." << std::endl; } } //7.關閉偵聽socket close(listenfd); return 0; }
客戶端程式碼:
/** * 驗證阻塞模式下send函式的行為,client端 * zhangyl 2018.12.17 */ #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <iostream> #include <string.h> #define SERVER_ADDRESS "127.0.0.1" #define SERVER_PORT 3000 #define SEND_DATA "helloworld" int main(int argc, char* argv[]) { //1.建立一個socket int clientfd = socket(AF_INET, SOCK_STREAM, 0); if (clientfd == -1) { std::cout << "create client socket error." << std::endl; return -1; } //2.連線伺服器 struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); serveraddr.sin_port = htons(SERVER_PORT); if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) { std::cout << "connect socket error." << std::endl; close(clientfd); return -1; } //3. 不斷向伺服器傳送資料,或者出錯退出 int count = 0; while (true) { int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0); if (ret != strlen(SEND_DATA)) { std::cout << "send data error." << std::endl; break; } else { count ++; std::cout << "send data successfully, count = " << count << std::endl; } } //5. 關閉socket close(clientfd); return 0; }
先啟動server在啟動client,客戶端會不斷向服務端傳送helloworld,每次傳送成功後會列印計數器,執行一頓時間後,停止列印,計數器不再增加
當程式不再有輸出,說明阻塞在某個函式gdb看一看
(gdb) bt
#0 0x00007ffff7d03690 in __libc_send (fd=3, buf=0x555555556045, len=10,
flags=0) at ../sysdeps/unix/sysv/linux/send.c:28
#1 0x00005555555553bb in main (argc=1, argv=0x7fffffffdf28) at client.cpp:42
(gdb)
果然是send
函式
上面這個例子證明了如果一端一直髮送資料,另一端不接收資料,核心緩衝區很快就會被填滿,發生阻塞. 其實這裡所說的核心緩衝區就是TCP視窗
我們現在利用tcpdump
工具檢視一下這種情況下TCP視窗的大小
22:01:57.543364 IP 127.0.0.1.53382 > 127.0.0.1.3000: Flags [S], seq 1832090129, win 65495, options [mss 65495,sackOK,TS val 451488646 ecr 0,nop,wscale 7], length 0
22:01:57.543379 IP 127.0.0.1.3000 > 127.0.0.1.53382: Flags [S.], seq 1797517498, ack 1832090130, win 65483, options [mss 65495,sackOK,TS val 451488646 ecr 451488646,nop,wscale 7], length 0
22:01:57.543386 IP 127.0.0.1.53382 > 127.0.0.1.3000: Flags [.], ack 1797517499, win 512, options [nop,nop,TS val 451488646 ecr 451488646], length 0
...
22:02:11.342670 IP 127.0.0.1.3000 > 127.0.0.1.53382: Flags [.], ack 1832177322, win 0, options [nop,nop,TS val 451502445 ecr 451488936], length 0
win就是TCP視窗的大小可以看出,逐漸減小最後變為零
-
socket
非阻塞模式下send
函式的表現就是返回一個錯誤碼,不阻塞了,略...
-
socket
阻塞模式下recv
函式的表現阻塞了就...
-
socket
非阻塞模式下recv
函式的表現recv
在沒有資料可讀的情況下,會立即返回,返回值為-1
非阻塞模式下send
和recv
函式返回值總結
返回值n | 含義 |
---|---|
大於0 | 成功傳送或接受n位元組 |
等於零 | 對方關閉連線 |
小於零 | 出錯,被訊號中斷,TCP視窗太小導致資料傳送不出去,或者當前網絡卡緩衝區已經無資料可以接受 |
詳細介紹:
-
返回值大於0
當
send
和recv
函式返回值大於0時,表示傳送或者接收多少位元組.需要注意的是,在這種情況下,**判斷send
返回值是否等於要傳送的位元組數,而不是簡單地判斷返回值是否大於零int n = send(socketfd,buf,buf_length,0); if (n>0) { printf("send successfully"); }
很多新手就會寫出以上的程式碼(比如我...)雖然返回值大於零,但由於對端TCP視窗已滿,搜易我們所期望傳送的位元組,並沒有全部被對方接收,所以
n
的大小在區間(0,buf_length)內解決辦法:
- 在返回值等於
buf_length
時才認為正確 - 在一個迴圈中呼叫
send
函式,如果一次性發送不完,記錄偏移量,接著從偏移量處傳送
- 在返回值等於
-
返回值等於0
- 對端關閉了連線
- 特殊情況:
send
函式主動傳送了0位元組
-
小於零
出錯啦唄