如何實現非同步 connect

寫過網路程式的同學,應該都知道 connect 函數,在 socket 開始讀寫操作之前,先要進行連接,也即 TCP 的三次握手 , 這個過程就是在 connect 函數中完成的, connect 函數本身是阻塞的,通過設置 socket 的選項及調用 select/poll 函數可以實現非同步 connect 的功能

socket 默認是阻塞模式,處於阻塞模式時,調用 connect 函數之後, 會一直等待連接結果返回為止,要麼成功,要麼失敗,connect 函數返回 0 時成功,返回 -1 失敗

在區域網中,調用 connect 函數,基本上會立即返回結果,當伺服器在國外時,connect 函數時會阻塞一段時間,大概幾秒鐘吧,具體的時間還要看當時的網路狀況

為什麼要用非同步 connect

Linux 下 connect 默認的超時時間大概在一分鐘左右(不同的Linux版本略有差別),在實際的開發中,這個時間顯得有點兒長了

對於伺服器來說,需要為很多的客戶端服務,要盡量減少阻塞,所以,一般都是採用 非同步 connect 的技術

對於每一個編寫網路程式的同學來說,非同步connect 應該是必須掌握的基本功

非同步connect 步驟

(1) 創建socket,調用 fcntl 函數將其設置為非阻塞 

(2) 調用 connect 函數,返回 0 表示連接成功,返回 -1,需要檢查錯誤碼

    如果錯誤碼為 EINPROGRESS,表示正在建立連接中
    
    如果錯誤碼是 EINTR 表示,表示發生了系統中斷,這時繼續執行連接即可
    
    如果是其他錯誤碼,調用 close(fd) 函數關閉 socket, 連接失敗

(3) 將 socket 加入 select/poll 的可寫文件描述符集合中,並設置超時時間

(4) 判斷 select/poll 函數的返回值
    
    小於等於 0 表示失敗
    
    其他,表示 socket 可寫,調用 getsockopt 函數 捕獲 socket 的錯誤資訊
            

具體的程式碼如下:

/*
    非同步 connect 測試程式碼, test_connect.cpp
*/
#include <stdint.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <poll.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <netdb.h>
#include <errno.h>
#include <stdarg.h>
#include <poll.h>
#include <limits.h>
#include <iostream>
using namespace std;

int32_t main(int32_t argc, char *argv[])
{
    if(argc < 3)
    {
        std::cout << "argc < 3..." << std::endl;
        return -1;
    }
    std::string strip = argv[1];
    uint32_t port = atoi(argv[2]);
    //創建 socket
    int32_t fd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == fd)
    {
        std::cout << "create socket error:" << errno << std::endl;
        return -1;
    }
    //將 socket 設置成非阻塞
    int32_t flag = fcntl(fd, F_GETFL, 0);
    flag |= O_NONBLOCK;
    if(-1 == fcntl(fd, F_SETFL, flag))
    {
        std::cout << " set socket nonblock error:" << errno << std::endl;
        close(fd);
        return -1;
    }
    //伺服器地址
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(strip.c_str());
    //
    for(; ;)
    {
        //連接伺服器
        int32_t ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr) );
        if(-1 == ret)
        {
            int32_t err = errno;
            if(EINTR == err)
            {
                //connect被中斷,繼續重試
                //如果不處理 EINTR 錯誤的話,connect邏輯可以不用放到 for 循環中
                continue;
            }
            if(EINPROGRESS != err)
            {
                std::cout << "connect err:" << errno << ", str:" << strerror(errno) <<  std::endl;
                goto exit;
            }
            //正在連接中
            std::cout << "connecting..." << std::endl;
            //處理結果
            int32_t result = -1;
    #if 1
            //將 socket 加入到 poll 的可寫集合中
            struct pollfd wfd[1];
            wfd[0].fd = fd;
            wfd[0].events = POLLOUT;
            //檢測 socket 是否可寫
            result = poll(wfd, 1, 3000);
    #elif 0
            //設置超時時間
            struct timeval tval;
            tval.tv_sec = 3;
            tval.tv_usec = 0;
            //將 socket 加入到 select 的可寫集合中
            fd_set wfds;
            FD_ZERO(&wfds);
            FD_SET(fd,&wfds);
            //檢測 socket 是否可寫
            result = select(fd + 1, nullptr, &wfds, nullptr,&tval);
    #endif
            std::cout << "async connect result:" << result << std::endl;
            // 失敗
            if(result <= 0 )
            { 
                std::cout << "async connect err:" << errno << ", str:" << strerror(errno) << std::endl;
                goto exit;
            }
            //檢查socket 錯誤資訊
            int32_t temperr = 0;
            socklen_t temperrlen = sizeof(temperr);
            if(-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, (void*)&temperr, &temperrlen) )
            {
                std::cout << "async connect...getsockopt err:" << errno << ", str:" << strerror(errno) <<  std::endl;
                goto exit;
            }
            if(0 != temperr)
            {
                std::cout << "async connect...getsockopt temperr:" << temperr << ", str:" << strerror(temperr) << std::endl;
                goto exit;
            }
            //成功
            std::cout << "async connect success..." << std::endl;
            goto exit;
        }
        else
        {
             //連接成功
            std::cout << "connect success..." << std::endl;
            goto exit;          
        }
    } // end of  for(; ;)
exit:
    std::cout << "quit...." << std::endl;
    close(fd);
    return 0;
}

  • 程式碼說明

如果不處理 EINTR 錯誤的話,connect 函數及後面的邏輯可以不用放到 for 循環中

檢查 socket 是否可寫,調用 select 或者 poll 函數都可以,上述程式碼中使用的是 poll 函數,將程式碼中的 #if 1 改成 #if 0 以及 #elif 0 改成 #elif 1 , 就是使用 select 函數檢測 socket 是否可寫了

測試

在另一台機器上執行 nc -l -v -k 192.168.70.20 5000 命令,啟動一個伺服器程式

在當前機器上執行 g++ -g -Wall -std=c++11 -o test_connect test_connect.cpp 進行編譯

執行 ./test_connect 192.168.70.20 5000, 結果如下圖

此時,伺服器程式顯示如下:

通過 test_connect 程式端的截圖可以看出,調用 connect 函數之後,返回了 EINPROGRESS 錯誤碼,然後調用 select/poll 函數返回 1, 表示 socket 可寫,緊接著調用 getsockopt 函數檢查 socket 錯誤資訊,通過列印的資訊知道,socket 無錯誤資訊,即 連接成功

我們在伺服器機器上按 CTRL + C 停止伺服器程式,然後關閉 test_connect 程式,再次執行 ./test_connect 192.168.70.20 5000 ,結果如下圖:

從上圖可以看出,即使伺服器程式已經退出了,調用 select/poll 之後還是返回 socket 可寫,當繼續調用 getsockopt 函數檢查 socket 錯誤碼,此時錯誤碼是 111, 表示連接被拒絕,也即連接失敗

這裡要注意一個很重要的點, 在 Linux 上,即使 socket 沒有連接成功,調用 select/poll 時,仍然返回 socket 是可寫的,所以 除了調用 select/poll 檢查 socket 可寫之外,還需要調用 getsockopt 函數檢查 socket 的錯誤碼,錯誤碼為 0 表示連接成功,其他表示連接失敗

Tags: