如何將socket設置爲非阻塞模式
無論是Windows還是Linux,默認創建的socket
都是阻塞模式的。
在linux上,我們可以使用fcntl
函數或者ioctl
函數給創建的socket
增加O_NONBLOCK
標誌來將socket
設置爲非阻塞模式。
int oldSocketFlag = fcntl(sockfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
fcntl(sockfd, F_SETFL, newSocketFlag);
Linux上的socket
函數也可以直接在創建時將socket
設置爲非阻塞式,socket
函數簽名如下:
int socket(int domain, int type, int protocol);
給type
參數增加一個SOCK_NONBLOCK
標誌即可,例如:
int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
不僅如此,在Linux上利用accept
函數返回的代表與客戶端通信的socket
也提供了一個擴展函數accept4
,直接將accept
函數返回的socket
設置爲非阻塞的;
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen, int flags);
只要將accept4
函數的最後一個參數flags
設置爲SOCK_NONBLOCK
即可。
以下代碼等價。
socklen_t addrlen = sizeof(clientaddr); int clientfd = accept4(listenfd, &clientaddr, &addrlen, SOCK_NONBLOCK); socklen_t addrlen = sizeof(clientaddr); int clientfd = accept(listenfd,&clientaddr,&addrlen); if(clientfd != -1) { int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); int newSocketFlag = oldSocketFlag | O_NONBLOCK; fcntl(clientfd, F_SETFL, newSocketFlag); }
send和recv函數在阻塞和非阻塞模式下的表現
send
函數在本質上並不是向網絡上發送數據,而是將應用層發送緩衝區的數據拷貝到內核緩衝區中,至於數據什麼時候會從網卡緩衝區中真正地發到網絡中,要根據TCP/IP協議棧的行爲來確定。如果socket
設置了TCP_NODELAY
選項(即禁用nagel算法)。存放到內核緩衝區的數據就會被立即發送出去,反之,一次放入內核緩衝區的數據包如果太小,則系統會在多個小的數據包湊成一個足夠大的數據包之後纔會將數據發送出去。
recv
函數在本質上並不是從網絡上收取數據,而是將內核緩衝區中的數據拷貝到應用程序的緩衝區中。在拷貝完成後會將內核緩衝區中的該部分數據移除。
通過上圖可以知道,不同的程序在進行網絡通信時,發送的一方會將內核緩衝區的數據通過網絡傳輸給接收方的內核緩衝區。在應用程序A與應用程序B建立連接之後,假設應用程序A不斷調用send
函數,則數據會不斷拷貝至對應的內核緩衝區中,如果應用程序B一直不調用recv
函數,那麼在應用程序B的內核緩衝區被填滿之後,應用程序A的內核緩衝區也會被填滿,此時應用程序A繼續調用send
函數會發生什麼結果?具體的結果取決於socket
是否是阻塞模式,這裏先給出結論。
- 當
socket
是阻塞模式時,繼續調用send/recv
函數,程序會阻塞在send/recv
調用處; - 當
socket
是非阻塞模式時,繼續調用send/recv
函數,send/recv
不會阻塞程序執行流,而是立即出錯返回,我們會得到一個相關的錯誤碼,在Linux上該錯誤碼爲EWOULDBLOCK
或EAGAIN
。
非阻塞模式下send和recv函數的返回值總結
返回值n | 返回值的含義 |
---|---|
大於0 | 成功發送send 或接收recv n字節 |
0 | 對端關閉連接 |
小於0 | 出錯,被信號中斷,對端TCP窗口太小導致數據發送不出去或者當前網卡緩衝區已無數據可接收 |
這三種情況:
- 返回值大於0。當
send/recv
函數的返回值大於0時,表示發送或接收多少字節。需要注意的是,在這種情況下,我們一定要判斷send
函數的返回值是不是我們期望發送的字節數,而不是簡單判斷其返回值大於0。舉個例子int n = send(socket, buf, buf_length, 0); if(n > 0) { printf("send data successful"); }
雖然返回值
n
大於0,但在實際情況中,由於對端的TCP窗口可能因爲缺少一部分字節就滿了,所以n
的值可能爲 ( 0 , b u f _ l e n g t h ] (0, buf\_length] (0,buf_length]。當 0 < n < b u f _ l e n g t h 0 < n < buf\_length 0<n<buf_length時,雖然此時send
函數調用成功,但在業務上並不算正確,因爲有部分數據並沒有被髮送出去。我們可能在一次測試中測不出n
小於buf_length
的情況,但不代表實際上不存在。所以,建議要麼在返回值n
等於buf_length
時才認爲正確,要麼在一個循環中調用send
函數,如果數據一次性發送不完,則記錄偏移量,下一次從偏移量處接着發送,直到全部發送完爲止。//不推薦的方式 int n = send(socket, buf, buf_length, 0); if(n == buf_length) { printf("send data successfully\n"); } //推薦的方式 bool SendData(const char* buf, int buf_length) { //已經發送的字節數 int sent_bytes = 0; int ret = 0; while(true) { ret = send(m_hSocket, buf + sent_bytes, buf_length - sent_bytes, 0); if(ret == -1) { if(errno == EWOULDBLOCK) { //嚴謹的做法:如果發送不出去,則應該緩存尚未發送出去的數據 break; } else if(errno == EINTR) continue; else return false; } else if(ret == 0) return false; sent_bytes += ret; if(sent_bytes == buf_length) break; } return true; }
- 返回值等於0。在通常情況下,如果
send
或recv
函數返回0,我們就認爲對端關閉了連接,我們這端也關閉連接即可。send
函數主動發送0字節時也會返回0,這是一種特例。 - 返回值小於0。對於
send
或recv
函數返回值小於0的情況(即返回-1),此時並不表示send
或者recv
函數一定調用出錯。
下表表示的是非阻塞模式下,
socket
的send
和recv
返回值,對於阻塞模式下的socket
,如果返回值爲-1,則一定表示出錯。返回值和錯誤碼 send函數 recv函數 返回-1,錯誤碼是EWOULDBLOCK或EAGAIN TCP窗口太小,數據暫時發送不出去 在當前緩衝區中無可讀數據 返回-1,錯誤碼是EINTR 被信號中斷,需要重試 被信號中斷,需要重試 返回-1,錯誤碼不是以上3種 出錯 出錯 阻塞與非阻塞socket的各自使用場景
阻塞的
socket
函數在調用send
,recv
,connect
,accept
等函數時,如果特定的條件不滿足,就會阻塞其調用線程直至超時,非阻塞的socket
恰恰相反。非阻塞模式一般用於需要支持高併發多QPS的場景(如服務器程序),但是正如前文所述,這種模式讓程序的執行流和控制邏輯變得複雜;相反,阻塞模式邏輯簡單,程序結構簡單明瞭,常用於一些特殊場景中。
應用場景一:某程序需要臨時發送一個文件,文件分段發送,每發送一段,對端都會給予一個響應,該程序可以單獨開一個任務線程,在這個任務線程函數裏面,使用先
send
後recv
再send
再recv
的模式,每次send
和recv
都是阻塞模式的。應用場景二:A端與B端之間的通信只有問答模式,即A端每發送給B端一個請求,B端比定會給A端一個響應,除此之外,B端不會向A端推送任何數據,此時A端就可以採用阻塞模式,在每次
send
完請求後,都可以直接使用阻塞式的recv
函數接收應答包。發送給B端一個請求,B端比定會給A端一個響應,除此之外,B端不會向A端推送任何數據,此時A端就可以採用阻塞模式,在每次
send
完請求後,都可以直接使用阻塞式的recv
函數接收應答包。 - 返回值等於0。在通常情況下,如果