在掌握了socket相关的一些函数后,套接字编程还是比较简单的,日常工作中碰到很多的问题就是客户端/服务器模型中,如何让服务端在同一时间高效的处理多个客户端的连接,我们的处理办法可能会是在服务端不停的监听客户端的请求,有新的请求到达时,开辟一个新的线程去和该客户端进行后续处理,但是这样针对每一个客户端都需要去开辟一个新的线程,效率必定底下。
其实,socket编程提供了很多的模型来处理这种情形,我们只要按照模型去实现我们的代码就可以解决这个问题。主要有select模型和重叠I/o模型,以及完成端口模型。这次,我们主要介绍下select模型,该模型又分为普通select模型,wsaasyncselect模型,wsaeventselect模型。我们将通过样例代码的方式逐一介绍。
一、select模型
使用该模型时,在服务端我们可以开辟两个线程,一个线程用来监听客户端的连接
请求,另一个用来处理客户端的请求。主要用到的函数为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模型就是这样一个解决了普通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] = {
'