TCP/IP网络编程 第二十二章:重叠I/O模型

理解重叠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章的必要条件,请各位务必掌握本章内容。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Reol520

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值