linux網路程式設計之如何實現非同步 connect

linux網路程式設計之如何實現非同步 connect

推薦影片:

支撐網際網路的基石tcpip,5個方面全面解析

手寫一個使用者態協議棧以及零複製的實現

C/C++ Linux伺服器開發學習地址:C/C++Linux伺服器開發/後臺架構師【零聲教育】-學習影片教程-騰訊課堂

寫過網路程式的同學,應該都知道 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 是否可寫了

【文章福利】需要C/C++ Linux伺服器架構師學習資料加群812855908(資料包括C/C++,Linux,golang技術,核心,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等)

linux網路程式設計之如何實現非同步 connect

測試

在另一臺機器上執行 nc -l -v -k 192。168。70。20 5000 命令,啟動一個伺服器程式

linux網路程式設計之如何實現非同步 connect

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

執行 。/test_connect 192。168。70。20 5000, 結果如下圖

linux網路程式設計之如何實現非同步 connect

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

linux網路程式設計之如何實現非同步 connect

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

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

linux網路程式設計之如何實現非同步 connect

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

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