TCP/IP网络编程_基于Windows的编程_第21章异步通知I/O模型

在这里插入图片描述

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

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值