网络编程--重叠IO模型

写在前面

异步通知IO模型一文中介绍的异步通知IO模型属于通知IO模型,类似的select实现IO复用一文中介绍的IO复用也属于通信IO模型。

何谓通知IO模型,就是IO完成给通知开发人员,某个IO操作已完成,根据通知的时机,又分为为同步通知IO模型(select实现IO复用)和异步通知IO模型(WSAEventSelect实现异步通知IO模型)。这里再回看前两种模型实现,发现都是使用的同步的IO函数send和recv,只是在该调用的时机(IO完成)调用。

而本章则会介绍以异步方式处理IO,注意区别于前两种通知模型及同步IO函数。

重叠IO

同一个线程内向多个目标传输(或从多个目标接收)数据引起的IO重叠现象称为重叠IO。

为了实现,调用的IO函数应该立即返回,而不是等待数据完全传输到输出缓冲(或读取到指定大小的数据后从输入缓冲)返回,只有这样才能无须等待,发送后续数据。

可以看出,为了完成异步IO,调用的IO函数应该以非阻塞模式工作。

创建重叠IO套接字

要IO函数以非阻塞模式工作,就需要能提供非阻塞模式工作的重叠IO套接字。可通过以下函数完成:

#include <winsock2.h>

SOCKET WSASocket(int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags);

af: 协议族信息
type:套接字的数据传输方式
protocol:2个套接字质检使用的协议信息
lpProtocolInfo:包含创建的套接字信息的WSAPROTOCOL_INFO结构体变量地址值,不需要时传递NULL
g:为扩展而预约的参数,暂未使用,传递0
dwFlags:套接字属性信息,通过该参数指定创建重叠IO套接字

可以看到前三个参数和socket函数无异,第四、第五个参数暂未使用传递NULL和0,通过向最后一个参数传递WSA_FLAG_OVERLAPPED,赋予创建出的套接字重叠IO特性。

常用创建语句如下:

SOCKET hSock = WSASOCKET(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (INVALID_SOCKET == hSock)
{
	//WSASOCKET error!
}

后续的连接过程(bind、listen、accept、connect)等和一般的套接字的连接过程相同。

下面开始介绍进行异步IO的WSASend和WSARecv函数。

执行重叠IO的WSASend函数

原型如下:

#include <winsock2.h>

int WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverLapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);

s: 具有重叠IO属性的套接字句柄
lpBuffers:WSABUF结构体变量数组的地址值,WSABUF中存有待传输数据
dwBufferCount: 第二个参数中结构体变量数组的长度
lpNumberOfBytesSent:用于保存实际发送字节数的变量地址值(因为是异步立马返回,数据未传输完成时无意义)
dwFlags:用于更改数据传输特性,如传递MSG_OOB时发送OOB模式的数据
lpOverLapped:WSAOVERLAPPED结构体变量的地址值,使用时间对象,用于确认完成数据传输
lpCompletionRoutine:传入Completion Routine函数的入口地址值,可以通过该函数确认是否完成数据传输。

返回值:成功时返回0, 失败时返回SOCKET_ERROR

下面介绍该函数参数中涉及的结构体。

WSABUF

定义如下:

typedef struct _WSABUF {
    ULONG len;     /* 待传输数据的大小 */
    __field_bcount(len) CHAR FAR *buf; /* 缓冲地址值 */
} WSABUF, FAR * LPWSABUF;

WSAOVERLAPPED

定义如下:

#define WSAOVERLAPPED           OVERLAPPED

typedef struct _OVERLAPPED 
{
    ULONG_PTR Internal;
    ULONG_PTR InternalHigh;
    union 
    {
        struct 
        {
            DWORD Offset;
            DWORD OffsetHigh;
        } DUMMYSTRUCTNAME;
        PVOID Pointer;
    } DUMMYUNIONNAME;

    HANDLE  hEvent;
} OVERLAPPED, *LPOVERLAPPED;

Internal、InternalHigh成员时进行重叠IO时操作系统内部使用的成员,而Offset、OffsetHigh用于属于具有特殊用途的成员,这里无需关注。

因此,实际开发中需要关注的只有hEvent成员,这是一个句柄,在通过事件确认IO完成的模式中用于保存套接字对应的事件对象。

需要注意的是:为了进行重叠IO,WSASend函数的lpOverlapped参数中应该传递有效的结构体变量地址值,而不是NULL。

如果向lpOverlapped传递NULL,WSASend函数的第一个参数中的句柄所指的套接字(即使具有重叠属性,也)将以阻塞模式工作。

另外,利用WSASend函数同时向多个目标传输数据时,需要分别构建传入第六个参数的WSAOVERLAPPED结构体变量,而不是使用同一个变量。这是因为,进行重叠IO的过程中,操作系统将使用WSAOVERLAPPED结构体变量。

