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字節
-
小於零
出錯啦唄