Select模型原理
利用select函數,判斷通訊端上是否存在資料,或者能否向一個通訊端寫入資料。目的是防止應用程式在通訊端處於鎖定模式時,調用recv(或send)從沒有資料的通訊端上接收資料,被迫進入阻塞狀態。
select參數和傳回值意義如下:
int select (
IN int nfds, //0,無意義
IN OUT fd_set* readfds, //檢查可讀性
IN OUT fd_set* writefds, //檢查可寫性
IN OUT fd_set* exceptfds, //例外資料
IN const struct timeval* timeout); //函數的返回時間
struct timeval {
long tv_sec; //秒
long tv_usec; //毫秒
};
select返回fd_set中可用的通訊端個數。
fd_set是一個SOCKET隊列,以下宏可以對該隊列進行操作:
FD_CLR( s, *set) 從隊列set刪除控制代碼s;
FD_ISSET( s, *set) 檢查控制代碼s是否存在與隊列set中;
FD_SET( s, *set )把控制代碼s添加到隊列set中;
FD_ZERO( *set ) 把set隊列初始化成空隊列.
Select工作流程
1:用FD_ZERO宏來初始化我們感興趣的fd_set。
也就是select函數的第二三四個參數。
2:用FD_SET宏來將通訊端控制代碼分配給相應的fd_set。
如果想要檢查一個通訊端是否有資料需要接收,可以用FD_SET宏把套接接字控制代碼加入可讀性檢查隊列中
3:調用select函數。
如果該通訊端沒有資料需要接收,select函數會把該通訊端從可讀性檢查隊列中刪除掉,
4:用FD_ISSET對通訊端控制代碼進行檢查。
如果我們所關注的那個通訊端控制代碼仍然在開始分配的那個fd_set裡,那麼說明馬上可以進行相應的IO操 作。比如一個分配給select第一個參數的通訊端控制代碼在select返回後仍然在select第一個參數的fd_set裡,那麼說明當前資料已經來了, 馬上可以讀取成功而不會被阻塞。
#include <winsock2.h> #include <stdio.h> #define PORT 5150 #define MSGSIZE 1024 #pragma comment(lib, "ws2_32.lib") int g_iTotalConn1 = 0; int g_iTotalConn2 = 0; SOCKET g_CliSocketArr1[FD_SETSIZE]; SOCKET g_CliSocketArr2[FD_SETSIZE]; DWORD WINAPI WorkerThread1(LPVOID lpParam); int CALLBACK ConditionFunc1(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR * g,DWORD dwCallbackData);DWORD WINAPI WorkerThread2(LPVOID lpParam); int CALLBACK ConditionFunc2(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR * g,DWORD dwCallbackData);int main(int argc, char* argv[]) { 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_family = AF_INET; local.sin_addr.S_un.S_addr = htonl(INADDR_ANY); local.sin_port = htons(PORT); bind(sListen, (sockaddr*)&local, sizeof(SOCKADDR_IN)); // Listen listen(sListen, 3); // Create worker thread CreateThread(NULL, 0, WorkerThread1, NULL, 0, &dwThreadId); // CreateThread(NULL, 0, WorkerThread2, NULL, 0, &dwThreadId); while (TRUE) { sClient = WSAAccept(sListen, (sockaddr*)&client, &iAddrSize, ConditionFunc1, 0); printf("1:Accepted client:%s:%d:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port), g_iTotalConn1); g_CliSocketArr1[g_iTotalConn1++] = sClient; /* sClient = WSAAccept(sListen, (sockaddr*)&client, &iAddrSize, ConditionFunc2, 0); printf("2:Accepted client:%s:%d:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port), g_iTotalConn2); g_CliSocketArr2[g_iTotalConn2++] = sClient; */ } return 0; } DWORD WINAPI WorkerThread1(LPVOID lpParam) { int i; fd_set fdread; int ret; struct timeval tv = {1, 0}; char szMessage[MSGSIZE]; while (TRUE) { FD_ZERO(&fdread); //1清空隊列 for (i = 0; i < g_iTotalConn1; i++) { FD_SET(g_CliSocketArr1[i], &fdread); //2將要檢查的套介面排入佇列 } // We only care read event ret = select(0, &fdread, NULL, NULL, &tv); //3查詢滿足要求的通訊端,不滿足要求,出隊 if (ret == 0) { // Time expired continue; } for (i = 0; i < g_iTotalConn1; i++) { if (FD_ISSET(g_CliSocketArr1[i], &fdread)) //4.是否依然在隊列 { // A read event happened on g_CliSocketArr ret = recv(g_CliSocketArr1[i], szMessage, MSGSIZE, 0); if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)) { // Client socket closed printf("1:Client socket %d closed.\n", g_CliSocketArr1[i]); closesocket(g_CliSocketArr1[i]); if (i < g_iTotalConn1-1) { g_CliSocketArr1[i--] = g_CliSocketArr1[--g_iTotalConn1]; } } else { // We reveived a message from client szMessage[ret] = '\0'; send(g_CliSocketArr1[i], szMessage, strlen(szMessage), 0); } } } } } int CALLBACK ConditionFunc1(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR * g,DWORD dwCallbackData){ if (g_iTotalConn1 < FD_SETSIZE) return CF_ACCEPT; else return CF_REJECT;}DWORD WINAPI WorkerThread2(LPVOID lpParam) { int i; fd_set fdread; int ret; struct timeval tv = {1, 0}; char szMessage[MSGSIZE]; while (TRUE) { FD_ZERO(&fdread); //1清空隊列 for (i = 0; i < g_iTotalConn2; i++) { FD_SET(g_CliSocketArr2[i], &fdread); //2將要檢查的套介面排入佇列 } // We only care read event ret = select(0, &fdread, NULL, NULL, &tv); //3查詢滿足要求的通訊端,不滿足要求,出隊 if (ret == 0) { // Time expired continue; } for (i = 0; i < g_iTotalConn2; i++) { if (FD_ISSET(g_CliSocketArr2[i], &fdread)) //4.是否依然在隊列 { // A read event happened on g_CliSocketArr ret = recv(g_CliSocketArr2[i], szMessage, MSGSIZE, 0); if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)) { // Client socket closed printf("2:Client socket %d closed.\n", g_CliSocketArr1[i]); closesocket(g_CliSocketArr2[i]); if (i < g_iTotalConn2-1) { g_CliSocketArr2[i--] = g_CliSocketArr2[--g_iTotalConn2]; } } else { // We reveived a message from client szMessage[ret] = '\0'; send(g_CliSocketArr2[i], szMessage, strlen(szMessage), 0); } } } } } int CALLBACK ConditionFunc2(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR * g,DWORD dwCallbackData){ if (g_iTotalConn2 < FD_SETSIZE) return CF_ACCEPT; else return CF_REJECT;}
伺服器的幾個主要動作如下:
1.建立監聽通訊端,綁定,監聽;
2.建立工作者線程;
3.建立一個通訊端數組,用來存放當前所有活動的用戶端通訊端,每accept一個串連就更新一次數組;
4.接受用戶端的串連。
這裡有一點需要注意的,就是我沒有重新定義FD_SETSIZE宏,所以伺服器最多支援的並發串連數為64。而且,這裡決不能無條件的accept,伺服器應該根據當前的串連數來決定是否接受來自某個用戶端的串連。
工作者線程裡面是一個死迴圈,一次迴圈完成的動作是:
1.將當前所有的用戶端通訊端加入到讀集fdread中;
2.調用select函數;
3.查看某個通訊端是否仍然處於讀集中,如果是,則接收資料。如果接收的資料長度為0,或者發生WSAECONNRESET錯誤,則表示用戶端通訊端主動關閉,這時需要將伺服器中對應的通訊端所綁定的資源釋放掉,然後調整我們的通訊端數組(將數組中最後一個通訊端挪到當前的位置上)
除了需要有條件接受用戶端的串連外,還需要在串連數為0的情形下做特殊處理,因為如果讀集中沒有任何通訊端,select函數會立刻返回,這將導致工作者線程成為一個毫無停頓的死迴圈,CPU的佔用率馬上達到100%。
關係到通訊端列表的操作都需要使用迴圈,在輪詢的時候,需要遍曆一次,再新的一輪開始時,將列表排入佇列又需要遍曆一次.也就是說,Select在工作一次時,需要至少遍曆2次列表,這是它效率較低的原因之一.在大規模的網路連接方面,還是推薦使用IOCP或EPOLL模型.但是Select模型可以使用在諸如對戰類遊戲上,比如類似星際這種,因為它小巧易於實現,而且對戰類遊戲的網路連接量並不大.
對於Select模型想要突破Windows 64個限制的話,可以採取分段輪詢,一次輪詢64個.例如通訊端列表為128個,在第一次輪詢時,將前64個放入隊列中用Select進行狀態查詢,待本次操作全部結束後.將後64個再加入輪詢隊列中進行輪詢處理.這樣處理需要在非阻塞式下工作.以此類推,Select也能支援無限多個.
注意:
1.那個最大的串連數是指每一個線程可以處理的串連數,當你有多個線程時,串連數是可以無限增長的,不過此時的效率就比較低。
2.關於發送操作writefds的問題,當通訊端成功串連或者一個通訊端剛剛成功接收資訊時都會調用。
3.我們通常會建立一個通訊端來進行監聽,之後用accept返回的通訊端進行通訊。這裡要注意一點,用於監聽的通訊端在沒有新串連時也會進行writefds的操作。