这里给出WSASend的调用示例:




int nRecvLen = 0;

WSAEVENT event = WSACreateEvent();	//创建manual-reset模式的事件对象

WSAOVERLAPPED overlapped;
memset(&overlapped, 0, sizeof(overlapped));
overlapped.hEvent = event;

WSABUF dataBuf;
char buf[] = {"待传输数据"};
dataBuf.buf = buf;
dataBuf.len = sizeof(buf);

//因为第二个参数dataBuf中待传输的缓冲个数为1,因此这里传1即可。
WSASend(hSock, &dataBuf, 1, &recvBytes, 0, &overlapped, NULL);

补充

WSASend的第四个参数lpNumberOfBytesSent, 用于保存实际传输的数据大小。

不过这里WSASend调用后马上返回,因此这里保存的值是无意义的。

实际上,WSASend函数调用过程中,函数返回时间点和数据传输完成时间点并非总不一致。如果输出缓冲是空的,且传输的数据不大,那么函数调用后可以立即完成数据传输,此时WSASend返回0,而lpNumberOfBytesSent中保存的就是实际传输的数据大小信息。

反之,WSASend函数返回后数据还未传输完成时,将返回SOCKET_ERROR,并将WSA_IO_PENDING注册为错误代码,该代码可以通过WSAGetLastError函数得到,这时就应该通过以下函数获取实际传输的数据大小。

#include <winsock2.h>

BOOL WSAGetOverlappedResult(SOCKET s, LPWSAOVERLAPPED lpOverlapped, LPDWORD lpcbTransfer, BOOL fWait, LPDWORD lpdwFlags);

s: 进行重叠IO的套接字句柄
lpOverlapped:进行重叠IO时传递的WSAOVERLAPPED结构体变量的地址值
lpcbTransfet:用于保存实际传输的字节数的变量地址值
fWait:如果调用该函数时仍在进行IO,fWait为TRUE则等待IO完成,fWait为FALSE时将返回FALSE并跳出函数
lpdwFlags:调用WSARecv函数时,用于获取附加信息(例如OOB消息)。如果不需要可以传NULL。

通过该函数不仅可以得到实际的传输结果,还可以验证接收数据的状态,可参见后续的示例。

进行重叠IO的WSARecv函数

原型如下:

#include <winsock2.h>

int WSARecv(SCOKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);

s: 赋予重叠IO属性的套接字句柄
lpBuffers:保存接收的数据
dwBufferCount:接收数据数组的长度
lpNumberOfBytesRecvd:保存接收的数据大小
lpFlags:用于设置或读取传输特性信息(例接收OOB带外消息)
lpOverlapped:WSAOVERLAPPED结构体变量地址值
lpCompletionRoutine:CompletionRoutine函数地址值

重叠IO的IO完成确认

上面的WSASend、WSARecv函数调用完成后立即返回,这时可进行其他任务,当其他任务执行完后后如何确定之前的WSASend或WSARecv的IO已完成了呢?

重叠IO中有2种方法确认IO的完成并获取IO结果:
①利用WSASend、WSARecv函数的第六个参数lpOverlapped的hEvent成员,基于事件对象确认IO完成
②利用WSASend、WSARecv函数的第七个参数lpCompletionRoutine,基于Completion Routine回调函数确认IO完成

使用事件对象确认IO完成

完成IO时,lpOverlapped参数的hEvent成员将变为signaled状态,后面可通过WSAWaitForMultipleEvents函数等待事件触发,然后通过WSAGetOverlappedResult函数验证IO完成并获取结果。

示例如下,这里不再区分服务器端和客户端,而是发送端和接收端。

发送端

// OverlappedSend.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")


#define BUF_SIZE 1024


