这个模型中有两个函数可以交换着用,那就是WSAWaitForMultipleEvents()和SleepEx()函数,前者需要一个事件驱动,后者则不需要。是不是听起来后者比较厉害,当然不是,简单肯定是拿某种性能换来的,那就是当多client同时发出请求的时候,SleepEx如果等候时间设置成比较大的话,会造成client连接不上的现象。具体可以运行一下示例代码体会一下。
示例代码1(WSAWaitForMultipleEvents()版本)
示例代码2(SleepEx()版本)
使用该模型的步骤如下:
一、打开服务器(和事件通知那里一样)
二、创建ThreadAccept线程
这里要先创建一个事件对象,然后把该事件对象作为参数传入ThreadBind线程中。之后就不断的等待client的请求,一有新的请求立即用WSASetEvent函数将该事件对象状态设置为有信号。
伪代码如下:
create a event object; //WSACreateEvent()
...
call ThreadAccept and use this event object as param;
...
while(1)
{
accept new client request;
...
set the event has single; //WSASetEvent()
}
如图:
三、创建ThreadBind线程
这个主要用来为new client绑定一个完成例程,然后再投递一个WSARecv。
伪代码如下:
while(1)
{
while(1)
{
Wait for accept() to signal an event and also process CompletionRoutine ;//WSAWaitForMultipleEvents()
...
reset the event object;//WSAResetEvent()
...
alloc a global mem for save client information;//GlobalAlloc()
...
post a WSARecv;
}
}
如图:
四、创建完成例程函数(回调函数)
其实这个就相当于是写一个自定义的回调函数给系统调用。
该函数的参数一定,不能更改。名字随便起。
如下:
void CALLBACK CompeletRoutine(DWORD dwError, DWORD dwBytesTransferred,
LPWSAOVERLAPPED Overlapped, DWORD dwFlags)
{
......
}
其主要功能如下所描述
伪代码:
get the client information;
...
error handle;
...
data handle;
...
post a WSARecv
如图:
SleepEx版本的基本差不多,就是把事件去掉,改为用一个变量判断有无new client以及用SleepEx等待完成例程的操作。
如图:
示例代码1
#include <WinSock2.h>
#include <process.h>
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")
#define PORT 18000
#define MAXBUF 128
//自定义一个存放socket信息的结构体,用于完成例程中对OVERLAPPED的转换
typedef struct _SOCKET_INFORMATION {
OVERLAPPED Overlapped; //这个字段一定要放在第一个,否则转换的时候,数据的赋值会出错
SOCKET Socket; //后面的字段顺序可打乱并且不限制字段数,也就是说你还可以多定义几个字段
CHAR Buffer[MAXBUF];
WSABUF wsaBuf;
} SOCKET_INFORMATION, *LPSOCKET_INFORMATION;
SOCKET g_sClient; //不断新加进来的client
//打开服务器
BOOL OpenServer(SOCKET* sServer)
{
BOOL bRet = FALSE;
WSADATA wsaData = { 0 };
SOCKADDR_IN addrServer = { 0 };
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(PORT);
addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
do
{
if (!WSAStartup(MAKEWORD(2, 2), &wsaData))
{
if (LOBYTE(wsaData.wVersion) == 2 || HIBYTE(wsaData.wVersion) == 2)
{
//在套接字上使用重叠I/O模型,必须使用WSA_FLAG_OVERLAPPED标志创建套接字
//g_sServer = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
*sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (*sServer != INVALID_SOCKET)
{
if (SOCKET_ERROR != bind(*sServer, (SOCKADDR*)&addrServer, sizeof(addrServer)))
{
if (SOCKET_ERROR != listen(*sServer, SOMAXCONN))
{
bRet = TRUE;
break;
}
closesocket(*sServer);
}
closesocket(*sServer);
}
}
}
} while (FALSE);
return bRet;
}
//完成例程
void CALLBACK CompeletRoutine(DWORD dwError, DWORD dwBytesTransferred, LPWSAOVERLAPPED Overlapped, DWORD dwFlags)
{
DWORD dwSendBytes, dwRecvBytes;
DWORD dwFlag;
//强制转换为我们自定义的结构,这里就解释了为什么第一个字段要是OVERLAPPED
//因为转换后首地址肯定会相同,读取的数据一定会是Overlapped的数据
//所以要先把Overlapped的数据保存下来,接下来内存中的数据再由系统分配到各个字段中
LPSOCKET_INFORMATION pSi = (LPSOCKET_INFORMATION)Overlapped;
if (dwError != 0) //错误显示
printf("I/O operation failed with error %d\n", dwError);
if (dwBytesTransferred == 0)
printf("Closing socket %d\n\n", pSi->Socket);
if (dwError != 0 || dwBytesTransferred == 0) //错误处理
{
closesocket(pSi->Socket);
GlobalFree(pSi);
return;
}
//如果已经发送完成了,接着投递下一个WSARecv
printf("Recv%d:%s\n", pSi->Socket, pSi->wsaBuf.buf);
dwFlag = 0;
ZeroMemory(&(pSi->Overlapped), sizeof(WSAOVERLAPPED));
pSi->wsaBuf.len = MAXBUF;
pSi->wsaBuf.buf = pSi->Buffer;
if (WSARecv(pSi->Socket, &(pSi->wsaBuf), 1, &dwRecvBytes, &dwFlag, &(pSi->Overlapped), CompeletRoutine) == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("WSARecv() failed with error %d\n", WSAGetLastError());
return;
}
}
}
//把client和完成例程绑定起来
unsigned int __stdcall ThreadBind(void* lparam)
{
DWORD dwFlags;
LPSOCKET_INFORMATION pSi;
DWORD dwIndex;
DWORD dwRecvBytes;
WSAEVENT eventArry[1];
eventArry[0] = (WSAEVENT)lparam;
while (1)
{
//等待一个完成例程返回
while (TRUE)
{
dwIndex = WSAWaitForMultipleEvents(1, eventArry, FALSE, WSA_INFINITE, TRUE);
if (dwIndex == WSA_WAIT_FAILED)
{
printf("WSAWaitForMultipleEvents() failed with error %d\n", WSAGetLastError());
return FALSE;
}
if (dwIndex != WAIT_IO_COMPLETION)
break;
}
//重设事件
WSAResetEvent(eventArry[0]);
//为SOCKET_INFORMATION分配一个全局内存空间,相当于全局变量了
//这里为什么要分配全局的呢?因为我们要在完成例程中引用socket的数据
if ((pSi = (LPSOCKET_INFORMATION)GlobalAlloc(GPTR, sizeof(SOCKET_INFORMATION))) == NULL)
{
printf("GlobalAlloc() failed with error %d\n", GetLastError());
return 1;
}
//填充各个字段
pSi->Socket = g_sClient;
ZeroMemory(&(pSi->Overlapped), sizeof(WSAOVERLAPPED));
pSi->wsaBuf.len = MAXBUF;
pSi->wsaBuf.buf = pSi->Buffer;
dwFlags = 0;
//投递一个WSARecv
if (WSARecv(pSi->Socket, &(pSi->wsaBuf), 1, &dwRecvBytes, &dwFlags,
&(pSi->Overlapped), CompeletRoutine) == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("WSARecv() failed with error %d\n", WSAGetLastError());
return 1;
}
}
printf("Socket %d got connected...\n", g_sClient);
}
return 0;
}
//接受client请求线程
unsigned int __stdcall ThreadAccept(void* lparam)
{
SOCKET sServer = *(SOCKET*)lparam;
WSAEVENT event = WSACreateEvent();
_beginthreadex(NULL, 0, ThreadBind, event, 0, NULL);
while (TRUE)
{
g_sClient = accept(sServer, NULL, NULL);
if (g_sClient != INVALID_SOCKET)
WSASetEvent(event);
}
return 0;
}
int main(int argc, char **argv)
{
SOCKET sServer = INVALID_SOCKET;
if (OpenServer(&sServer))
_beginthreadex(NULL, 0, ThreadAccept, &sServer, 0, NULL);
Sleep(10000000);
return 0;
}
示例代码2
因为只是ThreadAccept和ThreadBind有变动,所以只贴出这两段代码
//接受client请求线程
unsigned int __stdcall ThreadAccept(void* lparam)
{
SOCKET sServer = *(SOCKET*)lparam;
while (TRUE)
{
g_sClient = accept(sServer, NULL, NULL);
if (g_sClient != INVALID_SOCKET)
g_bNewClient = TRUE;
}
return 0;
}
//把client和完成例程绑定起来
unsigned int __stdcall ThreadBind(void* lparam)
{
DWORD dwFlags;
LPSOCKET_INFORMATION pSi;
DWORD dwIndex;
DWORD dwRecvBytes;
while (1)
{
//等待一个完成例程返回
while (TRUE)
{
dwIndex = SleepEx(10, TRUE); //这里等待10ms,如果等待时间越大,越容易出现内存读取错误
if (dwIndex == WSA_WAIT_FAILED) //如果用事件通知,则不用考虑这个,不会冲突的。
{
printf("SleepEx() failed with error %d\n", WSAGetLastError());
return 1;
}
if (dwIndex != WAIT_IO_COMPLETION)
break; //有新的client加入,跳出循环,继续为新的client绑定例程
}
if (g_bNewClient)//如果有new client 才为它绑定一个完成例程
{
//为SOCKET_INFORMATION分配一个全局内存空间,相当于全局变量了
//这里为什么要分配全局的呢?因为我们要在完成例程中引用socket的数据
if ((pSi = (LPSOCKET_INFORMATION)GlobalAlloc(GPTR, sizeof(SOCKET_INFORMATION))) == NULL)
{
printf("GlobalAlloc() failed with error %d\n", GetLastError());
return 1;
}
//填充各个字段
pSi->Socket = g_sClient;
ZeroMemory(&(pSi->Overlapped), sizeof(WSAOVERLAPPED));
pSi->wsaBuf.len = MAXBUF;
pSi->wsaBuf.buf = pSi->Buffer;
dwFlags = 0;
//投递一个WSARecv
if (WSARecv(pSi->Socket, &(pSi->wsaBuf), 1, &dwRecvBytes, &dwFlags,
&(pSi->Overlapped), CompeletRoutine) == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("WSARecv() failed with error %d\n", WSAGetLastError());
return 1;
}
}
printf("Socket %d got connected...\n", g_sClient);
g_bNewClient = FALSE;
}
}
return 0;
}
关于完成例程如何同时投递WSARecv和WSASend下一篇讲。