21.1 理解异步通知I/O模型
各位应该还记得介绍过的 select 函数, 它是实现并发服务器端的方法之一. 本章内容可以理解为 select 模型的改进方式.
理解同步和异步
首先解析 “异步(Asynchronous)的含义”. 异步主要指 “不一致”, 它在数据I/O中非常有用. 之前的 Windows 示例中主要通过send & recv 函数进行同步 I/O. 调用 send 函数时, 完成数据传输后才能从函数返回(确切地说, 只有把数据完全传输到输出缓冲后才能返回); 而调用 recv 函数时, 只有读到期望大小的数据后才能返回. 因此, 相当于同步方式的 I/O 处理.
各位或许有这种疑问, 但我想反问大家: “哪些部分进行了同步处理? " 同步的关键是函数的调用及返回时刻, 以及数据传输的开始和完成时刻.
可以通过图 21-1 解析上述两句话的含义(上述语句和图中的"完成传输” 都是指数据完全传输到输出缓冲).
相信各位能够通过上述图文理解同步的关键所在. 那异步 I/O 的含义又是什么呢? 图21-2 给出解析, 希望大家与图21-1 进行对比.
从图 21-2 中可以看到, 异步 I/O 是指 I/O 函数的返回时刻与数据接收的完成时刻不一致. 如此看来, 我们接触过异步 I/O. 如果记不清这些内容, 可以回顾第17章 epoll 的异步I/O 部分.
同步 I/O 的缺点及异步方式的解决方案
异步I/O 就是为了克服同步的缺点而设计的模型. 同步I/O有哪些缺点? 异步方式又是如何解决的呢? 其实, 第17章的最后部分 "条件触发和边缘触发孰优孰劣"中给出过答案. 各位可能因为忘记这些内容而感到沮丧, 考虑到这一点, 我将以不同的, 更简单的方式解析. 从图 21-1 中很容易找到同步I/O的缺点: "进行I/O的过程中函数无法返回, 所以不能执行其他任务! " 而图22-2 中, 无论数据是否完成交换都返回函数, 这就意味着可以执行其他任务. 所以说 “异步方式能够比同步方式更有效使用 CPU”.
理解异步通知 I/O 模型
之前分析了同步和异步方式的I/O函数, 确切得说, 分析了同步和异步方式下 I/O 函数返回时间点的差异. 下面我希望扩展讨论的对象(同步和异步并不局限于 I/O ).
本章题目为 “异步通知I/O模型”, 意为 “通知I/O” 是以异步方式工作的. 首先了解一下"通知I/O" 的含义:
故名思义, “通知I/O” 是指发生了I/O相关的特定情况. 典型的通知 I/O 模型是 select 方式. 还记得 select 监视的3种情况吗? 其中具有代表性的就是 “收到数据的情况”. select函数就是从返回调用的函数时通知需要 I/O 处理的, 或可以进行 I/O 处理的情况. 但这种通知是以同步方式进行的, 原因在于, 需要 I/O 或可以进行 I/O 的时间点(简言之就是 I/O 相关事件发生的时间点) 与 select 函数的返回时间点一致.
相信各位已理解通知 I/O 模型的含义. 与 “select 函数只在需要或可以进行I/O 的情况下返回” 不同, 异步通知I/O 模型中函数的返回与I/O状态无关. 本章的 WSAEventSelect 函数就是 select 函数的差异版本.
当然需要! 异步通知I/O中, 指定I/O监视对象的函数和实际验证状态变化的函数是相互分离的, 因此, 指定监视对象后可以离开执行其他任务, 最后再回来验证状态变化. 以上就是通知 I/O 的所有理论, 下面通过具体函数实现该模型.
21.2 理解和实现异步通知 I/O 模型
异步通知 I/O 模型的实现方法有2种: 一种是使用本书介绍的 WSAEventSelect 函数, 另外一种是使用 WSAAsyncSelect 函数. 使用 WSAAsyncSelect 函数时需要指定 Windows 句柄以获取发生的事件(UI相关内容), 因此本书不会涉及, 但大家要知道这个函数.
WSAEventSelect 函数和通知
如前所述, 告知I/O 状态变化的操作就是 “通知”, I/O的状态变化可以分为不同情况.
这2种情况都意味着发生了需要或可以进行I/O的事件, 我将根据上下文适当混用这些概念.
首先介绍 WSAEventSelect 函数, 该函数用于指定某一套接字为事件监视对象.
传入参数s的套接字内只要发生 INetworkEvent 中指定的事件之一, WSAEventSelect 函数就将 hEventObject 句柄所指内核对象改为 signaled 状态. 因此, 该函数又称 “连接事件对象和套接字的函数”.
另外一个重要的事实是, 无论事件发生与否, WSAEventSelect 函数调用后都会直接返回, 所以执行其他任务. 也就是说, 该函数以异步通知方式工作. 下面介绍作为该函数第三个参数的事件类型信息, 可以通过位或运算同时指定多个信息.
以上就是 WSAEventSelect 函数的调用方法. 各位或许有如下疑问 (很好的问题):
的确, 仅从概念上看, WSAEventSelect 函数的功能偏弱. 但使用该函数时, 没必要针多个套接字进行调用. 从select 函数返回时, 为了验证事件的发生需要再次针对所有句柄(文件描述符)调用函数, 但通过调用 WSAEventSelect 函数传递的套接字信息已注册到操作系统, 所以无需再次调用. 这反而是 WSAEventSelect 函数比select 函数的优势所在.
从前面关于 WSAEventSelect 函数的说明中可以看出, 需要补充如下内容.
上述过程中只要插入 WSAEventSelect 函数的调用就与服务器端的实现过程完全一致, 下面分别讲解.
manual-reset 模式事件对象的其他创建方法
我们之前利用 CreateEvent 函数创建了事件对象. CreateEvent 函数在创建事件对象时, 可以 在auto-reset模式和 manual-reset 模式中任选其一. 我们只需要 manual-reset 模式 non-signaled 状态的事件对象, 所以利用如下函数传创建较为方便.
上述声明中返回类型 WSAEVENT 的定义如下:
实际上就是我们熟悉的内核对象句柄, 这一点需要注意. 另外, 为了销毁通过上述函数创建的事件对象, 系统提供了如下函数.
验证是否发生事件
既然介绍了 WSACreateEvent 函数, 那调用 WSAEventSelect 函数应该不成问题. 接下来就要考虑调用 WSAEventSelect 函数后的处理. 为了验证是否发生事件, 需要查看事件对象. 完成该任务的函数如下, 除了多个参数外, 其余部分与 WaitForMulipleObjects 函数完全相同.
由于发生套接字事件, 事件对象转为 signale 状态后该函数才返回, 所以它非常有利于确认事件发生与否. 但由于最多可传递64个事件对象, 如果需要监视更多句柄, 就只能创建线程或扩展保存句柄的数组, 并多次调用上述函数.
对于 WSAWaitForMultipleEvents 函数, 各位可能产生如下疑问:
答案是: 只能通过1次函数无法得到转为 signaled 状态的所有事件对象句柄的信息. 通过该函数可以得到转为 signaled 状态的事件对象中的第一个 (按数组中的保存顺序) 索引值. 但可以利用 “事件对象为manual-reset模式” 的特点, 通过如下方式获得所有 signaled 状态的事件对象.
注意观察上述代码中的循环. 循环中从第一个事件对象最后一个事件对象逐一次序验证是否转为 signaled 状态(超时信息为0, 所以调用函数后立即返回). 之所以能做到这一点, 完全是因为事件为 manual-reset 模式, 这也解析了为何异步通过 I/O 模型中事件对象必须为 manual-reset 模式.
区分事件类型
既然已经通过 WSAWaitForMultipleEvents 函数得到了转为 signaled 状态的事件对象, 最后就要确定相应对象进入 signaled 状态的原因. 为完成该任务, 我们引入如下函数. 调用时, 不仅需要 signaled 状态的事件对象句柄, 还需要与之连接的(由WSAEventSelect 函数调用引发的) 发生事件的套接字句柄.
上述函数将 manual-reset 模式的事件对象改为 non-signale 状态, 所以得到发生的事件类型后, 不必单独调用ResetEvent 函数. 下面介绍与上述函数有关的 WSANETWORKEVENTS 结构体.
上述结构体的 INetworkEvents 成员将保存发生的事件信息. 与 WSAEventSelect 函数的第三个参数相同, 需要接收数据时, 该成员函数为 FD_READ; 有连接请求时, 该成员为 FD_ACCEPT. 因此, 可通过如下方式查看发生的事件类型.
另外, 错误信息将保存到声明为成员的 iErrorCode 数组(发生错误的原因可能很多, 因此用数组声明). 验证方法如下.
可通过如下描述理解上述内容.
因此可以用如下 方式检查错误.
以上就是异步通知I/O模型的全部内容, 下面利用这些 知识编写示例.
利用异步通知 I/O 模型实现回声服务器端
下面要介绍的回声服务器端代码相对偏长, 所以将分为几个 部分逐个介绍.
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#define BUF_SIZE 100
void CompressSockets(SOCKET hSockArr[], int idx, int total);
void CompressEvents(WSAEVENT hEventArr[], int idx, int total);
void ErrorHandling(const char* msg);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hServeSock, hClntSock;
SOCKADDR_IN servAdr, clntAdr;
SOCKET hSockArr[WSA_MAXIMUM_WAIT_EVENTS];
WSAEVENT hEventArr[WSA_MAXIMUM_WAIT_EVENTS];
WSAEVENT newEvent;
WSANETWORKEVENTS netEvents;
int numOfClntSock = 0;
int strLen, i;
int posInfo, startIdx;
int clntAdrLen;
char msg[BUF_SIZE];
if (argc != 2)
{
printf("Usage: %s <port> \n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
ErrorHandling("WSAStartup() error");
}
hServeSock = socket(PF_INET, SOCK_STREAM, 0);
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
servAdr.sin_port = htons(atoi(argv[1]));
if (bind(hServeSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
{
ErrorHandling("bind() error");
}
if (listen(hServeSock, 5) == -1)
{
ErrorHandling("listen() error");
}
newEvent = WSACreateEvent();
if (WSAEventSelect(hServeSock, newEvent, FD_ACCEPT) == SOCKET_ERROR)
{
ErrorHandling("WSAEventSelect() error");
}
hSockArr[numOfClntSock] = hServeSock;
hEventArr[numOfClntSock] = newEvent;
numOfClntSock++;
while (1)
{
posInfo = WSAWaitForMultipleEvents(numOfClntSock, hEventArr, FALSE, WSA_INFINITE, FALSE);
startIdx = posInfo - WSA_WAIT_EVENT_0;
for (i = startIdx; i < numOfClntSock; i++)
{
int sigEventIdx = WSAWaitForMultipleEvents(1, &hEventArr[i], TRUE, 0, FALSE);
if ((sigEventIdx == WSA_WAIT_FAILED || sigEventIdx == WSA_WAIT_TIMEOUT))
{
continue;
}
else
{
sigEventIdx = i;
WSAEnumNetworkEvents(hSockArr[sigEventIdx], hEventArr[sigEventIdx], &netEvents);
if (netEvents.lNetworkEvents & FD_ACCEPT) /* 请求连接时 */
{
if (netEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
{
puts("Accept Error");
break;
}
clntAdrLen = sizeof(clntAdr);
hClntSock = accept(hSockArr[sigEventIdx], (SOCKADDR*)&clntAdr, &clntAdrLen);
newEvent = WSACreateEvent();
WSAEventSelect(hClntSock, newEvent, FD_READ | FD_CLOSE);
hEventArr[numOfClntSock] = newEvent;
hSockArr[numOfClntSock] = hClntSock;
numOfClntSock++;
puts("connected new client...");
}
if (netEvents.lNetworkEvents & FD_READ)
{
if (netEvents.iErrorCode[FD_READ_BIT] != 0)
{
puts("Read Error");
break;
}
strLen = recv(hSockArr[sigEventIdx], msg, sizeof(msg), 0);
send(hSockArr[sigEventIdx], msg, strLen, 0);
}
if (netEvents.lNetworkEvents & FD_CLOSE) /* 断开连接 */
{
if (netEvents.iErrorCode[FD_CLOSE_BIT] != 0)
{
puts("Close Error");
break;
}
WSACloseEvent(hEventArr[sigEventIdx]);
closesocket(hSockArr[sigEventIdx]);
numOfClntSock--;
CompressSockets(hSockArr, sigEventIdx, numOfClntSock);
CompressEvents(hEventArr, sigEventIdx, numOfClntSock);
}
}
}
}
WSACleanup();
return 0;
}
void CompressSockets(SOCKET hSockArr[], int idx, int total)
{
int i;
for (i = idx; i < total; i++)
{
hSockArr[i] = hSockArr[i + 1];
}
}
void CompressEvents(WSAEVENT hEventArr[], int idx, int total)
{
int i;
for (i = idx; i < total; i++)
{
hEventArr[i] = hEventArr[i + 1];
}
}
void ErrorHandling(const char* msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
断开连接并从数组中删除套接字以及与之相连的对象时调用上述2个函数(以Compress…开头), 它们主要用于填充数组空间, 只有同时调用才能维持套接字和事件对象之间的关系.
既然分析了所有代码, 本应给出运行结果, 但因其与之前的回声服务器端/客户端并无差异, 故省略. 另外 , 上述示例可以与任意 回声客户端配合运行, 各位可以选择Windows 平台下的客户端作为配套程序 .
结语:
我最近 买了实体书 , 先看完电子版(先过一遍知识点, 我没有这么牛逼能记住, 可以复习的嘛! ), 再买实体版 , 避免它又成为收藏书没啥用, 这本书非常适合新手
你可以下面这个网站下载这本书<TCP/IP网络编程>
https://www.jiumodiary.com/
时间: 2020-06-17