通訊程式設計:Select 模型通訊
非阻塞模式
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 連線中,客戶端和伺服器相互發送一條資料即可。
伺服器
使用 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