目錄
- 阻塞與非阻塞定義
- send與recv
- connect
- 一些問題
- 為什么要將監聽socket設置為非阻塞
阻塞與非阻塞定義
阻塞模式指的是當前某個函數執行效果未達預期,該函數會阻塞當前的執行線程,程序執行流在超時時間到達或者執行成功后恢復原有流程。非阻塞模式相反,即使某個函數執行結果未達預期,該函數也不會阻塞當前執行線程,而是立即返回。
網絡socket編程中,常見的connect、accept、send、recv函數均具有阻塞與非阻塞兩種調用方式。
阻塞與非阻塞socket具有各自適用的場景
非阻塞模式一般用于需要支持高并發QPS的場景,但是該模式會讓程序執行流和控制邏輯變復雜。
阻塞模式邏輯簡單,結構簡單。
send與recv
send函數本質不是向網絡發送數據,而是將應用層發送緩沖區的數據拷貝到內核緩沖區,至于數據什么時候從網卡緩沖區中真正的發到網絡中,要根據TCP/IP協議棧的行為來確定。
如果禁用nagel算法,存放到內核緩沖區的數據就會被立即發送出去。
否則如果一次放入緩沖區的數據包太小,系統會在多個小的數據包湊成一個足夠大的數據包后再發送。
反之,recv函數的本質則是將內核緩沖區的數據拷貝到應用緩沖區
而兩個程序進行網絡通信時,發送的一方會將內核緩沖區的數據通過網絡傳輸給接收方的內核緩沖區。這里的內核緩沖區也可以被稱為TCP窗口
如果一端一直發送數據,對端應用一直不收取數據的話,則兩端的內核緩沖區很快會被填滿,導致調用send函數被阻塞(如果是阻塞模式下的話),從而影響當前線程的流程。如果是阻塞模式下德華,對端和本端的TCP窗口已滿,數據發送不出去,send函數會立即返回-1,并且得到EWOULDBLOCK的錯誤碼。
下面是非阻塞模式下send和recv函數的返回值總結
返回值 | 返回值含義 |
---|---|
大于0 | 成功發送或者接受n個字節 |
0 | 對端關閉連接 |
小于0 | 出錯、信號被中斷、對端窗口太小導致數據發送不出去、當前網卡緩沖區無數據可接收 |
此時需要判斷返回值是否是我們期望的發送or接收的字節數。
如果對端的TCP窗口可能因為接收了部分數據就滿了,此時n的值就是(0,buf_length]了。
所以一般在循環中調用send函數,如果數據一次性發送不出去,則記錄偏移量,下一次從偏移量處接著發送,直到全部發送完為止:
bool sendData(int socketfd, const char* buf, int bufLength)
{// 已經發送的字節數int sentBytes = 0;int ret = 0;while (true) {ret = send(socketfd, buf + sentBytes, bufLength - sentBytes, 0);if (ret == -1) {if (errno == EWOULDBLOCK) {// 緩存尚未發送出去的數據,這里不具體寫// ... 緩存未發送出去的數據break;} else if (errno == EINTR) {continue;} else {return false;}} else if (ret == 0) {return false;}// 否則發送成功sentBytes += ret;if (sentBytes == bufLength)break;}return true;
}
當返回值為-1的時候我們需要根據不同的錯誤碼來進行對應處理:
錯誤碼 | send函數 | recv函數 |
---|---|---|
EWOULDBLOCK 或者 EAGAIN | TCP窗口太小,數據暫時發送不出去 | 當前內核緩沖區中無可讀數據 |
EINTR | 被信號中斷,需要重試 | 被信號中斷,需要重試 |
不是以上兩種 | 出錯 | 出錯 |
connect
使用非阻塞的connect的步驟如下:
1、創建socket,將socket設置為非阻塞模式
2、調用connect函數,無論connect函數是否連接成功都立即返回;
3、調用select函數,在指定時間內判斷該socket是否可寫,若可寫,則說明連接成功,反之認為連接失敗。不過在linux系統上有些特殊:
connect之后,不僅要調用select檢測是否可寫,還要調用getsockpt
檢測此時socket是否出錯,通過錯誤碼來檢測是否連接上,錯誤碼為0表示連接上。
在上一講中我們在服務端使用了select函數來監聽三種事件的發生,在客戶端也是可以用的。在這個問答中:select()可以用于客戶端,而不僅僅是服務器嗎?有這樣一個回答:
在客戶端套接字上使用select()
的另一個好理由是跟蹤傳出的TCP連接進度。例如,這允許設置連接超時。 將客戶端套接字設置為非阻塞。 調用connect()
。可能它會返回EINPROGRESS錯誤集(連接正在進行中,因為套接字是非阻塞的,所以不會被阻止)。 現在select()
配置FD_SET
以跟蹤客戶端套接字為’write-ready’。你也可以設置超時。 分析select()
結果。 分析上次客戶端套接字操作是否失敗或成功。 最有用的是你可以在不同狀態的幾個套接字上使用它。因此,您可以真正無阻塞地處理多個套接字(客戶端,服務器,傳出,偵聽,接受…)。所有這一切只有一個線程。
代碼如下:
#include <iostream>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 3000
#define SEND_DATA "helloworld"using namespace std;int main() {// 創建一個socketint clientfd = socket(AF_INET, SOCK_STREAM, 0);if (clientfd == -1) {cout << " create client socket error " << endl;return -1;}// 將clientfd設置為非阻塞模式int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);int newSocketFlag = oldSocketFlag | O_NONBLOCK;if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) {close(clientfd);cout << "set socket to noblock error" << endl;return -1;}// 連接服務器struct sockaddr_in serveraddr;serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);serveraddr.sin_port = htons(SERVER_PORT);// 此處與之前的阻塞式connect就不一樣了,需要用for循環,來輪詢狀態while (true) {int ret = connect(clientfd, (struct sockaddr *)& serveraddr, sizeof(serveraddr));if (ret == 0) {cout << "connect to server sucessfully" << endl;close(clientfd);return 0;} else if (ret == -1) {if (errno == EINTR) {// connect 被信號中斷了,重試connectcout << "connect interruptted by signal, try again" << endl;continue;} else if (errno == EINPROGRESS) {// 連接嘗試中break;} else {// 真的出錯了close(clientfd);return -1;}}}fd_set writeset;FD_ZERO(&writeset);FD_SET(clientfd, &writeset);struct timeval time;time.tv_sec = 3;time.tv_usec = 0;// 調用select判斷socket是否可寫if (select(clientfd + 1, NULL, &writeset, NULL, &time) != 1) {cout << "select connect to server error" << endl;close(clientfd);return -1;}int err;socklen_t len = static_cast<socklen_t>(sizeof err);// 調用getsockopt檢測此時socket是否出錯if (::getsockopt(clientfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) {close(clientfd);return -1;}if (err == 0) {cout << "connect to server successfully" <<endl;} else {cout << "connect to server error" << endl; }close(clientfd);return 0;
}
一些問題
為什么要將監聽socket設置為非阻塞
在第二講中我們談到select模型,常見的網絡通信模型都會使用IO多路復用技術如select、poll、epoll等。當有新的連接請求到來時,監聽套接字變為可讀,然后調用accept()接收新連接、返回一個連接套接字。
如果監聽套接字是阻塞的,問題可能出在什么地方?
根據TCP三次握手的示意圖:
從圖中可知,connect()會先于accep()函數返回。
當一個連接到來的時候,監聽套接字可讀,此時,我們稍微等一段時間之后再調用accept()。就在這段時間內,客戶端設置linger選項(l_onoff = 1, l_linger = 0),然后調用了close(),那么客戶端將不經過四次揮手過程,通過發送RST報文斷開連接。服務端接收到RST報文,系統會將排隊的這個未完成連接直接刪除,此時就相當于沒有任何的連接請求到來, 而接著調用的accept()將會被阻塞,直到另外的新連接到來時才會返回。這是與IO多路復用的思想相違背的(系統不阻塞在某個具體的IO操作上,而是阻塞在select、poll、epoll這些IO復用上的)。
上述這種情況下,如果監聽套接字為非阻塞的,accept()不會阻塞住,立即返回-1,同時errno = EWOULDBLOCK