理解重叠I/O模型
第21章异步处理的并非I/O,而是“通知”。本章讲解的才是以异步方式处理I/O的方法。
重叠I/O
同一线程内部向多个目标传输数据引起的I/O重叠现象称为“重叠I/O”。为了完成这项任务,调用的I/O函数应立即返回,只有这样才能发送后续数据。从结果来看,利用上述模型收发数据时,最重要的前提条件就是异步I/O。调用的I/O函数应以非阻塞模式工作。
本章讨论的重叠I/O的重点不在于I/O
前面对异步I/O和重叠I/O进行了比较,这些内容看似是本章的全部理论说明,但其实还未进入重叠I/O的正题。因为Windows中重叠I/O的重点并非I/O本身,而是如何确认I/O完成时的状态。不管是输入还是输出,只要是非阻塞模式的,就要另外确认执行结果。关于这种确认方法我们还一无所知。确认执行结果前需要经过特殊的处理过程,这就是本章要讲述的内容。
创建重叠 I/O套接字
首先要创建适用于重叠I/O的套接字,可以通过如下函数完成。
#include<winsock2.h>
SOCKET WSASocket(int af,int type,int protocol,LPWSAPROTOCOL_INFO lpProtocolInfo,GROUP g,DWORD dwFlags);
//成功时返回套接字句柄,失败时返回INVALID_SOCKET。
af //协议族信息
type //套接字数据传输方式
protocol //2个套接字之间使用的协议信息
lpProtocolInfo //包含创建的套接字信息的WSAPROTOCOL_INFO结构体变量地址值,不需要时
//传递NULL。
g //为扩展函数而预约的参数,可以使用0
dwFlags //套接字属性信息
各位对前3个参数比较熟悉,第四个和第五个参数与目前的工作无关,可以简单设置为NULL和0。可以向最后一个参数传递WSA_FLAG_OVERLAPPED,赋予创建出的套接字重叠I/O特性。可以通过如下函数调用创建出可以进行重叠I/O的非阻塞模式的套接字。
WSASocket(PF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED);
执行重叠I/O的WSASend函数
创建出具有重叠I/O属性的套接字后,接下来2个套接字(服务器端/客户端之间的)连接过程
与一般的套接字连接过程相同,但I/O数据时使用的函数不同。先介绍重叠I/O中使用的数据输出
函数。
#include <winsock2.h>
int WSASend(SOCKET S, LPWSABUF IpBuffers,DWORD dwBuffencount,LPDWORD lpNumberofBytesSent, DWORD dwFlags, LPWSAOVERLAPPED IpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
//成功时返回0,失败时返回SOCKET_ERROR。
s //套接字句柄,传递具有重叠I/O属性的套接字句柄时,以重叠I/O模型输出。
IpBuffers //WSABUF结构体变量数组的地址值,WSABUF中存有待传输数据。
dwBufferCount //第二个参数中数组的长度。
IpNumberOfBytesSent//用于保有际发送字节数的变量地址值(稍后进行说明)
dwFlags //用于便改数据传输特性,如传递MSG_OOB时发送OOB模式的数据。
IpOverlapped //WSAOVERLAPPED结构体变量的地址值,使用事件对象,用于确认完成数据传输。
IpCompletionRoutine//传入Completion Routine函数的入口地址值,可以通过该函数确认是否完成数
//据传输。
接下来介绍上述函数的第二个结构体参数类型,该结构体中存有待传输数据的地址和大小等信息。
typedef struct __WSABUF{
u_long len;// 待传输数据的大小
char FAR * buf;//缓冲地址值
}WSABUF,* LPWSABUF;
下面给出上述函数的调用示例。利用上述函数传输数据时可以按如下方式编写代码。
WSAEVENT event;
WSAOVERLAPPED overlapped;
WSABUF dataBuf;
char buf[BUF_SIZE]={"待传输的数据"}
int revcBytes=0;
......
event=WSACreateEvent();
memset(&overlapped,0,sizeof(overlapped));
overlapped.hEvent=event;
dataBuf.len=sizeof(buf);
dataBuf.buf=buf;
WSASend(hSocket,&dataBuf,1,&recvBytes,0,&overlapped,NULL);
......
调用WSASend函数时将第三个参数设置为1,因为策二个参数中待传输数据的缓冲个数为1。另外,多余参数均设置为NULL或0,其中需要注意第六个和第七个参数(稍后将具体解释,现阶段只需留意即可)。第六个参数中的WSAOVERLAPPED结构体定义如下。
typedef struct _WSAOVERLAPPED{
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
WSAEVENT hEvent;
} WSAOVERLAPPED,* LPWSAOVERLAPPED;
Internal、InternalHigh成员是进行重叠I/O时操作系统内部使用的成员,而Offset、OffsetHigh同样属于具有特殊用途的成员。所以各位实际只需要关注hEvent成员,稍后将介绍该成员的使用方法。
如果向lpOverlapped传递NULL,WSASend函数的第一个参数中的句柄所指的套接字将以阻
塞模式工作。还需要了解以下这个事实,否则也会影响开发。
“利用WSASend函教同时向多个目标传输数据时,需要分别构建传入第六个参数的WSAOVERLAPPED结构体变量。”
这是因为,进行重叠I/O的过程中,操作系统将使用WSAOVERLAPPED结构体变量。
关于WSASend再补充一点
前面谈到,通过WSASend函数的IpNumberOfBytesSent参数可应获得实际传输的数据大小
各位关于这一点不感到困惑吗?
实际上,WSASend函数调用过程中,函数返回时间点和数据传输完成时间点并非总不一致。如果输出缓冲是空的,且传输的数据并不大,那么函数调用后可以立即完成数据传输。此时,WSASend函数将返回0,lpNumberOfBytesSent中将保存实际传输的数据大小的信息。反之,WSASend函数返回后仍需要传输数据时,将返回SOCKET_ERROR,并将WSA_IO_PENDING注册为错误代码,该代码可以通过WSAGetLastError函数(稍后再介绍)得到。这时应该通过如下函效获取实际传输的数据大小。
#include <winsock2.h>
BOOL WSAGetOverlappedResult(SOCKET S, LPWSAOVERLAPPED IpOverlapped, LPDWORD 1pcbTransfer, B0OL fwait,LPDWORD lpdwFlags);
//成功时返回TRUE,失败时返回FALSE。
S //进行重叠I/O的套接字句柄。
IpOverlapped //进行重叠I/O时传递的WSAOVERLAPPED结构体变量的地址值。
lpcbTransfer //用于保实际传输的字节数的变量地址值。
fWait //如果调用该函数时仍在进行I/O,fWait为TRUE时等待I/O完成,fWait为FALSE时将
//返回FALSE并跳出函数。
IpdwFlags //调用WSARecv函数时,用于获取附加信息(例如OOB消息)。如果不需要,可以传
//递NULL。
通过此函数不仅可以获取数据传输结果,还可以验证接收数据的状态。
进行重叠I/O的WSARecv函数
#include <winsock2.h>
int WSARecv(SOCKET S,LPWSABUF lpBuffers, DWORD dwBuffercount,LPDWORD IpNumberofBytesRecvd, LPDWORD 1pFlags, LPWSAOVERLAPPED 1pOverlapped,LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
//成功时返回0,失败时返回SOCKET_ERROR。
s //赋予重叠I/O属性的套接字句柄
IpBuffers //用于保存接收数据的WSABUF结构体数组地址值。
dwBufferCount //向第二个参数传递的数组的长度。
IpNumberOfBytesRecvd //保存接收的数据大小信息的变量地址值。
IpFlags //用于设置或读取传输特性信息。
IpOverlapped //WSAOVERLAPPED结构体变量地址值。
IpCompletionRoutine //Completion Routine函数地址值。
关于上述函数的使用方法将同样结合示例进行说明。以上就是重叠I/O中的数据I/O方法,下一节将介绍I/O完成及如何确认结果。
重叠I/O的I/O完成确认
重叠I/O中有2种方法确认I/O的完成并获取结果。
□利用WSASend、WSARecv函数的第六个参数,基于事件对象。
□利用WSASend、WSARecv函数的第七个参数,基于Completion Routine。
只有理解了这2种方法,才能算是掌握了重叠I/O。首先介绍利用第六个参数的方法。
使用事件对象
之前已经介绍了WSASend、WSARecv函数的第六个参数—WSAOVERLAPPED结构体,
因此直接给出示例。希望各位通过该示例验证如下2点。
□完成I/O时,WSAOVERLAPPED结构体变量引用的事件对象将变为signaled状态
□为了验证I/O的完成和完成结果,需要调用WSAGetOvrlappedResult函数。
需要说明的是,该示例的目的在于整理之前的一系列知识点。因此,推荐各位在此基础上自行编写可以体现重叠I/O优点的示例。
#include<stdio.h>
#include<stdlib.h>
#include<winsock2.h>
void ErrorHandling(char *msg);
int main(int argc, char *argv[]){
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN sendAdr;
WSABUF dataBuf;
char msg[]="Network is Computer!";
int sendBytes=0;
WSAEVENT evobj;
WSAOVERLAPPED overlapped;
if(argc!=3){
printf("Usage: %s <IP> <port>\n",argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2,2), &wsaData)!=0)ErrorHandling("WSAStartup() error!");
hSocket=WSASocket(PF_INET,SOCK_STREAM, 0, NULL,0,WSA_FLAG_OVERLAPPED);
memset(&sendAdr, 0, sizeof(sendAdr));
sendAdr.sin_family=AF_INET;
sendAdr.sin_addr.s_addr=inet_addr(argv[1]);
sendAdr.sin_port=htons(atoi(argv[2]));
if(connect(hSocket,(SOCKADDR*)&sendAdr, sizeof(sendAdr))==SOCKET_ERROR)
ErrorHandling("connect() error!");
evobj=WSACreateEvent();
memset(&overlapped, 0, sizeof(overlapped));
overlapped.hEvent=evobj;
dataBuf.len=strlen(msg)+1;
dataBuf.buf=msg;
if(WSASend(hSocket, &dataBuf,1,&sendBytes,0,&overlapped,NULL)==SOCKET_ERROR){
if(WSAGetLastError()==WSA_IO_PENDING){
puts("Background data send");
WSAWaitForMultipleEvents(1, &evObj,TRUE, WSA_INFINITE, FALSE);
WSAGetOverlappedResult(hSocket,&overlapped,&sendBytes,FALSE,NULL);
}
else{
ErrorHandling("WSASend() error");
}
}
printf("Send data size: %d \n", sendBytes);
WSACloseEvent(evObj);
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(char *msg){
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
上述示例的第44行调用的WSAGetLastError函数定义如下。调用套接字相关函数后,可以通过该函数获取错误信息。
#include<winsock2.h>
int WSAGetLastError(void);//返回错误代码(表示错误原因)
上述示例中该函数的返回值为WSA_IO_PENDING,由此可以判断WSASend函数的调用结果并非发生了错误,而是尚未完成的状态。下面介绍与上述示例配套使用的Receiver,该示例的结构与之前的Sender类似。
#include<stdio.h>
#include<stdlib.h>
#include<winsock2.h>
#define BUF_SIZE 1024
void ErrorHandling(char *message);
int main(int argc, char* argv[]){
WSADATA wsaData;
SOCKET hLisnSock, hRecvSock;
SOCKADDR_IN lisnAdr, recvAdr;
int recvAdrSz;
WSABUF dataBuf;
WSAEVENT evObj;
WSAOVERLAPPED overlapped;
char buf[BUF_SIZE];
int recvBytes=0, flags=0;
if(argc!=2){
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2,2), &wsaData)!=0)ErrorHandling("WSAStartup() error!");
hLisnSock=WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
memset(&lisnAdr, 0, sizeof(lisnAdr));
lisnAdr.sin_family=AF_INET;
lisnAdr.sin_addr.s_addr=htonl(INADDR_ANY);
lisnAdr.sin_port=htons(atoi(argv[1]));
if(bind(hLisnsock,(SOCKADDR*)&lisnAdr, sizeof(lisnAdr))==SOCKET_ERROR)
ErrorHandling("bind() error");
if(listen(hLisnsock,5)==SOCKET_ERROR)
ErrorHandling("listen() error");
recvAdrSz=sizeof(recvAdr);
hRecvSock=accept(hLisnSock,(SOCKADDR*)&recvAdr,&recvAdrsz);
evObj=WSACreateEvent();
memset(&overlapped,0,sizeof(overlapped));
overlapped.hEvent=evObj;
dataBuf.len=BUF_SIZE;
dataBuf.buf=buf;
if(WSARecv(hRecvSock,&dataBuf,1,&recvBytes,&flags,&overlapped, NULL)==SOCKET_ERROR){
if(WSAGetLastError()==WSA_IO_PENDING){
puts("Background data receive");
WSAWaitForMultipleEvents(1,&evObj,TRUE, WSA_INFINITE,FALSE);
WSAGetoverlappedResult(hRecvSock,&overlapped, &recvBytes,FALSE, NULL);
}
else{
ErrorHandling("WSARecv() error");
}
}
printf("Received message: %s \n", buf);
WSACloseEvent(evObj);
closesocket(hRecvSock);
closesocket(hLisnSock);
WSACleanup();
return 0;
}
void ErrorHandling(char *message){
//与之前示例的ErrorHandling函数一致。
}
使用Completion Routine函数
前面的示例通过事件对象验证了I/O完成与否,下面介绍如何通过WSASend、WSARecv函数的最后一个参数中指定的Completion Routine(以下简称CR)函数验证I/O完成情况。“注册CR”具有如下含义:"Pending的I/O完成时调用此函数"
I/O完成时调用注册过的函数进行事后处理,这就是CompletionRoutine的运作方式。如果执行重要任务时突然调用Completion Routine,则有可能破坏程序的正常执行流。
因此,操作系统通常会预先定义规则:"只有请求I/O的线程处于alertable wait状态时才能调用Completion Routine函数!"
“alertable wait状态”是等待接收操作系统消息的线程状态。调用下列函数时进人alertable wait状态。
□ WaitForSingleObjectEx
□ WaitForMultipleObjectsEx
□ WSAWaitForMultipleEvents
□ SleepEx
第一、第二、第四个函数提供的功能与WaitForSingleObject、WaitForMultipleObjects、Sleep函数相同。上述函数只增加了1个参数,如果该参数为TRUE,则相应线程将进入alerteble wait状态。另外,第21章介绍过以WSA为前缀的函数,该函数的最后一个参数设置为TRUE时,线程同样进人alertable wait状态。
因此,启动I/O任务后,执行完紧急任务时可以调用上述任一函数验证I/O完成与否。此时操作系统知道线程进人alertable wait状态,如果有已完成的I/O,则调用相应Completion Routine函数。 调用后,上述函数将金部返回WUTIO_COMPLETION并开始执行接下来的程序。
#include<stdio.h>
#include<stdlib.h>
#include <winsock2.h>
#define BUF_SIZE 1024
void CALLBACK CompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void ErrorHandling(char *message);
WSABUF dataBuf;
char buf[BUF_SIZE];
int recvBytes=0;
int main(int argc, char* argv[]){
WSADATA wsaData;
SOCKET hLisnSock, hRecvSock;
SOCKADDR_IN lisnAdr, recvAdr;
WSAOVERLAPPED overlapped;
WSAEVENT evObj;
int idx, recvAdrSz, flags=0;
if(argc!=2){
printf("Usage:%s <port>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2,2), &wsaData)!= 0)ErrorHandling("WSAStartup() error!");
hLisnSock=WSASocket(PF_INET, SOCK_STREAM, 0,NULL, 0,WSA_FLAG_OVERLAPPED);
memset(&lisnAdr, 0, sizeof(lisnAdr));
lisnAdr.sin_family=AF_INET;
lisnAdr.sin_addr.s_addr=htonl(INADDR_ANY);
lisnAdr.sin_port=htons(atoi(argv[1]));
if(bind(hLisnSock,(SOCKADDR*)&lisnAdr,sizeof(lisnAdr))==SOCKET_ERROR)
ErrorHandling("bind() error");
if(listen(hLisnSock, 5)==SOCKET_ERROR)ErrorHandling("listen() error");
recvAdrSz=sizeof(recvAdr);
hRecvSock=accept(hLisnSock,(SOCKADDR*)&recvAdr,&recvAdrSz);
if(hRecvSock==INVALID_SOCKET)ErrorHandling("accept() error");
memset(&overlapped, 0, sizeof(overlapped));
dataBuf.len=BUF_SIZE;
dataBuf.buf=buf;
evObj=WSACreateEvent();//没什么用的事件对象
if(WSARecv(hRecvSock,&dataBuf,1,&recvBytes,&flags,&overiapped,CompRoutine)==SOCKET_ERROR)
{
if(WSAGetLastError()==WSA_IO_PENDING){
puts("Background data receive");
}
}
idx=WSAWaitForMultipleEvents(1,&evObj, FALSE, WSA_INFINITE,TRUE);
if(idx==WAIT_IO_COMPLETION)
puts("Overlapped I/O Completed");
else
ErrorHandling("WSARecv() error");
wSACloseEvent(evObj);
closesocket(hRecvSock);
closesocket(hLisnSock);
WSACleanup();
return 0;
}
void CALLBACK CompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED IpOverlapped, DWORD flags){
if(dwError!=0)ErrorHandling("CompRoutine error");
else{
recvBytes=szRecvBytes;
printf("Received message: %s \n", buf);
}
}
void ErrorHandling(char *message){
//与之前示例的函数一致,故省略。
}
下面给出传入WSARecv函数的最后一个参数的Completion Routine函数原型。
void CALLBACK CompletionRoutine(
DWORD dwError,
DWORD cbTransferred,
LPWSAOVERLAPPED lpOverlapped,
DWORD dwFlags
);
其中第一个参数中写入错误信息(正常结束时写入0),第二个参数中写入实际收发的字节数。第三个参数中写入WSASend、WSARecv函数的参数IpOverlapped,dwFlags中写入调用I/O函数时传入的特性信息或0。另外,返回值类型void后插入的CALLBACK关键字与main函数中声明的关键字WINAPI相同,都是声明函数的调用规范,所以定义Completion Routine函数时必须添加。本章介绍了不少内容,这些都是为了理解第23章的IOCP而讲解的。可以说,本章是学习第23章的必要条件,请各位务必掌握本章内容。