重叠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()
}

如图:

三、创建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下一篇讲。

重叠IO模型之OverLapped完成例程模型WSACompletionRoutineServer VS2010 基础入门 客户端与服务器端 客户端向服务器端发送数据 可接收多个客户端 #include #include #pragma comment (lib, "ws2_32.lib") #define PORT 8088 #define MSG_SIZE 1024 SOCKET g_sConnect; bool g_bConnect = false; typedef struct { WSAOVERLAPPED overLap; WSABUF wsaBuf; char chMsg[MSG_SIZE]; DWORD nRecvNum; DWORD nFlags; SOCKET sClient; }PRE_IO_OPERATION_DATA, *LP_PER_IO_OPERATION_DATA; void CALLBACK CompletionRoutine(DWORD dwError, DWORD dwTrans, LPWSAOVERLAPPED lpOverlap, DWORD nFlags); DWORD WINAPI workThread(LPVOID lp) { LP_PER_IO_OPERATION_DATA lpData; while(TRUE) { if (g_bConnect) // 有新的连接 { // 为lpData分配空间并初始化 lpData = (LP_PER_IO_OPERATION_DATA)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PRE_IO_OPERATION_DATA)); lpData->wsaBuf.len = MSG_SIZE; lpData->wsaBuf.buf = lpData->chMsg; lpData->sClient = g_sConnect; WSARecv(lpData->sClient, &lpData->wsaBuf, 1, &lpData->nRecvNum, &lpData->nFlags, &lpData->overLap, CompletionRoutine); g_bConnect = false; // 处理完毕 } SleepEx(1000, TRUE); } return 0; } // 系统在WSARecv收到信息后,自动调用此函数,并传入参数--回调函数 void CALLBACK CompletionRoutine(DWORD dwError, DWORD dwTrans, LPWSAOVERLAPPED lpOverlap, DWORD nFlags) { LP_PER_IO_OPERATION_DATA lpData = (LP_PER_IO_OPERATION_DATA)lpOverlap; if (0 != dwError) // 接收失败 { printf("Socket %d Close!\n", lpData->sClient); closesocket(lpData->sClient); HeapFree(GetProcessHeap(), 0, lpData); } else // 接收成功 { lpData->chMsg[dwTrans] = '\0'; send(lpData->sClient, lpData->chMsg, dwTrans, 0); printf("Socket:%d MSG: %s \n", lpData->sClient, lpData->chMsg); memset(&lpData->overLap, 0, sizeof(WSAOVERLAPPED)); lpData->wsaBuf.len = MSG_SIZE; lpData->wsaBuf.buf = lpData->chMsg; // 继续接收来自客户端的数据 实现 WSARecv与CompletionRoutine循环 WSARecv(lpData->sClient, &lpData->wsaBuf,1, &lpData->nRecvNum, &lpData->nFlags, &lpData->overLap, CompletionRoutine); } } int main() { WSADATA wsaData; WSAStartup(0x0202, &wsaData); SOCKET sListen; sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in addrListen; addrListen.sin_family = AF_INET; addrListen.sin_port = htons(PORT); addrListen.sin_addr.S_un.S_addr = htonl(ADDR_ANY); int nErrorCode = 0; nErrorCode = bind(sListen, (sockaddr*)&addrListen, sizeof(sockaddr)); nErrorCode = listen(sListen, 5); DWORD nThreadID; CreateThread(NULL, 0, workThread, NULL, 0, &nThreadID); sockaddr_in addrConnect; int nAddrLen = sizeof(sockaddr_in); printf("Server Started!\n"); while(TRUE) { g_sConnect= accept(sListen, (sockaddr*)&addrConnect, &nAddrLen); if (INVALID_SOCKET == g_sConnect) { return -1; } g_bConnect = true; // 连接成功 printf("Accept Client :%s -- PORT:%d\n", inet_ntoa(addrConnect.sin_addr), htons(addrConnect.sin_port)); } return 0; }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值