写在前面
异步通知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);
}
}