非阻塞模式
Winsock 可以在阻塞和非阻塞模式下執行 I/O 操作,套接字創建時默認工作在阻塞模式下。也就是說當某個操作不能執行時,程序會先阻塞,等待操作可以被執行時才繼續程序。例如對 recv 函數的調用會使程序進入等待狀態,直到接收到數據才返回。
阻塞套接字的好處是使用簡單,但是當需要處理多個套接字連接時,就必須創建多個線程,給編程帶來了許多不便。所以實際開發中使用最多的還是非阻塞模式,它使用起來比較復雜,但是處理發送和接收數據或者管理連接的 Winsock 調用將會立即返回,效率很高。
不過如果系統輸入緩沖區中沒有待處理的數據,那么對 recv 的調用將返回 WSAEWOULDBLOCK 錯誤。關鍵的問題在於如何確定套接字什么時候可讀/可寫,如果需要不斷調用函數去測試的話,程序的性能勢必會受到影響,解決的辦法就是使用 Windows 提供的不同的 I/O 模型。
Select 模型
select 模型的設計源於 UNIX 系統,主要實現的原理是 IO 多路復用。select 模型的優勢是程序能夠在單個線程內同時處理多個套接字連接,這避免了阻塞模式下的線程膨脹問題。但是添加到 fd_set 結構的套接字數量是有限制的,如果能能添加的 socket 太多的話,服務器性能就會受到影響。

select 函數
模型通過使用 select 函數來管理 I/O,函數可以確定一個或者多個套接字的狀態。如果套接字上沒有網絡事件發生,便進入等待狀態,以便執行同步 I/O。
int
WSAAPI
select(
_In_ int nfds,
_Inout_opt_ fd_set FAR * readfds,
_Inout_opt_ fd_set FAR * writefds,
_Inout_opt_ fd_set FAR * exceptfds,
_In_opt_ const struct timeval FAR * timeout
);
函數調用成功返回發生網絡事件的所有 socket 數量的綜合,超過時間限制就返回 0.
| 參數 | 說明 |
|---|---|
| nfds | 忽略,為了與 Berkeley 套接字兼容 |
| readfds | 指向一個套接字集合,用來檢查其可讀性 |
| writefds | 指向一個套接字集合,用來檢查其可寫性 |
| exceptfds | 指向一個套接字集合,用來檢查錯誤 |
| timeout | 指定此函數等待的最長時間,為 NULL 時最長時間為無限大 |
套接字集合
fd_set 結構是 socket 集合,它可以把多個套接字連在一起,select 函數可以測試這個集合中哪些套接字有事件發生。
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
WINSOCK 定義了 4 個操作 fd_set 的宏。
| 宏 | 功能 |
|---|---|
| FD_ZERO(*set) | 初始化 set 為空集合,集合在使用前應該總是清空 |
| FD_CLR(s, *set) | 從 set 移除套接字 s |
| FD_ISSET(s, *set) | 檢查 s 是不是 set 的成員,如果是返回 TRUE |
| FD_SET(s, *set) | 添加套接字到集合 |
網絡事件
傳遞給 select 函數的 3 個 fd_set 結構分別用於為了檢查可讀性(readfds)、檢查可寫性(writefds)和檢查錯誤(exceptfds)。當我們想要測試某個 socket 的某種狀態是,就把它放入對應的 fd_set 中,等待 select 函數返回。select 函數調用完成后,若 socket 還在 fd_set 中,就說明該 socket 滿足可讀、可寫或者出錯了。
設置超時
timeout 是 timeval 結構的指針,它指定了 select 函數等待的最長時間。
/*
* Structure used in select() call, taken from the BSD file sys/time.h.
*/
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* and microseconds */
};
| 參數 | 說明 |
|---|---|
| tv_sec | 等待多少秒 |
| tv_usec | 等待多少毫秒 |
| 如果 timeout 設為 NULL,select 將會無限阻塞。 |
Select 模型樣例
注意無論是客戶端還是服務器,都需要包含頭文件 initsock.h 來載入 Winsock。
功能設計
模擬實現 TCP 協議通信過程,要求編程實現服務器端與客戶端之間雙向數據傳遞。也就是在一條 TCP 連接中,客戶端和服務器相互發送一條數據即可。
initsock.h
#include <winsock2.h>
#pragma comment(lib, "WS2_32") // 鏈接到 WS2_32.lib
class CInitSock
{
public:
/*CInitSock 的構造器*/
CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
{
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(minorVer, majorVer);
if (::WSAStartup(sockVersion, &wsaData) != 0)
{
exit(0);
}
}
/*CInitSock 的析構器*/
~CInitSock()
{
::WSACleanup();
}
};
服務器
使用 Select 模型實現的服務器需要按照如圖所示的步驟進行編程,具體編碼如下所示。

