
22.1 理解重叠 I/O 模型
第21章异步处理的并非I/O, 而是"通知". 本章讲解的才是以异步方式处理 I/O 的方法. 只有理解了二者的区别和各自的优势, 才能更轻松地学习第23章的 IOCP.
重叠 I/O
其实各位对于重叠 I/O 并不陌生. 大家已经掌握了异步 I/O. 我通过图21-2说明过异步I/O模型, 实际上, 这种异步 I/O 就相当于 重叠I/O. 下面我将给出重叠I/O, 各位可自行判断二者是否相似. 图22-1 给出重叠 I/O 的原理.

如图22-1 所示, 同一线程内部向多个目标传输(或从多个目标接收)数据引起的I/O重叠现象 称为 “重叠I/O”. 为了完成这项任务, 调用的I/O函数应立即返回, 只有这样才能发送后续数据. 从结果上看, 利用上述模型收发数据时, 最重要的前提条件就是异步I/O. 而且, 为了完成异步I/O, 调用的I/O函数应以非阻塞模式工作.
接下来的判断交给各位. 异步I/O和重叠I/O之间存在差异众说, 关键是要理解二者的关系. 异步方式进行I/O处理时, 即使不采用本章介绍的方式, 也可以通过其他方法构造如图 22-1 所示的 I/O 处理方式. 因此, 我认为不用明确区分.
本章讨论的重叠 I/O 的重点不在于 I/O
前面对异步 I/O 和重叠I/O进行了比较, 这些内容看似是本章的全部理论说明, 但其实还未进入重叠I/O的正题. 因为 Windows 中重叠I/O的重点并非 I/O 本身, 而是如何确认I/O完成时的状态. 不管是输入还是输出, 只要是非阻塞模式的, 就要另外确认执行结果. 关于这种确认方法我们还一无所知. 确认执行结果前需要经过特殊的处理过程, 这是本章要讲述的内容. Windows 中的重叠 I/O 不仅包含图 22-1 所示的I/O(这是基础). 还包含确认 IO 完成状态的方法.

创建重叠 I/O 套接字
首先要创建适用于重叠 I/O 的套接字, 可以通过如下函数完成.

各位对前3个参数比较熟悉, 第四个和第5个参数与目前的工作无关, 可以简单设置为 NULL 和 0. 可以向最后一个参数传递 WSA_FLAG_OVERLAPPED, 赋予创建出的套接字重叠 I/O 特性. 总之, 可以通过如下函数调用创建出可以进行重叠 I/O 的非阻塞模式的套接字.

执行重叠 I/O 的 WSASend 函数
创建出具有重叠 I/O 属性的套接字后, 接下来2个套接字(服务器/客户端之间的) 连接过程与一般的套接字连接过程相同, 但 I/O 数据时使用的函数不同. 先介绍重叠 I/O 使用的数据输出函数.

接下来介绍上述函数的第二个结构体参数类型, 该结构体中存有待传输数据的地址和大小等信息.

下面给出上函数的调用示例. 利用上函数传输数据时可以按如下方式编写代码.

调用 WSASend 函数时将第三个参数设置为1, 因为第二个参数中待传输数据的缓冲个数为1. 另外, 多余参数均设置为 NULL 或 0, 其中需要注意第六个和第7个参数(稍后将具体解析, 现阶段只需留意即可). 第六个参数中的 WSAOVERLAPPED 结构体定义如下.

Internal , InternalHigh 成员是进行重叠 I/O 时操作系统内部使用的成员, 而Offset , OffsetHigh 同样属于具有特殊用途的成员. 所以各位实际只需关注 hEvent 成员, 稍后将介绍成员的使用方法.

如果向 IpOverlapped 传递NULL, WSASend 函数的第一个参数中参数中的句柄所指的套接字将阻塞模式工作. 还需要了解一下这个事实, 否则也会影响开发.

这是因为, 进行重叠I/O的过程中, 操作系统将使用 WSAOVERLAPPED 结构体变量.
关于 WSASend 再次补充一点
前面谈到, 通过WSASend 函数的 IpNumberberOfBytesSent 参数可以获得实际传输的数据大小. 各位关于这一点不感到困惑吗?

实际上, WSASend 函数调用过程中, 函数返回时间点和数据传输完成点并非总不一致. 如果输出缓冲是空的, 且传输的数据并不大, 那么函数调用后可以立即完成数据传输. 此时, WSASend 函数将返回0, 而 lpNumberOfBytesSent 中将保存实际传输的数据大小信息. 反之, WSASend 函数返回仍需要传输数据时, 将返回 SOCKET_ERROR, 并将 WSA_IO_PENDING注册为错误代码, 该代码可以通过 WSAGetLastError 函数(稍后再介绍) 得到. 这时应该通过如下函数获取实际传输的数据大小.

通过此函数不仅可以获取数据传输结果, 还可以验证接收数据的状态. 如果给出示例前进行过多理论说明会使人感到乏味, 所以稍后将通过示例讲解此函数的使用方法.
进行重叠I/O 的 WSAPRecv 函数
有了 WSASend 函数的基础, WSARecv 函数将不难理解. 因为他们大同小异, 只是在功能上有接收和传输之分.

关于上述函数的使用方法将同样结合示例进行说明.
以上就是重叠 I/O 中的数据 I/O 方法, 下一节将介绍 I/O 完成及如何确认结果.

