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的操作。