socket 的阻塞模式和非阻塞模式

對 socket 在阻塞和非阻塞模式下的各個函數的行為差別深入的理解是掌握網路編程的基本要求之一,是重點也是難點。

阻塞和非阻塞模式下,我們常討論的具有不同行為表現的 socket 函數一般有如下幾個,見下表:

  • connect
  • accept
  • send (Linux 平台上對 socket 進行操作時也包括 write 函數,下文中對 send 函數的討論也適用於 write 函數)
  • recv (Linux 平台上對 socket 進行操作時也包括 read 函數,下文中對 recv 函數的討論也適用於 read 函數)

在正式討論以上四個函數之前,我們先解釋一下阻塞模式和非阻塞模式的概念。所謂阻塞模式就當某個函數「執行成功的條件」當前不能滿足時,該函數會阻塞當前執行線程,程序執行流在超時時間到達或「執行成功的條件」滿足後恢復繼續執行。而非阻塞模式恰恰相反,即使某個函數的「執行成功的條件」不當前不能滿足,該函數也不會阻塞當前執行線程,而是立即返回,繼續運行執行程序流。如果讀者不太明白這兩個定義也沒關係,後面我們會以具體的示例來講解這兩種模式的區別。

如何將 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);

ioctl() 函數fcntl() 函數使用方式基本一致,這裡就不再給出示例代碼了。

當然,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 accept4(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);
}

在 Windows 平台上,可以調用 ioctlsocket() 函數 將 socket 設置成非阻塞模式,ioctlsocket() 簽名如下:

int ioctlsocket(SOCKET s, long cmd, u_long *argp);

cmd 參數設置為 FIONBIOargp* 設置為 0 即可將 socket 設置成阻塞模式,而將 argp* 設置成非 0 即可設置成非阻塞模式。示例如下:

//將 socket 設置成非阻塞模式
u_long argp = 1;
ioctlsocket(s, FIONBIO, &argp);

//將 socket 設置成阻塞模式
u_long argp = 0;
ioctlsocket(s, FIONBIO, &argp);

Windows 平台需要注意一個地方,如果對一個 socket 調用了 WSAAsyncSelect()WSAEventSelect() 函數後,再調用 ioctlsocket() 函數將該 socket 設置為非阻塞模式會失敗,你必須先調用 WSAAsyncSelect() 通過將 lEvent 參數為 0 或調用 WSAEventSelect() 通過設置 lNetworkEvents 參數為 0 來清除已經設置的 socket 相關標誌位,再次調用 ioctlsocket() 將該 socket 設置成阻塞模式才會成功。因為調用 WSAAsyncSelect()WSAEventSelect() 函數會自動將 socket 設置成非阻塞模式。MSDN 上原文(docs.microsoft.com/en-u)如下:

The WSAAsyncSelect and WSAEventSelect functions automatically set a socket to nonblocking mode. If WSAAsyncSelect or WSAEventSelect has been issued on a socket, then any attempt to use ioctlsocket to set the socket back to blocking mode will fail with WSAEINVAL. To set the socket back to blocking mode, an application must first disable WSAAsyncSelect by calling WSAAsyncSelect with the lEvent parameter equal to zero, or disable WSAEventSelect by calling WSAEventSelect with the lNetworkEvents parameter equal to zero.

關於 WSAAsyncSelect()WSAEventSelect() 這兩個函數,後文中會詳細講解。

注意事項:無論是 Linux 的 fcntl 函數,還是 Windows 的 ioctlsocket,建議讀者在實際編碼中判斷一下函數返回值以確定是否調用成功。


本文首發於『easyserverdev』公眾號,歡迎關注,轉載請保留版權信息。

歡迎加入高性能伺服器開發 QQ 群一起交流: 578019391 。


推薦閱讀:
相关文章