WSAEventSelect模型
Winsock提供另一种有用的异步事件通知I/O模型――WSAEventSelect模型。这个模型与WSAAsyncSelect模型类似,允许应用程序在一个或者多个套接字上接收基于事件网络通知。它与WSAAsyncSelect模型类似是因为它也接收FD_XXX类型的网络事件,不过并不是依靠Windows的消息驱动机制,而是经由事件对象句柄通知。
使用这个模型的基本思路是为感兴趣的一组网络事件创建一个事件对象,再调用WSAEventSelect函数将网络事件和事件对象关联起来。当网络事件发生时,Winsock使相应的事对象触发,在事件对象上的等待函数就会返回。之后调用WSAEnumNetworkEvents函数便可获取到底发生了什么网络事件。
具体编程流程:
1、创建一个事件句柄表和一个对应的套接字句柄表
2、每创建一个套接字,就创建一个事件对象,把它们的句柄分别放入上面的两个表中,并调用WSAEventSelect添加它们的关联。
3、调用WSAWaitForMultipleEvents在所有事件对象上等待,此函数返回后,我们对事件句柄表中每个事件调用 WSAWaitForMultipleEvents函数,以便确认在哪些套接字上发生了网络事件。
4、处理发生的网络事件,继续在事件对象上等待。
书上源代码的实现流程:
1、声明两个结构体,其中一个为套接字对象。
typedef struct _SOCKET_OBJ
{
SOCKET s ; //套接字,调用WSAEventSelect与相应事件对象就是用这个套接字
HANDLE event ; //与套接字相关的事件句柄,,调用WSAEventSelect与相应套接字关联就是用这个事件对象了
sockaddr_in addrRemote ; //客户端地址
_SOCKET_OBJ *pNext ; //下一个地址,套接字对象用链表连接起来
}SOCKET_OBJ,*PSOCKET_OBJ ;
因为WSAWaitForMultipleEvents最多支持WSA_MAXIMUM_WAIT_EVENTS个对象,而WSA_MAXIMUM_WAIT_EVENTS被定义为64。因此这个I/O模型在一个线程中同一时间最多能支持64个套接字,如果需要使用这个模型管理更多套接字,就需要创建额外的工作线程,WSAWaitForMultipleEvents更多的对象了。或者不用创建线程的方法,而用另外的数据结构存储要等待的对象,设置一个超时,分批轮询等待。
所以需要声明另外一个结构体,线程对象,用于工作线程管理对应的套接字对象。
//线程对象,每一个线程负责管理最多63连接,其中一个事件对象用于指示重建操作
typedef struct _THREAD_OBJ
{
HANDLE events[WSA_MAXIMUM_WAIT_EVENTS] ; //记录当前线程要等待的事件对象的句柄,事件对象表
int nSocketCount ; //记录当前线程处理的套接字的数量,最多为63个
PSOCKET_OBJ pSockHeader ; //当前线程处理的套接字对象的列表,pSockHeader指向表头
PSOCKET_OBJ pSockTail ; //pSockTail指向表尾
CRITICAL_SECTION cs ; //关键代码段变量,为的是同步对本结构的访问。有两个线程会对线程对象进行访问。一个是监听线程,需要分配新连 //接的套接字对象给线程对象。一个是对应于该线程对象的工作线程,管理所拥有的套接字对象
_THREAD_OBJ *pNext ; //指向下一个THREAD_OBJ对象,为的是连成一个表
} THREAD_OBJ ,*PTHREAD_OBJ ;
线程:
主线程:
主线程负责监听新连接的到来,关联新套接字与新的事件对象,分配新的套接字对象到现存或者新建的线程对象当中,并每隔一段时间,打印“已经接受过的连接”和“当前连接数”。
工作线程:
工作线程负责等待自己所属线程对象拥有的套接字对象。
执行重建操作或者分别处理可读、可写、关闭套接字的事件。
另外一点需要说明的是,线程对象中的event[0]对象,是用于指示线程是否需要重建套接字对象与事件对象的映射关系。因为套接字对象在线程对象中是以链表形式储存的,而事件对象则是以数组形式储存的。当有中间的套接字对象释放时,事件对象数组的索引与链表中第几个套接字对象就不是一一对应的关系了,所以需要移动事件对象数组中事件对象。
线程对象、线程对象中的套接字链表和事件对象数组映射关系。
Bug:
另外,书上的代码有一处地方是有Bug的。就是HandleIO函数中关于各种事件的判断。FD_CLOSE和FD_READ事件事实上是可以同时发生的,也就是客户端将数据和FIN标志一起发送的情况。而书上的代码则是将这两个事件分开了。所以当出现“客户端将数据和FIN标志一起发送”的情况发生了,服务器并不知道,就会一直持有已经关闭的套接字对象不释放,造成一定量内存浪费。
后话:
另外一点就是,虽然网上有源码,但是感觉还是自己敲出来比较实际点。虽然花了点时间,但是对自己理解这个模型很有帮助,也懂得了一点设计思路。
源代码:
#define _WIN32_WINNT 0x0400
#include<windows.h>
#include<cstdio>
#include"InitSocket.h"
CInitSock initSock ; //进入main函数前已经进行了初始化
//套接字对象结构,s和event成员必须放在最开始的位置
typedef struct _SOCKET_OBJ
{
SOCKET s ; //套接字
HANDLE event ; //与套接字相关的事件句柄
sockaddr_in addrRemote ; //客户端地址
_SOCKET_OBJ *pNext ; //下一个地址
}SOCKET_OBJ,*PSOCKET_OBJ ;
//线程对象,每一个线程负责管理最多64连接
typedef struct _THREAD_OBJ
{
HANDLE events[WSA_MAXIMUM_WAIT_EVENTS] ; //记录当前线程要等待的事件对象的句柄,事件对象表
int nSocketCount ; //记录当前线程处理的套接字的数量
PSOCKET_OBJ pSockHeader ; //当前线程处理的套接字对象的列表,pSockHeader指向表头
PSOCKET_OBJ pSockTail ; //pSockTail指向表尾
CRITICAL_SECTION cs ; //关键代码段变量,为的是同步对本结构的访问
_THREAD_OBJ *pNext ; //指向下一个THREAD_OBJ对象,为的是连成一个表
} THREAD_OBJ ,*PTHREAD_OBJ ;
//套接字对象处理函数
PSOCKET_OBJ GetSocketObj(SOCKET s) ; //申请一个套接字对象,初始化它的成员
void FreeSocketObj(PSOCKET_OBJ pSocket) ; //释放一个套接字对象
//线程对象处理函数
PTHREAD_OBJ GetThreadObj() ;
void FreeThreadObj(PTHREAD_OBJ pThread) ;
//重新建立线程对象的events数组
void RebuildArray(PTHREAD_OBJ pThread) ;
//向一个线程的套接字列表中插入一个套接字
BOOL InsertSocketObj(PTHREAD_OBJ pThead,PSOCKET_OBJ pSocket) ;
//将一个套接字对象安排给空闲的线程处理
void AssignToFreeThread(PSOCKET_OBJ pSocket) ;
//从给定线程的套接字对象列表中移除一个套接字对象
void RemoveSocketObj(PTHREAD_OBJ pThread,PSOCKET_OBJ pSocket) ;
//工作线程负责处理客户的I/O的请求
DWORD WINAPI SeverThread(LPVOID lpParam) ;
//处理真正的I/O
BOOL HandleIO(PTHREAD_OBJ pThread,PSOCKET_OBJ pSocket) ;
//FindSocketObj函数根据事件对象在events数组中的索引查找相应的套接字对象
PSOCKET_OBJ FindSocketObj(PTHREAD_OBJ pThread,int nIndex) ;
//全局变量
PTHREAD_OBJ g_pThreadList ; //指向线程对象列表表头
CRITICAL_SECTION g_cs ; //同步对全局变量的访问
//主线程维护的
LONG g_nTotalConnections ; //总共连接数量,也就是处理过的,包括已经断开的
LONG g_nCurrentConnections ; //当前连接数量
//主函数
int main(void)
{
USHORT nPort = 4567 ;
SOCKET sListen = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP) ;
sockaddr_in sin ;
sin.sin_family = AF_INET ;
sin.sin_port = htons(nPort) ;
sin.sin_addr.s_addr = INADDR_ANY ; //所有接口地址
if(SOCKET_ERROR == bind(sListen,(sockaddr*)&sin,sizeof(sin)))
{
printf("Failed bind()\n") ;
return -1 ;
}
listen(sListen,200) ;
//创建监听事件对象,并关联到监听的套接字
WSAEVENT event = WSACreateEvent() ;
WSAEventSelect(sListen,event,FD_ACCEPT|FD_CLOSE) ; //事件选择监听套接字,选择连接和退出事件
InitializeCriticalSection(&g_cs) ;
//处理客户连接请求,打印状态信息
while(TRUE)
{
int nRet = WaitForSingleObject(event,5*1000) ;
if(nRet == WAIT_FAILED)
{
printf("Failed WaitForSingleObject() \n") ;
break ;
}
else if(nRet == WSA_WAIT_TIMEOUT) //定时显示状态信息
{
printf("\n") ;
printf("Total Connections : %d\n",g_nTotalConnections) ;
printf("Current Connections : %d\n",g_nCurrentConnections) ;
}
else //有连接事件发生,监听事件被触发
{
ResetEvent(event) ;
//循环处理所有未决的连接
while(TRUE)
{
sockaddr_in si ;
int nLen = sizeof(si) ;
SOCKET sNew = accept(sListen,(sockaddr *)&si,&nLen) ; //因为是已经经过了事件选择,所以是立即返回的
if(SOCKET_ERROR == sNew)
{
break ;
}
PSOCKET_OBJ pSocket = GetSocketObj(sNew) ;
pSocket->addrRemote = si ;
WSAEventSelect(pSocket->s,pSocket->event,FD_READ|FD_CLOSE|FD_WRITE) ; //添加新的套接字对象和相应的事件
AssignToFreeThread(pSocket) ;
}
}
}
DeleteCriticalSection(&g_cs) ;
return 0 ;
}
//申请一个套接字对象,初始化它的成员
PSOCKET_OBJ GetSocketObj(SOCKET s)
{
PSOCKET_OBJ pSocket = (PSOCKET_OBJ)GlobalAlloc(GPTR,sizeof(SOCKET_OBJ)) ; //不区分全局与局部堆,推荐使用HeapAlloc
if(pSocket != NULL)
{
pSocket->s = s ;
pSocket->event = WSACreateEvent() ;
}
return pSocket ;
}
//释放套接字对象
void FreeSocketObj(PSOCKET_OBJ pSocket)
{
CloseHandle(pSocket->event) ;
if(pSocket->s != INVALID_SOCKET)
{
closesocket(pSocket->s) ;
}
GlobalFree(pSocket) ;
}
//得到一个线程对象
PTHREAD_OBJ GetThreadObj()
{
PTHREAD_OBJ pThread = (PTHREAD_OBJ)GlobalAlloc(GPTR,sizeof(THREAD_OBJ)) ;
if(pThread != NULL)
{
InitializeCriticalSection(&pThread->cs) ;
//创建一个事件对象,用于指示该该线程的句柄数组需要重建,
pThread->events[0] = WSACreateEvent() ;
//将新申请的线程对象添加到列表中
EnterCriticalSection(&g_cs) ;
pThread->pNext = g_pThreadList ; //插入到线程对象链表
g_pThreadList = pThread ;
LeaveCriticalSection(&g_cs) ;
}
return pThread ;
}
//释放一个线程对象
void FreeThreadObj(PTHREAD_OBJ pThread)
{
//在线程对象列表中查找pThread所指的对象,如果找到就从中移除
EnterCriticalSection(&g_cs) ;
PTHREAD_OBJ p = g_pThreadList ;
if(p == pThread) //如果要删除的是头结点
{
g_pThreadList = p->pNext ;
}
else
{
while(p != NULL && p->pNext != pThread)
{
p = p->pNext ;
}
if(p != NULL)
{
//此时p是pThread的前一个,即"p->pNext == pThread"
p->pNext = pThread->pNext ;
}
}
LeaveCriticalSection(&g_cs);
//释放资源
CloseHandle(pThread->events[0]) ;
DeleteCriticalSection(&pThread->cs) ;
GlobalFree(pThread) ;
}
//重新建立线程对象的events数组.因为事件对象是用数组来存储的,而套接字对象是用链表来存储的,当有套接字对象关闭的时候会出现映射不一致的情况.
void RebuildArray(PTHREAD_OBJ pThread)
{
EnterCriticalSection(&pThread->cs) ; //为了同步监听连接线程对本线程对象的访问
PSOCKET_OBJ pSocket = pThread->pSockHeader ;
int n = 1 ; //从第1个开始写,第0个用于指示需要重建了
while(pSocket != NULL)
{
pThread->events[n] = pSocket->event ;
pSocket = pSocket->pNext ;
n++ ;
}
LeaveCriticalSection(&pThread->cs) ;
}
//向一个线程的套接字列表中插入一个套接字
BOOL InsertSocketObj(PTHREAD_OBJ pThread,PSOCKET_OBJ pSocket)
{
BOOL bRet = FALSE ;
EnterCriticalSection(&pThread->cs) ;
if(pThread->nSocketCount < WSA_MAXIMUM_WAIT_EVENTS-1) //一个线程最多能够等待的事件对象个数
{
if(NULL == pThread->pSockHeader) //线程的套接字列表为空的
{
pThread->pSockHeader = pThread->pSockTail = pSocket ;
}
else
{
pThread->pSockTail->pNext = pSocket ;
pThread->pSockTail = pSocket ;
}
pThread->nSocketCount++ ;
bRet = TRUE ;
}
LeaveCriticalSection(&pThread->cs) ;
//插入成功,说明成功处理了客户的连接请求
if(bRet)
{
InterlockedIncrement(&g_nTotalConnections) ; //原子操作
InterlockedIncrement(&g_nCurrentConnections) ;
}
return bRet ;
}
//将一个套接字对象安排给空闲的线程处理
void AssignToFreeThread(PSOCKET_OBJ pSocket)
{
pSocket->pNext = NULL ;
EnterCriticalSection(&g_cs) ;
PTHREAD_OBJ pThread = g_pThreadList ; //线程对象链表头
//试图插入到现存线程
while(pThread != NULL)
{
if(InsertSocketObj(pThread,pSocket))
{
break ;
}
pThread = pThread->pNext ;
}
//没有空闲线程,为这个套接字创建新的线程
if(NULL == pThread)
{
pThread = GetThreadObj() ;
InsertSocketObj(pThread,pSocket) ;
CreateThread(NULL,0,SeverThread,pThread,0,NULL) ; //用beginthreadex,一个线程对象对应一个线程,线程参数为线程对应的线程对象
}
LeaveCriticalSection(&g_cs) ;
//指示新线程重建句柄数组
WSASetEvent(pThread->events[0]) ;
}
//从给定线程的套接字对象列表中移除一个套接字对象
void RemoveSocketObj(PTHREAD_OBJ pThread,PSOCKET_OBJ pSocket)
{
EnterCriticalSection(&pThread->cs) ;
//在套接字对象列表中查找指定的套接字对象,找到后将之移除
PSOCKET_OBJ pTest = pThread->pSockHeader ;
if(pTest == pSocket) //删除的是头结点
{
if(pThread->pSockHeader == pThread->pSockTail) //而且只有一个结点
{
pThread->pSockTail = pThread->pSockHeader = pTest->pNext ;
}
else //不只有一个头结点
{
pThread->pSockHeader = pTest->pNext ;
}
}
else
{
while(pTest != NULL && pTest->pNext != pSocket)
{
pTest = pTest->pNext ;
}
if(pTest != NULL)
{
if(pThread->pSockTail == pSocket) //删除的是尾结点
{
pThread->pSockTail = pTest ;
}
pTest->pNext = pSocket->pNext ;
}
}
pThread->nSocketCount-- ;
LeaveCriticalSection(&pThread->cs) ;
WSASetEvent(pThread->events[0]) ; //指示线程重建句柄数组
InterlockedDecrement(&g_nCurrentConnections) ; //说明一个连接中断
}
//工作线程负责处理客户的I/O的请求
DWORD WINAPI SeverThread(LPVOID lpParam)
{
//取得本线程的对象的指针
PTHREAD_OBJ pThread = (PTHREAD_OBJ)lpParam ;
while(TRUE)
{
//等待网络事件
int nIndex = WSAWaitForMultipleEvents(pThread->nSocketCount+1,pThread->events,FALSE,WSA_INFINITE,FALSE) ;
nIndex = nIndex - WSA_WAIT_EVENT_0 ; //这里是会触发索引一定是最低的吗?
//因为有可能已经有多个触发,所以需要查看nIndex后面是否已触发的事件对象
for(int i = nIndex ; i < pThread->nSocketCount+1 ; ++i)
{
nIndex = WSAWaitForMultipleEvents(1,&pThread->events[i],TRUE,1000,FALSE) ; //只等待单独一个事件对象
if(nIndex == WSA_WAIT_FAILED || nIndex == WSA_WAIT_TIMEOUT)
{
continue ;
}
else //有事件触发了
{
if(0 == i) //events[0] 已触发,重建数组
{
RebuildArray(pThread) ;
//如果没有客户I/O要处理了,则本线程退出
if(0 == pThread->nSocketCount)
{
FreeThreadObj(pThread) ;
return 0 ;
}
WSAResetEvent(pThread->events[0]) ; //重置事件对应
}
else //处理网络事件
{
//查找对应的套接字对象指针,调用HandleIO处理网络事件.用的是索引i进行查找,所以前面就需要重建数组了.
PSOCKET_OBJ pSocket = (PSOCKET_OBJ)FindSocketObj(pThread,i) ;
if(pSocket != NULL)
{
if(!HandleIO(pThread,pSocket)) //当HandleIO返回FALSE时,代表套接字关闭,或者有错误发生,虽然重建数组
{
RebuildArray(pThread) ;
}
}
else
{
printf("Unable to find socket object\n") ;
}
}
}
}
}
return 0 ;
}
//FindSocketObj函数根据事件对象在events数组中的索引查找相应的套接字对象
PSOCKET_OBJ FindSocketObj(PTHREAD_OBJ pThread,int nIndex) //nIndex 从1开始
{
//在套接字列表中查找
PSOCKET_OBJ pSocket = pThread->pSockHeader ;
while(--nIndex) //倒序查找的原因
{
if(NULL == pSocket)
{
return NULL ;
}
pSocket = pSocket->pNext ;
}
return pSocket ;
}
//处理真正的I/O
BOOL HandleIO(PTHREAD_OBJ pThread,PSOCKET_OBJ pSocket)
{
//获取具体发生的网络事件
WSANETWORKEVENTS event ;
WSAEnumNetworkEvents(pSocket->s,pSocket->event,&event) ; //将套接字绑定到某一个网络事件上面
do
{
if(event.lNetworkEvents & FD_READ)
{
if(event.iErrorCode[FD_READ_BIT] == 0)
{
char szText[256] ;
int nRecv = recv(pSocket->s,szText,sizeof(szText),0) ; //sizeof?
if(nRecv >0)
{
szText[nRecv] = '\0' ;
printf("接收到数据:%s\n",szText) ;
}
}
else
{
break ;
}
}
if(event.lNetworkEvents & FD_CLOSE) //这里要减少连接数才行,不能/*else if*/,书上就是这样做,导致不能关闭连接
{
if(event.iErrorCode[FD_CLOSE_BIT] == 0)
{
printf("关闭一个连接\n") ;
break ;
}
else
{
printf("关闭连接时出错\n") ;
break ;
}
}
if(event.lNetworkEvents & FD_WRITE) //不能用else if
{
if(event.iErrorCode[FD_WRITE_BIT] == 0)
{
}
else
{
break ;
}
}
return TRUE ;
}while(FALSE) ;
//如果套接字关闭,或者有错误发生,程序都会转到这里执行
RemoveSocketObj(pThread,pSocket) ;
FreeSocketObj(pSocket) ;
return FALSE ;
}
总结:
WSAEventSelect模型虽然感觉相对于select模型和WSAAsyncSelect模型来说,有一定的伸缩性,但是对于成千上万的连接来说,基于WSAEventSelect模型的服务器可能会创建过多的线程,而随之而来的便是工作线程之间上下文切换带来的巨大花销。另外,如果客户端在连接之后的一段较短的时间会断开连接,也会造成服务器短时间内创建、销毁大量的线程,这里也应该会带来一定开销。