int _tmain(int argc, _TCHAR* argv[])
{

	if (argc != 3)
	{
		puts("argc error!");
		return -1;
	}

	WSADATA wsaData;
	if (0 != WSAStartup(MAKEWORD(2, 2), &wsaData))
	{
		puts("WSAStartup error!");
		return -1;
	}

	//使用WSASocket函数创建用于重叠IO的套接字
	SOCKET cltSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
	if (INVALID_SOCKET == cltSock)
	{
		puts("WSASocket error!");
		WSACleanup();
		return -1;
	}

	SOCKADDR_IN srvAddr;
	memset(&srvAddr, 0, sizeof(srvAddr));
	srvAddr.sin_family = PF_INET;
	srvAddr.sin_addr.s_addr = inet_addr(argv[1]);
	srvAddr.sin_port = htons(_ttoi(argv[2]));

	if (SOCKET_ERROR == connect(cltSock, (sockaddr*)&srvAddr, sizeof(srvAddr)))
	{
		puts("connect error!");
		closesocket(cltSock);
		WSACleanup();
		return -1;
	}

	char buf[] = "Network is Computer";

	//重叠IO模型准备
	WSAEVENT overlappedEvent = WSACreateEvent();
	WSAOVERLAPPED overlapped;
	memset(&overlapped, 0, sizeof(WSAOVERLAPPED));
	overlapped.hEvent = overlappedEvent;

	WSABUF dataBuf;
	dataBuf.buf = buf;
	dataBuf.len = strlen(buf) + 1;

	int nTmp = sizeof(buf);

	int nSendLen = 0, flags = 0;
	if (SOCKET_ERROR == WSASend(cltSock, &dataBuf, 1, (LPDWORD)&nSendLen, 0, &overlapped, NULL))
	{
		if (GetLastError() == WSA_IO_PENDING)
		{
			puts("Background data send.");

			//等待数据发送完成
			WSAWaitForMultipleEvents(1, &overlappedEvent, TRUE, WSA_INFINITE, FALSE);

			//获取发送数据信息
			WSAGetOverlappedResult(cltSock, &overlapped, (LPDWORD)&nSendLen, FALSE, NULL);
		}
		else
		{
			puts("WSASend error");
		}
	}

	printf("send data size: %d \n", nSendLen);
	WSACloseEvent(overlappedEvent);
	closesocket(cltSock);
	WSACleanup();

	puts("任意键继续...");
	getchar();

	return 0;
}


接收端

// OverlappedRecv.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")


#define BUF_SIZE 1024

int _tmain(int argc, _TCHAR* argv[])
{
	if (argc != 2)
	{
		puts("argc error!");
		return -1;
	}

	WSADATA wsaData;
	if (0 != WSAStartup(MAKEWORD(2, 2), &wsaData))
	{
		puts("WSAStartup error!");
		return -1;
	}

	//使用WSASocket函数创建用于重叠IO的套接字
	SOCKET srvSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
	if (INVALID_SOCKET == srvSock)
	{
		puts("WSASocket error!");
		WSACleanup();
		return -1;
	}

	SOCKADDR_IN srvAddr;
	memset(&srvAddr, 0, sizeof(srvAddr));
	srvAddr.sin_family = PF_INET;
	srvAddr.sin_addr.s_addr = htonl(ADDR_ANY);
	srvAddr.sin_port = htons(_ttoi(argv[1]));

	if (SOCKET_ERROR == bind(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr)))
	{
		puts("bind error!");
		closesocket(srvSock);
		WSACleanup();
		return -1;
	}

	if (SOCKET_ERROR == listen(srvSock, 5))
	{
		puts("listen error!");
		closesocket(srvSock);
		WSACleanup();
		return -1;
	}

	SOCKADDR_IN cltAddr;
	int nCltAddrSize = sizeof(cltAddr);
	memset(&cltAddr, 0, nCltAddrSize);
	
	SOCKET cltSock = accept(srvSock, (sockaddr*)&cltAddr, &nCltAddrSize);


	WSAEVENT overlappedEvent = WSACreateEvent();
	WSAOVERLAPPED overlapped;
	memset(&overlapped, 0, sizeof(overlapped));
	overlapped.hEvent = overlappedEvent;

	char buf[BUF_SIZE] = {};
	WSABUF dataBuf;
	dataBuf.buf = buf;
	dataBuf.len = BUF_SIZE;

	int nRecvLen = 0, flags = 0;
	if (SOCKET_ERROR == WSARecv(cltSock, &dataBuf, 1, (LPDWORD)&nRecvLen, (LPDWORD)&flags, &overlapped, NULL))
	{
		if (WSAGetLastError() == WSA_IO_PENDING)
		{
			//后台正在接收数据
			puts("Background data receive");

			//这里阻塞等待数据传输完成
			WSAWaitForMultipleEvents(1, &overlappedEvent, TRUE, WSA_INFINITE, FALSE);

			//得到接收数据信息
			WSAGetOverlappedResult(cltSock, &overlapped, (LPDWORD)&nRecvLen, FALSE, NULL);
		}
		else
		{
			puts("WSARecv error!");
		}
	}

	//如果WSARecv不返回SOCKET_ERROR,则说明数据立马接收完成
	//dataBuf.buf[nRecvLen] = 0;
	printf("Received len: %d, message: %s \n", nRecvLen, dataBuf.buf);

	closesocket(cltSock);
	closesocket(srvSock);
	WSACloseEvent(overlappedEvent);
	WSACleanup();

	puts("任意键继续...");
	getchar();

	return 0;
}


使用Completion Routine确认IO完成