22.2 重叠 I/O 的 I/O 完成确认
重叠 I/O 中有2种方法确认 I/O 的完成并获取结果.

只有理解了这2种方法, 才能算是掌握了重叠 I/O (其实比22.1节更重要). 首先介绍利用第六个参数的方法.
使用事件对象
之前已经介绍了 WSASend, WSARecv 函数的第六个参数 – WSAOVERLAPPED 结构体, 因此直接给出示例. 希望各位通过该示例验证如下2点.

需要说明的是, 该示例的目的在于整理之前的系列知识点. 因此, 推荐各位在此基础上自行编写可以体现重叠I/O 优点的示例.

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
void ErrorHandling(const char* msg);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN sendAdr;
WSABUF dataBuf;
char msg[] = "Network is Computer!";
unsigned long 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(const char* msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
上述示例的第44行调用的 WSAGetLasError 函数定义如下. 调用套接字相关函数后, 可以通过该函数获取错误信息.

上述示例中该函数的返回值为 WSA_IO_PENDING, 由此可以判断 WSASend 函数的调用结果并非发生了错误, 而是尚未完成(Pendling) 的状态. 下面介绍与上述示例配套使用 Receiver, 该示例的结构与之前的 Sender 类似.

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#define BUF_SIZE 1024
void ErrorHandling(const 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];
unsigned long 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(const char* message)
{
fputs(message, stderr);
fputc('/n', stderr);
exit(1);
}
运行结果:

使用 Completion Routine 函数
前面的示例通过事件对象验证了 I/O 完成与否, 下面介绍如何通过 WSASend, WSARecv 函数的最后一个参数中指定的 Completion Routine (一下简介CR) 函数验证I/O 完成的情况. “注册CR” 具体有如下 含义:

I/O 完成时调用注册过的函数进行事后处理, 这就是 Completion Routine 的运作 方式. 如果执行重要任务时突然调用 Completin Routinue, 则有可能破坏程序的正常执行流. 因此, 操作系统通常会预先定义规则:

“alertable wait 状态” 是 等待 接收操作系统信息的线程状态. 调用下列 函数进入 altertable wait 状态.

第一 , 第二, 第四个函数提供的功能与 WaitForSingleObject, WaitForMultipleObjeects, Sleep 函数相同. 上述 函数只增加了1个参数, 如果该函数为TRUE, 则相应线程将 进入 alertalbe wait 状态. 另外, 第21章介绍过的 WSA 为前缀的函数, 该函数的最后一个参数设置为TRUE 时, 线程同步样 进入 alertable wait 状态. 因此, 启动I/O 任务后, 执行完紧急任务时可以调用上述任一函数验证I/O 完成与 否. 此时操作系统知道线程进入 alertable wait 状态. 如果有 已完成 的I/O, 则调用相应 Completion Routine 函数. 调用后, 上述函数将全部返回 WAIT_IO_COMPLETION, 并开始执行接下 来的程序.
以上就是 Completion Roution 函数相关的全部理论说明. 下面将之前的OverlappedRoutine 函数相关的全部理论说明. 下面将之前的OverlappedRecv_win.c 改为 Completion Routine 方式.

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#define BUF_SIZE 1024
void CALLBACK CompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void ErrorHandling(const char* message);
WSABUF dataBuf;
char buf[BUF_SIZE];
unsigned long recvBytes = 0;
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hLisnSock, hRecvSock;
SOCKADDR_IN lisnAdr, recvAdr;
WSAOVERLAPPED overlapped;
WSAEVENT evObj;
unsigned long idx, flags = 0;
int recvAdrSz = 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, &overlapped, 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 szBecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD falgs)
{
if (dwError != 0)
{
ErrorHandling("ComRoutine error");
}
else
{
recvBytes = recvBytes;
printf("Received message: %s \n", buf);
}
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行结果:

下面给出传入 WSARecv 函数的最后一个参数的 Completion Routinue 函数原型.

其中第一个参数中写入错误信息(正常结束时写入0), 第二个参数中写入实际收发的字节数. 第三个参数中写入 WSASend , WSARecv 函数的参数 lpOverlapped, dwFlags 中写入 调用I/O函数时传入的特性信息或0. 另外, 返回值类型 void 后插入的CALLBACK 关键字与 main 函数中声明的关键字 WINAPI 相同. 都是用于函数的调用规范, 所以定义Completion Routinue 函数时必须添加.
本章介绍了不少内容, 这些都是为了理解第端3章 IOCP 而讲解的. 可以 这说, 本章是学习第23章的必要条件, 请各位务必=掌握本章的内容.

结语:
我最近 买了实体书 , 先看完电子版(先过一遍知识点, 我没有这么牛逼能记住, 可以复习的嘛! ), 再买实体版 , 避免它又成为收藏书没啥用, 这本书非常适合新手
你可以下面这个网站下载这本书<TCP/IP网络编程>
https://www.jiumodiary.com/
时间: 2020-06-18
本文详细解析了重叠I/O模型,强调其在异步处理中的重要性,特别是与异步通知的区别。通过示例代码,展示了如何使用WSASend和WSARecv函数进行数据传输,并介绍了两种确认I/O完成的方法:使用事件对象和CompletionRoutine函数。
666

被折叠的 条评论
为什么被折叠?