#include "initsock.h"
#include <iostream>
using namespace std;
CInitSock theSock; // 初始化Winsock庫
int main()
{
// 創建監聽套接字
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(4567);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
// 綁定套接字到本地機器
if (::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
{
cout << " Failed bind()" << endl;
return -1;
}
// 進入監聽模式
if (::listen(sListen, 5) == SOCKET_ERROR)
{
cout << " Failed listen()" << endl;
return 0;
}
cout << "服務器已啟動監聽,可以接收連接!" << endl;
// select模型處理過程
// 1)初始化一個套接字集合fdSocket,添加監聽套接字句柄到這個集合
fd_set fdSocket; // 所有可用套接字集合
FD_ZERO(&fdSocket);
FD_SET(sListen, &fdSocket);
while (TRUE)
{
// 2)將fdSocket集合的一個拷貝fdRead傳遞給select函數,
// 當有事件發生時,select函數移除fdRead集合中沒有未決I/O操作的套接字句柄,然后返回。
fd_set fdRead = fdSocket;
int nRet = ::select(0, &fdRead, NULL, NULL, NULL);
if (nRet > 0)
{
// 3)通過將原來fdSocket集合與select處理過的fdRead集合比較,
// 確定都有哪些套接字有未決I/O,並進一步處理這些I/O。
for (int i = 0; i < (int)fdSocket.fd_count; i++)
{
if (FD_ISSET(fdSocket.fd_array[i], &fdRead))
{
if (fdSocket.fd_array[i] == sListen) // (1)監聽套接字接收到新連接
{
if (fdSocket.fd_count < FD_SETSIZE)
{
sockaddr_in addrRemote;
int nAddrLen = sizeof(addrRemote);
//接收客戶端的連接請求
SOCKET sNew = ::accept(sListen, (SOCKADDR*)&addrRemote, &nAddrLen);
FD_SET(sNew, &fdSocket);
cout << "\n與主機" << ::inet_ntoa(addrRemote.sin_addr) << "建立連接" << endl;
}
else
{
cout << " Too much connections!" << endl;
continue;
}
}
else
{
char szText[256];
int nRecv = ::recv(fdSocket.fd_array[i], szText, strlen(szText), 0);
if (nRecv > 0) // (2)可讀
{
//接收數據
szText[nRecv] = '\0';
cout << " 接收到數據:" << szText << endl;
//發送數據
char result[20];
char sendText[] = "你好,客戶端!";
if(::send(fdSocket.fd_array[i], sendText, strlen(sendText), 0) > 0)
{
cout << " 向客戶端發送數據:" << sendText << endl;
}
}
else // (3)連接關閉、重啟或者中斷
{
::closesocket(fdSocket.fd_array[i]);
FD_CLR(fdSocket.fd_array[i], &fdSocket);
}
}
}
}
}
else
{
cout << " Failed select()" << endl;
break;
}
}
return 0;
}
客戶端
#include "InitSock.h"
#include <iostream>
using namespace std;
CInitSock initSock; // 初始化Winsock庫
int main()
{
// 創建套節字
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET)
{
cout << " Failed socket()" << endl;
return 0;
}
// 也可以在這里調用bind函數綁定一個本地地址
// 否則系統將會自動安排
char address[20] = "127.0.0.1";
// 填寫遠程地址信息
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(4567);
// 注意,這里要填寫服務器程序(TCPServer程序)所在機器的IP地址
// 如果你的計算機沒有聯網,直接使用127.0.0.1即可
servAddr.sin_addr.S_un.S_addr = inet_addr(address);
if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
{
cout << " Failed connect() " << endl;
return 0;
}
else
{
cout << "與服務器 " << address << "建立連接" << endl;
}
char szText[] = "你好,服務器!";
if (::send(s, szText, strlen(szText), 0) > 0)
{
cout << " 發送數據:" << szText << endl;
}
// 接收數據
char buff[256];
int nRecv = ::recv(s, buff, 256, 0);
if (nRecv > 0)
{
buff[nRecv] = '\0';
cout << " 接收到數據:" << buff << endl;
}
// 關閉套節字
::closesocket(s);
return 0;
}
運行效果

參考資料
《Windows 網絡與通信編程》,陳香凝 王燁陽 陳婷婷 張錚 編著,人民郵電出版社
UNIX再學習 -- 函數 select、poll、epoll
