兩種I/O模式
* 阻塞模式:執行I/O操作完成前會一直進行等待,不會將控制權交給程序。套接字 默認為阻塞模式。可以通過多線程技術進行處理。
* 非阻塞模式:執行I/O操作時,Winsock函數會返回並交出控制權。這種模式使用 起來比較復雜,因為函數在沒有運行完成就進行返回,會不斷地返回 WSAEWOULDBLOCK錯誤。但功能強大。
比較容易想到的一種服務器模型就是采用一個主線程,負責監聽客戶端的連接請求,當接收到某個客戶端的連接請求后,創建一個專門用於和該客戶端通信的 套接字和一個輔助線程。以后該客戶端和服務器的交互都在這個輔助線程內完成。這種方法比較直觀,程序非常簡單而且可移植性好,但是不能利用平台相關的特 性。例如,如果連接數增多的時候(成千上萬的連接),那么線程數成倍增長,操作系統忙於頻繁的線程間切換,而且大部分線程在其生命周期內都是處於非活動狀 態的,這大大浪費了系統的資源。所以,如果你已經知道你的代碼只會運行在Windows平台上,建議采用Winsock I/O模型。
select 五種IO模型之一
select模型:
通過調用select函數可以確定一個或多個套接字的狀態,判斷套接字上是否有數據,或 者能否向一個套接字寫入數據。
int select( int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR *exceptfds, const struct timeval FAR * timeout );
Select模型是最常見的I/O模型。
使用 int select( int nfds , fd_set FAR* readfds , fd_set FAR* writefds,fd_set FAR* exceptfds,const struct timeval FAR * timeout ) ;
函數來檢查你要調用的Socket套接字是否已經有了需要處理的數據。
select包含三個Socket隊列,分別代表: readfds ,檢查可讀性,writefds,檢查可寫性,exceptfds,例外數據。 timeout是select函數的返回時間。
例如,我們想要檢查一個套接字是否有數據需要接收,我們可以把套接字句柄加入可讀性檢查隊列中,然后調用select,如果,該套接字沒有數據需要接收, select函數會把該套接字從可讀性檢查隊列中刪除掉,所以我們只要檢查該套接字句柄是否還存在於可讀性隊列中,就可以知道到底有沒有數據需要接收了。
WinSock提供了一些宏用來操作套接字隊列fd_set。
FD_CLR( s,*set) 從隊列set刪除句柄s。
FD_ISSET( s, *set) 檢查句柄s是否存在與隊列set中。
FD_SET( s,*set )把句柄s添加到隊列set中。
FD_ZERO( *set ) 把set隊列初始化成空隊列。
◆先來看看涉及到的結構的定義: a、 fd_set結構:
#define FD_SETSIZE 64
typedef struct fd_set
{
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
fd_count為已設定socket的數量
fd_array為socket列表,FD_SETSIZE為最大socket數量,建議不小於64。這是微軟建 議的。(是否是不應該大於64)
B、timeval結構:
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* and microseconds */
};
tv_sec為時間的秒值。 tv_usec為時間的毫秒值。 這個結構主要是設置select()函數的等待值,如果將該結構設置為(0,0),則select()函數 會立即返回。
◆再來看看select函數各參數的作用:
1. nfds:沒有任何用處,主要用來進行系統兼容用,一般設置為0。
2. readfds:等待可讀性檢查的套接字組。
3. writefds;等待可寫性檢查的套接字組。
4. exceptfds:等待錯誤檢查的套接字組。
5. timeout:超時時間。
6. 函數失敗的返回值:調用失敗返回SOCKET_ERROR,超時返回0。
readfds、writefds、exceptfds三個變量至少有一個不為空,同時這個不為空的套接字組 種至少有一個socket,道理很簡單,否則要select干什么呢。 舉例:測試一個套接字是否可讀:
fd_set fdread;
FD_SET(s,&fdread); //加入套接字
if(FD_ISSET(s,&fread) //是否存在fread中
如果你想在Windows平台上構建服務器應用,那么I/O模型是你必須考慮的。Windows操作系統提供了選擇(Select)、異步選擇 (WSAAsyncSelect)、事件選擇(WSAEventSelect)、重疊I/O(Overlapped I/O)和完成端口(Completion Port)共五種I/O模型。每一種模型均適用於一種特定的應用場景。程序員應該對自己的應用需求非常明確,而且綜合考慮到程序的擴展性和可移植性等因 素,作出自己的選擇。
(節選自《Windows網絡編程》第八章) 下面的這段程序就是利用選擇模型實現的Echo服務器的代碼:
#include <winsock.h> #include <stdio.h> #define PORT 5150 #define MSGSIZE 1024 #pragma comment(lib, "ws2_32.lib") int g_iTotalConn = 0; SOCKET g_CliSocketArr[FD_SETSIZE]; DWORD WINAPI WorkerThread(LPVOID lpParameter); int main() { WSADATA wsaData; SOCKET sListen, sClient; SOCKADDR_IN local, client; int iaddrSize = sizeof(SOCKADDR_IN); DWORD dwThreadId; // Initialize Windows socket library WSAStartup(0x0202, &wsaData); // Create listening socket sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // Bind local.sin_addr.S_un.S_addr = htonl(INADDR_ANY); local.sin_family = AF_INET; local.sin_port = htons(PORT); bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN)); // Listen listen(sListen, 3); // Create worker thread HANDLE hHandle = CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId); CloseHandle(hHandle); while (TRUE) { // Accept a connection sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize); printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port)); // Add socket to fdTotal g_CliSocketArr[g_iTotalConn++] = sClient; } return 0; } DWORD WINAPI WorkerThread(LPVOID lpParam) { int i; fd_set fdread; int ret; struct timeval tv = {1, 0}; char szMessage[MSGSIZE]; while (TRUE) { FD_ZERO(&fdread); for (i = 0; i < g_iTotalConn; i++) { FD_SET(g_CliSocketArr[i], &fdread); } // We only care read event ret = select(0, &fdread, NULL, NULL, &tv); if (ret == 0) { // Time expired continue; } for (i = 0; i < g_iTotalConn; i++) { if (FD_ISSET(g_CliSocketArr[i], &fdread)) { // A read event happened on pfdTotal->fd_array[i] ret = recv(g_CliSocketArr[i], szMessage, MSGSIZE, 0); if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)) { // Client socket closed printf("Client socket %d closed.\n", g_CliSocketArr[i]); closesocket(g_CliSocketArr[i]); if (i < g_iTotalConn - 1) { g_CliSocketArr[i--] = g_CliSocketArr[--g_iTotalConn]; } } else { // We received a message from client szMessage[ret] = '\0'; send(g_CliSocketArr[i], szMessage, strlen(szMessage), 0); } } } } return 0; }
服務器的幾個主要動作如下:
1.創建監聽套接字,綁定,監聽;
2.創建工作者線程;
3.創建一個套接字數組,用來存放當前所有活動的客戶端套接字,每accept一個連接就更新一次數組;
4. 接受客戶端的連接。這里有一點需要注意的,就是我沒有重新定義FD_SETSIZE宏,所以服務器最多支持的並發連接數為64。而且,這里決不能無條件的 accept,服務器應該根據當前的連接數來決定是否接受來自某個客戶端的連接。一種比較好的實現方案就是采用WSAAccept函數,而且讓 WSAAccept回調自己實現的Condition Function。如下所示:
int CALLBACK ConditionFunc(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR * g,DWORD dwCallbackData)
{
if (當前連接數 < FD_SETSIZE)
return CF_ACCEPT;
else return CF_REJECT;
}
工作者線程里面是一個死循環,一次循環完成的動作是: 1.將當前所有的客戶端套接字加入到讀集fdread中; 2.調用select函數; 3. 查看某個套接字是否仍然處於讀集中,如果是,則接收數據。如果接收的數據長度為0,或者發生WSAECONNRESET錯誤,則表示客戶端套接字主動關 閉,這時需要將服務器中對應的套接字所綁定的資源釋放掉,然后調整我們的套接字數組(將數組中最后一個套接字挪到當前的位置上)
除了需要有條件接受客戶端的連接外,還需要在連接數為0的情形下做特殊處理,因為如果讀集中沒有任何套接字,select函數會立刻返回,這將導致工作者線程成為一個毫無停頓的死循環,CPU的占用率馬上達到100%。