connect 函數在阻塞和非阻塞模式下的行為

在 socket 是阻塞模式下 connect 函數會一直到有明確的結果才會返回(或連接成功或連接失敗),如果伺服器地址「較遠」,連接速度比較慢,connect 函數在連接過程中可能會導致程序阻塞在 connect 函數處好一會兒(如兩三秒之久),雖然這一般也不會對依賴於網路通信的程序造成什麼影響,但在實際項目中,我們一般傾向使用所謂的非同步的 connect 技術,或者叫非阻塞的 connect。這個流程一般有如下步驟:

1. 創建socket,並將 socket 設置成非阻塞模式;
2. 調用 connect 函數,此時無論 connect 函數是否連接成功會立即返回;如果返回 -1 並不一定表示連接出錯,如果此時錯誤碼是EINPROGRESS,則表示正在嘗試連接;
3. 接著調用 select 函數,在指定的時間內判斷該 socket 是否可寫,如果可寫說明連接成功,反之則認為連接失敗。

按上述流程編寫代碼如下:

/**
* 非同步的connect寫法,nonblocking_connect.cpp
* zhangyl 2018.12.17
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.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;
}

//再將 clientfd 設置成非阻塞模式
int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1)
{
close(clientfd);
std::cout << "set socket to nonblock 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);
for (;;)
{
int ret = connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if (ret == 0)
{
std::cout << "connect to server successfully." << std::endl;
close(clientfd);
return 0;
}
else if (ret == -1)
{
if (errno == EINTR)
{
//connect 動作被信號中斷,重試connect
std::cout << "connecting interruptted by signal, try again." << std::endl;
continue;
} else if (errno == EINPROGRESS)
{
//連接正在嘗試中
break;
} else {
//真的出錯了,
close(clientfd);
return -1;
}
}
}

fd_set writeset;
FD_ZERO(&writeset);
FD_SET(clientfd, &writeset);
//可以利用tv_sec和tv_usec做更小精度的超時控制
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 0;
if (select(clientfd + 1, NULL, &writeset, NULL, &tv) == 1)
{
std::cout << "[select] connect to server successfully." << std::endl;
} else {
std::cout << "[select] connect to server error." << std::endl;
}

//5. 關閉socket
close(clientfd);

return 0;
}

為了區別到底是在調用 connect 函數時判斷連接成功還是通過 select 函數判斷連接成功,我們在後者的輸出內容中加上了「[select]」標籤以示區別。

我們先用 nc 命令啟動一個伺服器程序:

nc -v -l 0.0.0.0 3000

然後編譯客戶端程序並執行:

[root@localhost testsocket]# g++ -g -o nonblocking_connect nonblocking_connect.cpp
[root@localhost testsocket]# ./nonblocking_connect
[select] connect to server successfully.

我們把伺服器程序關掉,再重新啟動一下客戶端,這個時候應該會連接失敗,程序輸出結果如下:

[root@localhost testsocket]# ./nonblocking_connect
[select] connect to server successfully.

奇怪?為什麼連接不上也會得出一樣的輸出結果?難道程序有問題?這是因為:

  • 在 Windows 系統上,一個 socket 沒有建立連接之前,我們使用 select 函數檢測其是否可寫,能得到正確的結果(不可寫),連接成功後檢測,會變為可寫。所以,上述介紹的非同步 connect 寫法流程在 Windows 系統上時沒有問題的。
  • 在 Linux 系統上一個 socket 沒有建立連接之前,用 select 函數檢測其是否可寫,你也會得到可寫的結果,所以上述流程並不適用於 Linux 系統。正確的做法是,connect 之後,不僅要用 select 檢測可寫,還要檢測此時 socket 是否出錯,通過錯誤碼來檢測確定是否連接上,錯誤碼為 0 表示連接上,反之為未連接上。完整代碼如下:

/**
* Linux 下正確的非同步的connect寫法,linux_nonblocking_connect.cpp
* zhangyl 2018.12.17
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.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;
}

//將 clientfd 設置成非阻塞模式,
int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1)
{
close(clientfd);
std::cout << "set socket to nonblock 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);
for (;;)
{
int ret = connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if (ret == 0)
{
std::cout << "connect to server successfully." << std::endl;
close(clientfd);
return 0;
}
else if (ret == -1)
{
if (errno == EINTR)
{
//connect 動作被信號中斷,重試connect
std::cout << "connecting interruptted by signal, try again." << std::endl;
continue;
} else if (errno == EINPROGRESS)
{
//連接正在嘗試中
break;
} else {
//真的出錯了,
close(clientfd);
return -1;
}
}
}

fd_set writeset;
FD_ZERO(&writeset);
FD_SET(clientfd, &writeset);
//可以利用tv_sec和tv_usec做更小精度的超時控制
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 0;
if (select(clientfd + 1, NULL, &writeset, NULL, &tv) != 1)
{
std::cout << "[select] connect to server error." << std::endl;
close(clientfd);
return -1;
}

int err;
socklen_t len = static_cast<socklen_t>(sizeof err);
if (::getsockopt(clientfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0)
{
close(clientfd);
return -1;
}

if (err == 0)
std::cout << "connect to server successfully." << std::endl;
else
std::cout << "connect to server error." << std::endl;

//5. 關閉socket
close(clientfd);

return 0;
}

當然,在實際的項目中,第 3 個步驟中 Linux 平臺上你也可以使用 poll 函數來判斷 socket 是否可寫;在 Windows 平臺上你可以使用 WSAEventSelectWSAAsyncSelect 函數判斷連接是否成功,關於這三個函數我們將在後面的章節中詳細講解,這裡暫且僅以 select 函數為例。


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

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

推薦閱讀:

相關文章