《TCP/IP网络编程》学习笔记 | Chapter 22:重叠 I/O 模型
《TCP/IP网络编程》学习笔记 | Chapter 22:重叠 I/O 模型
- 《TCP/IP网络编程》学习笔记 | Chapter 22:重叠 I/O 模型
- 理解重叠 I/O 模型
- 重叠 I/O
- 本章讨论的重叠 I/O 的重点不在于 I/O
- 创建重叠 I/O 套接字
- 执行重叠 I/O 的 WSASend 函数
- 进行重叠 I/O 的 WSARecv 函数
- 重叠 I/O 的 I/O 完成确认
- 使用事件对象
- 使用 Completion Routine 函数
《TCP/IP网络编程》学习笔记 | Chapter 22:重叠 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 loProtocolInfo,
GROUP g,
DWORD dwFlags
);
参数:
- af:协议族信息
- type:套接字数据传输方式
- protocol:2 个套接字之间使用的协议信息
- lpProtocolInfo:包含创建的套接字信息的WSAPROTOCOL_INFO结构体变量地址值,不需要时传递 NULL。
- g:为扩展函数而预约的参数,可以使用 0
- dwFlags:套接字属性信息
成功时返回套接字句柄,失败时返回 INVALID_SOCKET。
各位对前 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 lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
参数:
- s:套接字句柄,传递具有重叠 I/O 属性的套接字句柄时,以重叠 I/O 模型输出。
- IpBuffers:WSABUF 结构体变量数组的地址值,WSABUF 中存有待传输数据。
- dwBufferCount:第二个参数中数组的长度。
- IpNumberOfBytesSent:用于保存实际发送字节数的变量地址值
- dwFlags:用于便改数据传输特性,如传递 MSG_OOB 时发送 OOB 模式的数据。
- IpOverlapped:WSAOVERLAPPED 结构体变量的地址值,使用事件对象,用于确认完成数据传输。
- IpCompletionRoutine:传入 Completion Routine 函数的入口地址值,可以通过该函数确认是否完成数据传输。
成功时返回 0,失败时返回 SOCKET_ERROR。
接下来介绍上述函数的第二个结构体参数类型,该结构体中存有待传输数据的地址和大小等信息。
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 成员。
关于 WSAOVERLAPPED 结构体有 3 点需要注意:
- 为了进行重叠 I/O,WSASend 函数的 lpOverlapped 参数中应该传递有效的结构体变量地址值,而不是 NULL。
- 若向 lpOverlapped 传递 NULL,WSASend 函数的第一个参数中的句柄所指的套接字将以阻
塞模式工作。 - 利用 WSASend 函教同时向多个目标传输数据时,需要分别构建传入第六个参数的 WSAOVERLAPPED 结构体变量。这是因为,进行重叠 I/O 的过程中,操作系统将使用 WSAOVERLAPPED 结构体变量。
WSASend 函数调用过程中,函数返回时间点和数据传输完成时间点并非总不一致。分为以下两种情况:
- 如果输出缓冲是空的,且传输的数据并不大,那么函数调用后可以立即完成数据传输。此时,WSASend 函数将返回 0,lpNumberOfBytesSent 中将保存实际传输的数据大小的信息。
- 反之,WSASend 函数返回后仍需要传输数据时,将返回 SOCKET_ERROR,并将 WSA_IO_PENDING 注册为错误代码,该代码可以通过 WSAGetLastError 函数(稍后再介绍)得到。这时应该通过如下函效获取实际传输的数据大小。
#include <winsock2.h>
BOOL WSAGetOverlappedResult(
SOCKET s,
LPWSAOVERLAPPED lpOverlapped,
LPDWORD lpcbTransfer,
BOOL fWait,
LPDWORD lpdwFlags
);
参数:
- s:进行重叠 I/O 的套接字句柄。
- IpOverlapped:进行重叠 I/O 时传递的 WSAOVERLAPPED 结构体变量的地址值。
- lpcbTransfer:用于保存实际传输的字节数的变量地址值。
- fWait:如果调用该函数时仍在进行 I/O,fWait 为 TRUE 时等待 I/O 完成,fWait 为 FALSE 时将返回 FALSE 并跳出函数。
- IpdwFlags:调用 WSARecv 函数时,用于获取附加信息(例如 OOB 消息)。如果不需要,可以传递 NULL。
成功时返回 TRUE,失败时返回 FALSE。
通过此函数不仅可以获取数据传输结果,还可以验证接收数据的状态。
进行重叠 I/O 的 WSARecv 函数
#include <winsock2.h>
int WSARecv(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
参数:
- s:具有重叠 I/O 属性套接字句柄。
- IpBuffers:用于保存接收数据的 WSABUF 结构体变量数组的地址值。
- dwBufferCount:第二个参数中数组的长度。
- lpNumberOfBytesRecvd:用于保存接收字节数的变量地址值。
- lpFlags:用于设置或读取传输特性信息。
- IpOverlapped:WSAOVERLAPPED 结构体变量地址值。
- IpCompletionRoutine:Completion Routine 函数地址值。
成功时返回 0,失败时返回 SOCKET_ERROR。
Gather 输出指将多个缓冲中的数据累积到一定程度后一次性输出,Scatter 输入指将接收的数据分批保存。
重叠 I/O 的 WSASend 和 WSARecv 函数可以获得 writev & readv 函数的 Gather/Scatter I/O 功能。
重叠 I/O 的 I/O 完成确认
重叠 I/O 中有 2 种方法确认 I/O 的完成并获取结果。
- 利用 WSASend、WSARecv 函数的第六个参数,基于事件对象。
- 利用 WSASend、WSARecv 函数的第七个参数,基于 Completion Routine。
只有理解了这 2 种方法,才能算是掌握了重叠 I/O。首先介绍利用第六个参数的方法。
使用事件对象
直接给出示例。希望各位通过该示例验证如下 2 点:
- 完成 I/O 时,WSAOVERLAPPED 结构体变量引用的事件对象将变为 signaled 状态。
- 为了验证 I/O 的完成和完成结果,需要调用 WSAGetOvrlappedResult 函数。
发送端代码:
#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);
}
上述示例调用的 WSAGetLastError 函数定义如下。调用套接字相关函数后,可以通过该函数获取错误信息。
#include<winsock2.h>
int WSAGetLastError(void); // 返回错误代码(表示错误原因)
上述示例中该函数的返回值为 WSA_IO_PENDING,由此可以判断 WSASend 函数的调用结果并非发生了错误,而是尚未完成的状态。
下面介绍与上述示例配套使用的接收端代码:
在这里插入代码片