TCP/IP网络编程_基于Windows的编程_第22章重叠I/O模型

本文详细解析了重叠I/O模型,强调其在异步处理中的重要性,特别是与异步通知的区别。通过示例代码,展示了如何使用WSASend和WSARecv函数进行数据传输,并介绍了两种确认I/O完成的方法:使用事件对象和CompletionRoutine函数。

在这里插入图片描述

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值