重叠I/O之完成例程

==> 学习汇总(持续更新)
==> 从零搭建后端基础设施系列(一)-- 背景介绍


这个模型中有两个函数可以交换着用,那就是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()
}

如图:
重叠I/O之完成例程_第1张图片

三、创建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;
	}
}

如图:
重叠I/O之完成例程_第2张图片

四、创建完成例程函数(回调函数)

其实这个就相当于是写一个自定义的回调函数给系统调用。
该函数的参数一定,不能更改。名字随便起。
如下:

void CALLBACK CompeletRoutine(DWORD dwError, DWORD dwBytesTransferred, 
LPWSAOVERLAPPED Overlapped, DWORD dwFlags)
{
	......
}

其主要功能如下所描述
伪代码:

get the client information;
...
error handle;
...
data handle;
...
post a WSARecv

如图:
重叠I/O之完成例程_第3张图片

SleepEx版本的基本差不多,就是把事件去掉,改为用一个变量判断有无new client以及用SleepEx等待完成例程的操作。
如图:
重叠I/O之完成例程_第4张图片

重叠I/O之完成例程_第5张图片

示例代码1

#include 
#include 
#include 
#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下一篇讲。

你可能感兴趣的:(Socket编程)