原文出处:http://www.cnblogs.com/RascallySnake/archive/2013/07/11/3185071.html
日常工作中碰到很多的问题就是客户端/服务器模型中,如何让服务端在同一时间高效的处理多个客户端的连接,我们的处理办法可能会是在服务端不停的监听客户端的请求,有新的请求到达时,开辟一个新的线程去和该客户端进行后续处理,但是这样针对每一个客户端都需要去开辟一个新的线程,效率必定底下。其实,socket编程提供了很多的模型来处理这种情形,我们只要按照模型去实现我们的代码就可以解决这个问题。Windows操作系统提供了选择(Select)、异步选择(WSAAsyncSelect)、事件选择(WSAEventSelect)、重叠I/O(Overlapped I/O)和完成端口(Completion Port)共五种I/O模型。这里通过例子来说明三种选择模型。
老陈有一个在外地工作的女儿,不能经常回来,老陈和她通过信件联系。他们的信会被邮递员投递到他们的信箱里。
这和Socket模型非常类似。
1、selected模型
老陈非常想看到女儿的信。以至于他每隔10分钟就下楼检查信箱,看是否有女儿的信,在这种情况下,"下楼检查信箱"然后回到楼上耽误了老陈太多的时间,以至于老陈无法做其他工作。
select模型和老陈的这种情况非常相似:周而复始地去检查,如果有数据,接收/发送.......
select的函数原型:
int select
(
int nfds, //Winsock中此参数无意义
fd_set* readfds, //进行可读检测的Socket
fd_set* writefds, //进行可写检测的Socket
fd_set* exceptfds, //进行异常检测的Socket
const struct timeval* timeout //非阻塞模式中设置最大等待时间
)
参数解释
参数1 nfds:这个参数是一个被忽略的参数,我们在程序中传入0即可,其目的是与伯克利套接字兼容。
参数2 readfds:可读性监视集合,可读性指有连接到来、有数据到来、连接已关闭、重置或终止。
参数3 writefds:可写性监视集合,可写性指数据可以发送、连接可以成功。
参数4 exceptfds:例外性监视集合,例外性指连接会失败、外带数据到来
参数4 timeout:该参数指定select会等待的时间,者结构很简单,要是有兴趣可以看看msdn
注意:select参数中的三个监视集合至少有一个不能为空,任何其它两个都可为空
fd_set的结构,这个结构是用来装SOCKET的,把要监视SOCKET传给这个结构,然后同select函数进行监视。
struct fd_set
{
u_int fd_count; // how many are SET?
SOCKET fd_array[FD_SETSIZE]; // an array of SOCKETs
} ;
和select模型紧密结合的四个宏:
FD_CLR( s,*set) 从队列set删除句柄s。
FD_ISSET( s, *set) 检查句柄s是否存在与队列set中。
FD_SET( s,*set )把句柄s添加到队列set中。
FD_ZERO( *set ) 把set队列初始化成空队列。
select选择模式倚赖于select函数,其思想就是让select函数对传入fd_set进行监视(fd_set中装有你的SOCKET句柄),如果没什么事发生select就将fd_set中的SOCKET清除。
使用该模型时,在服务端我们可以开辟两个线程,一个线程用来监听客户端的连接
请求,另一个用来处理客户端的请求。主要用到的函数为select函数。如:
全局变量:
fd_set g_fdClientSock;
线程1处理函数:
SOCKET listenSock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(7788);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
int nRet = bind( listenSock, (sockaddr*)&sin, (int)(sizeof(sin)));
if ( nRet == SOCKET_ERROR )
{
DWORD errCode = GetLastError();
return;
}
listen( listenSock, 5);
int clientNum = 0;
sockaddr_in clientAddr;
int nameLen = sizeof( clientAddr );
while( clientNum < FD_SETSIZE )
{
SOCKET clientSock = accept( listenSock, (sockaddr*)&clientAddr, &nameLen );
FD_SET( clientSock, &g_fdClientSock);
clientNum++;
}
线程2处理函数
fd_set fdRead;
FD_ZERO( &fdRead );
int nRet = 0;
char* recvBuffer =(char*)malloc( sizeof(char) * 1024 );
if ( recvBuffer == NULL )
{
return;
}
memset( recvBuffer, 0, sizeof(char) * 1024 );
while ( true )
{
fdRead = g_fdClientSock;
nRet = select( 0, &fdRead, NULL, NULL, NULL );
if ( nRet != SOCKET_ERROR )
{
for ( int i = 0; i < g_fdClientSock.fd_count; i++ )
{
if ( FD_ISSET(g_fdClientSock.fd_array[i],&fdRead) )
{
memset( recvBuffer, 0, sizeof(char) * 1024 );
nRet = recv( g_fdClientSock.fd_array[i], recvBuffer, 1024, 0);
if ( nRet == SOCKET_ERROR )
{
closesocket( g_fdClientSock.fd_array[i] );
FD_CLR( g_fdClientSock.fd_array[i], &g_fdClientSock );
}
else
{
//todo:后续处理
}
}
}
}
}
if ( recvBuffer != NULL )
{
free( recvBuffer );
}
该模型有个最大的缺点就是,它需要一个死循环不停的去遍历所有的客户端套接字集合,询问是否有数据到来,这样,如果连接的客户端很多,势必会影响处理客户端请求的效率,但它的优点就是解决了每一个客户端都去开辟新的线程与其通信的问题。如果有一个模型,可以不用去轮询客户端套接字集合,而是等待系统通知,当有客户端数据到来时,系统自动的通知我们的程序,这就解决了select模型带来的问题了。
二、WsaAsyncSelect模型
后来,老陈使用了微软公司的新式信箱。这种信箱非常先进,一旦信箱里有新的信件,盖茨就会给老陈打电话:喂,大爷,你有新的信件了!从此,老陈再也不必频繁上下楼检查信箱了,微软提供的WSAAsyncSelect模型就是这个意思。盖茨相当于windows程序的窗口。
WsaAsyncSelect模型就是这样一个解决了普通select模型问题的socket编程模型。它是在有客户端数据到来时,系统发送消息给我们的程序,我们的程序只要定义好消息的处理方法就可以了,用到的函数只要是WSAAsyncSelect,如:
首先,我们定义一个Windows消息,告诉系统,当有客户端数据到来时,发送该消息给我们。
#define UM_SOCK_ASYNCRECVMSG WM_USER + 1
在我们的处理函数中可以如下监听客户端的连接:
SOCKET listenSock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(7788);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
int nRet = bind( listenSock, (sockaddr*)&sin, (int)(sizeof(sin)));
if ( nRet == SOCKET_ERROR )
{
DWORD errCode = GetLastError();
return;
}
listen( listenSock, 5);
int clientNum = 0;
sockaddr_in clientAddr;
int nameLen = sizeof( clientAddr );
while( clientNum < FD_SETSIZE )
{
SOCKET clientSock = accept( listenSock, (sockaddr*)&clientAddr, &nameLen );
//hWnd为接收系统发送的消息的窗口句柄
WSAAsyncSelect( clientSock, hWnd, UM_SOCK_ASYNCRECVMSG, FD_READ | FD_CLOSE );
clientNum++;
}
接下来,我们需要在我们的窗口添加对UM_SOCK_ASYNCRECVMSG消息的处理函数,在该函数中真正接收客户端发送过来的数据,在这个消息处理函数中的wparam参数表示的是客户端套接字,lparam参数表示的是发生的网络事件如:
SOCKET clientSock = (SOCKET)wParam;
if ( WSAGETSELECTERROR( lParam ) )
{
closesocket( clientSock );
return;
}
switch ( WSAGETSELECTEVENT( lParam ) )
{
case FD_READ:
{
char recvBuffer[1024] = {'\0'};
int nRet = recv( clientSock, recvBuffer, 1024, 0 );
if ( nRet > 0 )
{
szRecvMsg.AppendFormat(_T("Client %d Say:%s\r\n"), clientSock, recvBuffer );
}
else
{
//client disconnect
szRecvMsg.AppendFormat(_T("Client %d Disconnect!\r\n"), clientSock );
}
}
break;
case FD_CLOSE:
{
closesocket( clientSock );
szRecvMsg.AppendFormat(_T("Client %d Disconnect!\r\n"), clientSock );
}
break;
}
可以看到WsaAsyncSelect模型是非常简单的模型,它解决了普通select模型的问题,但是它最大的缺点就是它只能用在windows程序上,因为它需要一个接收系统消息的窗口句柄,那么有没有一个模型既可以解决select模型的问题,又不限定只能是windows程序才能用呢?下面我们来看看WsaEventSelect模型。
三、WsaEventSelect模型
后来,微软的信箱非常畅销,购买微软信箱的人以百万计数......以至于盖茨每天24小时给客户打电话,累得腰酸背痛,微软改进了他们的信箱:在客户的家中添加一个附加装置,这个装置会监视客户的信箱,每当新的信件来临,此装置会发出"新信件到达"声,提醒老陈去收信。终于没有盖茨什么事了。对于WsaEventSelect模型来说,就是不需要窗口的存在了。
WsaEventSelect模型是一个不用主动去轮询所有客户端套接字是否有数据到来的模型,它也是在客户端有数据到来时,系统发送通知给我们的程序,但是,它不是发送消息,而是通过事件的方式来通知我们的程序,这就解决了WsaAsyncSelect模型只能用在windows程序的问题。
该模型的实现,我们也可以开辟两个线程来进行处理,一个用来接收客户端的连接请求,一个用来与客户端进行通信,用到的主要函数有WSAEventSelect,WSAWaitForMultipleEvents,WSAEnumNetworkEvents实现方式如下:
首先定义三个全局数组
SOCKET g_SockArray[MAX_NUM_SOCKET];//存放客户端套接字
WSAEVENT g_EventArray[MAX_NUM_SOCKET];//存放该客户端有数据到来时,触发的事件
UINT32 g_totalEvent = 0;//记录客户端的连接数
线程1处理函数
SOCKET listenSock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(7788);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
int nRet = bind( listenSock, (sockaddr*)&sin, (int)(sizeof(sin)));
if ( nRet == SOCKET_ERROR )
{
DWORD errCode = GetLastError();
return;
}
listen( listenSock, 5);
sockaddr_in clientAddr;
int nameLen = sizeof( clientAddr );
while( g_totalEvent < MAX_NUM_SOCKET )
{
SOCKET clientSock = accept( listenSock, (sockaddr*)&clientAddr, &nameLen );
if ( clientSock == INVALID_SOCKET )
{
continue;
}
g_SockArray[g_totalEvent] = clientSock;
if( (g_EventArray[g_totalEvent] = WSACreateEvent()) == WSA_INVALID_EVENT )
{
continue;
}
WSAEventSelect( clientSock, g_EventArray[g_totalEvent],FD_READ | FD_CLOSE );
g_totalEvent++;
}
线程2处理函数
int nIndex = 0;
char* recvBuffer =(char*)malloc( sizeof(char) * 1024 );
if ( recvBuffer == NULL )
{
return;
}
memset( recvBuffer, 0, sizeof(char) * 1024 );
while( true )
{
nIndex = WSAWaitForMultipleEvents( g_totalEvent, g_EventArray, FALSE, WSA_INFINITE,FALSE );
if ( nIndex == WSA_WAIT_FAILED )
{
continue;
}
else
{
WSAResetEvent( g_EventArray[ nIndex - WSA_WAIT_EVENT_0]);
SOCKET clientSock = g_SockArray[ nIndex - WSA_WAIT_EVENT_0 ];
WSANETWORKEVENTS wsaNetWorkEvent;
int nRet = WSAEnumNetworkEvents( clientSock, g_EventArray[nIndex - WSA_WAIT_EVENT_0], &wsaNetWorkEvent );
if ( SOCKET_ERROR == nRet )
{
continue;
}
else if ( wsaNetWorkEvent.lNetworkEvents & FD_READ )
{
if ( wsaNetWorkEvent.iErrorCode[FD_READ_BIT] != 0 )
{
//occur error
closesocket( clientSock );
}
else
{
memset( recvBuffer, 0, sizeof(char) * 1024 );
nRet = recv( clientSock, recvBuffer, 1024, 0);
if ( nRet == SOCKET_ERROR )
{
closesocket( clientSock );
}
else
{
//todo:对接收到的客户端数据进行处理
}
}
}
else if( wsaNetWorkEvent.lNetworkEvents & FD_CLOSE )
{
if ( wsaNetWorkEvent.iErrorCode[FD_CLOSE_BIT] != 0 )
{
//occur error
closesocket( clientSock );
}
else
{
closesocket( clientSock );
}
}
}
}
if ( recvBuffer != NULL )
{
free( recvBuffer );
}
该模型通过一个死循环里面调用WSAWaitForMultipleEvents函数来等待客户端套接字对应的Event的到来,一旦事件通知到达,就通过该套接字去接收数据。虽然WsaEventSelect模型的实现较前两种方法复杂,但它在效率和兼容性方面是最好的。
以上三种模型虽然在效率方面有了不少的提升,但它们都存在一个问题,就是都预设了只能接收64个客户端连接,虽然我们在实现时可以不受这个限制,但是那样,它们所带来的效率提升又将打折扣,那又有没有什么模型可以解决这个问题呢?重叠I/0模型将解决这个问题。