除了通过事件对象验证IO是否完成,还可以通过WSASend、WSARecv函数的第七个参数指定Completion Routine(简称CR)验证IO完成情况。

“注册CR”具有以下含义:Pending的IO完成时调用此函数。

IO完成时调用注册过的函数进行事后处理,这就是Completion Routine的运作方式。

需要注意的是,如果执行重要任务时突然调用Compleiton Routine, 则有可能破坏程序的正常执行流。因此,操作系统通常会预先定义规则:只有请求IO的线程处于alertable wait状态时才能调用Completion Routine函数!

alertable wait状态是等待接收操作系统消息的线程状态。调用下列函数时进入alertable wait状态:

  • WaitForSingleObjectEx
  • WaitForMultipleObjectsEx
  • WSAWaitForMultipleEvents
  • SleepEx

调用后,上述函数将全部返回WAIT_IO_COMPLETION并开始执行后续程序。

示例如下:

基于Completion Routine的接收端

// CmplRoutinesRecv.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"

#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")


#define BUF_SIZE 1024

void CALLBACK CpmpRoutine(DWORD , DWORD , LPWSAOVERLAPPED , DWORD);

WSABUF dataBuf;
char buf[BUF_SIZE];
int nRecvLen = 0;

int _tmain(int argc, _TCHAR* argv[])
{
	if (argc != 2)
	{
		puts("argc error!");
		return -1;
	}

	WSADATA wsaData;
	if (0 != WSAStartup(MAKEWORD(2, 2), &wsaData))
	{
		puts("WSAStartup error!");
		return -1;
	}

	//使用WSASocket函数创建用于重叠IO的套接字
	SOCKET srvSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
	if (INVALID_SOCKET == srvSock)
	{
		puts("WSASocket error!");
		WSACleanup();
		return -1;
	}

	SOCKADDR_IN srvAddr;
	memset(&srvAddr, 0, sizeof(srvAddr));
	srvAddr.sin_family = PF_INET;
	srvAddr.sin_addr.s_addr = htonl(ADDR_ANY);
	srvAddr.sin_port = htons(_ttoi(argv[1]));

	if (SOCKET_ERROR == bind(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr)))
	{
		puts("bind error!");
		closesocket(srvSock);
		WSACleanup();
		return -1;
	}

	if (SOCKET_ERROR == listen(srvSock, 5))
	{
		puts("listen error!");
		closesocket(srvSock);
		WSACleanup();
		return -1;
	}

	SOCKADDR_IN cltAddr;
	int nCltAddrSize = sizeof(cltAddr);
	memset(&cltAddr, 0, nCltAddrSize);

	SOCKET cltSock = accept(srvSock, (sockaddr*)&cltAddr, &nCltAddrSize);

	//使用CompletionRoutine函数代替事件
	WSAOVERLAPPED overlapped;
	memset(&overlapped, 0, sizeof(overlapped));

	dataBuf.buf = buf;
	dataBuf.len = BUF_SIZE;

	WSAEVENT waitEvent = WSACreateEvent();

	int flags = 0;

	//接收数据,接收完成时,判断线程是否在alertable wait状态,在则调用CpmpRoutine
	if (SOCKET_ERROR == WSARecv(cltSock, &dataBuf, 1, (LPDWORD)&nRecvLen, (LPDWORD)&flags, &overlapped, CpmpRoutine))
	{
		if (GetLastError() == WSA_IO_PENDING)
		{
			//后台数据正在接收
			puts("Background data receive");

			
		}
		else
		{
			puts("WSARecv error!");
			closesocket(cltSock);
			closesocket(srvSock);
			WSACloseEvent(waitEvent);
			WSACleanup();
			return -1;
		}
	}

	//nRecvLen = szRecvBytes;
	//printf("Receive len: %d, message: %s\n", nRecvLen, dataBuf.buf);

	//进入alertable wait状态,等待数据接收完成
	int idx = WSAWaitForMultipleEvents(1, &waitEvent, FALSE, WSA_INFINITE, TRUE);

	if (idx == WSA_WAIT_IO_COMPLETION)
	{
		puts("Overlapped IO Completed!");
	}
	else
	{
		puts("WSAWaitForMultipleEvents error!");
	}

	closesocket(cltSock);
	closesocket(srvSock);
	WSACloseEvent(waitEvent);
	WSACleanup();

	puts("任意键继续...");
	getchar();

	return 0;
}

void CALLBACK CpmpRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
	if (dwError != 0)
	{
		puts("CompRoutine error!");
	}
	else
	{
		nRecvLen = szRecvBytes;
		printf("CpmpRoutine--Receive len: %d, message: %s\n", nRecvLen, dataBuf.buf);
	}
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值