一.選擇模型
Select模型原理
利用select函數(shù),判斷套接字上是否存在數(shù)據(jù),或者能否向一個套接字寫入數(shù)據(jù)。目的是防止應(yīng)用程序在套接字處于鎖定模式時,調(diào)用recv(或send)從沒有數(shù)據(jù)的套接字上接收數(shù)據(jù),被迫進入阻塞狀態(tài)。
select參數(shù)和返回值意義如下:
int select (
IN int nfds, //0,無意義
IN OUT fd_set* readfds, //檢查可讀性
IN OUT fd_set* writefds, //檢查可寫性
IN OUT fd_set* exceptfds, //例外數(shù)據(jù)
IN const struct timeval* timeout); //函數(shù)的返回時間
struct timeval {
long tv_sec; //秒
long tv_usec; //毫秒
};
select返回fd_set中可用的套接字個數(shù)。
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函數(shù)的第二三四個參數(shù)。
2:用FD_SET宏來將套接字句柄分配給相應(yīng)的fd_set。
如果想要檢查一個套接字是否有數(shù)據(jù)需要接收,可以用FD_SET宏把套接接字句柄加入可讀性檢查隊列中
3:調(diào)用select函數(shù)。
如果該套接字沒有數(shù)據(jù)需要接收,select函數(shù)會把該套接字從可讀性檢查隊列中刪除掉,
4:用FD_ISSET對套接字句柄進行檢查。
如果我們所關(guān)注的那個套接字句柄仍然在開始分配的那個fd_set里,那么說明馬上可以進行相應(yīng)的IO操 作。比如一個分配給select第一個參數(shù)的套接字句柄在select返回后仍然在select第一個參數(shù)的fd_set里,那么說明當(dāng)前數(shù)據(jù)已經(jīng)來了, 馬上可以讀取成功而不會被阻塞。
/*************************************************************************************/
#include "stdafx.h"
#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 lpParam);
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, WorkerThread, NULL, 0, &dwThreadId);
while (TRUE)
{
// Accept a connection
sClient = accept(sListen, (sockaddr*)&client, &iAddrSize);
printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
// Add socket to g_CliSocketArr
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); //1清空隊列
for (i = 0; i < g_iTotalConn; i++)
{
FD_SET(g_CliSocketArr[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_iTotalConn; i++)
{
if (FD_ISSET(g_CliSocketArr[i], &fdread)) //4.是否依然在隊列
{
// A read event happened on g_CliSocketArr
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 reveived a message from client
szMessage[ret] = '\0';
send(g_CliSocketArr[i], szMessage, strlen(szMessage), 0);
}
}
}
}
}
服務(wù)器的幾個主要動作如下:
1.創(chuàng)建監(jiān)聽套接字,綁定,監(jiān)聽;
2.創(chuàng)建工作者線程;
3.創(chuàng)建一個套接字數(shù)組,用來存放當(dāng)前所有活動的客戶端套接字,每accept一個連接就更新一次數(shù)組;
4.接受客戶端的連接。
這里有一點需要注意的,就是我沒有重新定義FD_SETSIZE宏,所以服務(wù)器最多支持的并發(fā)連接數(shù)為64。而且,這里決不能無條件的accept,服務(wù)器應(yīng)該根據(jù)當(dāng)前的連接數(shù)來決定是否接受來自某個客戶端的連接。一種比較好的實現(xiàn)方案就是采用WSAAccept函數(shù),而且讓WSAAccept回調(diào)自己實現(xiàn)的Condition Function。如下所示:
int CALLBACK ConditionFunc(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR * g,DWORD dwCallbackData)
{
if (當(dāng)前連接數(shù) < FD_SETSIZE)
return CF_ACCEPT;
else
return CF_REJECT;
}
工作者線程里面是一個死循環(huán),一次循環(huán)完成的動作是:
1.將當(dāng)前所有的客戶端套接字加入到讀集fdread中;
2.調(diào)用select函數(shù);
3.查看某個套接字是否仍然處于讀集中,如果是,則接收數(shù)據(jù)。如果接收的數(shù)據(jù)長度為0,或者發(fā)生WSAECONNRESET錯誤,則表示客戶端套接字主動關(guān)閉,這時需要將服務(wù)器中對應(yīng)的套接字所綁定的資源釋放掉,然后調(diào)整我們的套接字數(shù)組(將數(shù)組中最后一個套接字挪到當(dāng)前的位置上)
除了需要有條件接受客戶端的連接外,還需要在連接數(shù)為0的情形下做特殊處理,因為如果讀集中沒有任何套接字,select函數(shù)會立刻返回,這將導(dǎo)致工作者線程成為一個毫無停頓的死循環(huán),CPU的占用率馬上達到100%。
關(guān)系到套接字列表的操作都需要使用循環(huán),在輪詢的時候,需要遍歷一次,再新的一輪開始時,將列表加入隊列又需要遍歷一次.也就是說,Select在工作一次時,需要至少遍歷2次列表,這是它效率較低的原因之一.在大規(guī)模的網(wǎng)絡(luò)連接方面,還是推薦使用IOCP或EPOLL模型.但是Select模型可以使用在諸如對戰(zhàn)類游戲上,比如類似星際這種,因為它小巧易于實現(xiàn),而且對戰(zhàn)類游戲的網(wǎng)絡(luò)連接量并不大.
對于Select模型想要突破Windows 64個限制的話,可以采取分段輪詢,一次輪詢64個.例如套接字列表為128個,在第一次輪詢時,將前64個放入隊列中用Select進行狀態(tài)查詢,待本次操作全部結(jié)束后.將后64個再加入輪詢隊列中進行輪詢處理.這樣處理需要在非阻塞式下工作.以此類推,Select也能支持無限多個.