《TCP/IP网络编程》学习笔记 | Chapter 22:重叠 I/O 模型
《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!";
DWORD 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 函数的调用结果并非发生了错误,而是尚未完成的状态。
下面介绍与上述示例配套使用的接收端代码:
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#define BUF_SIZE 1024
void ErrorHandling(char *msg);
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];
DWORD 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(char *msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
编译:
gcc OverlappedSend_win.c -lws2_32 -o overlappedSend
gcc OverlappedRecv_win.c -lws2_32 -o overlappedRecv
运行结果:
使用 Completion Routine 函数
前面的示例通过事件对象验证了 I/O 完成与否,下面介绍如何通过 WSASend、WSARecv 函数的最后一个参数中指定的 Completion Routine 函数验证 I/O 完成情况。
注册 Completion Routine 函数的含义:Pending 的 I/O 完成时调用此函数。I/O 完成时调用注册过的函数进行事后处理,这就是 Completion Routine 的运作方式。
然而,如果执行重要任务时突然调用 Completion Routine 函数,有可能破坏程序的正常执行流。因此,操作系统通常会预先定义规则:只有请求 I/O 的线程处于 alertable wait 状态时才能调用 Completion Routine 函数。alertable wait 状态是等待接收操作系统消息的线程状态,调用下列函数时进入该状态:
- WaitForSingleObjectEx
- WaitForMultipleObjectsEx
- WSAWaitForMultipleEvents
- SleepEx
第一、第二、第四个函数提供的功能与 WaitForSingleObject、WaitForMultipleObjects、Sleep 函数相同。上述函数只增加了 1 个参数,如果该参数为 TRUE,则相应线程将进入 alertable wait 状态。另外,第 21 章介绍过以 WSA 为前缀的函数,该函数的最后一个参数设置为 TRUE 时,线程同样 alertable wait 状态。因此,启动 I/O 任务后,执行完紧急任务时可以调用上述任一函数验证 I/O 完成与否。此时操作系统知道线程进入 alertable wait 状态,如果有已完成的 I/O,则调用相应 Completion Routine 函数。 调用后,上述函数将全部返回 WAIT_IO_COMPLETION 并开始执行接下来的程序。
下面将之前的接收端代码改为 Completion Routine 方式。
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#define BUF_SIZE 1024
void CALLBACK CompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void ErrorHandling(char *msg);
WSABUF dataBuf;
char buf[BUF_SIZE];
DWORD recvBytes = 0, flags = 0;
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hLisnSock, hRecvSock;
SOCKADDR_IN lisnAdr, recvAdr;
WSAOVERLAPPED overlapped;
WSAEVENT evObj;
int idx, recvAdrSz;
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 szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
if (dwError != 0)
ErrorHandling("CompRoutine error");
else
{
recvBytes = szRecvBytes;
printf("Received message: %s \n", buf);
}
}
void ErrorHandling(char *msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
编译:
gcc CmplRoutinesRecv_win.c -lws2_32 -o cmplRoutinesRecv
运行结果:
下面给出传入 WSARecv 函数的最后一个参数的 Completion Routine 函数原型。
void CALLBACK CompletionRoutine(
DWORD dwError,
DWORD cbTransferred,
LPWSAOVERLAPPED lpOverlapped,
DWORD dwFlags
);
其中第一个参数中写入错误信息(正常结束时写入 0),第二个参数中写入实际收发的字节数。第三个参数中写入 WSASend、WSARecv 函数的参数 IpOverlapped,dwFlags 中写入调用 I/O 函数时传入的特性信息或 0。
另外,返回值类型 void 后插入的 CALLBACK 关键字与 main 函数中声明的关键字 WINAPI 相同,都是声明函数的调用规范,所以定义 Completion Routine 函数时必须添加。
习题
(1)异步通知 I/O 模型与重叠 I/O 模型在异步处理方面有哪些区别?
特性 | 异步通知 I/O 模型 | 重叠 I/O 模型 |
---|---|---|
概念 | 应用程序发起 I/O 请求后,不必阻塞等待操作完成,而是由操作系统在完成时发出通知。通知可以通过信号、回调函数、事件、状态标志等方式传递给应用程序 | 程序调用 I/O 函数时会传入一个专门的数据结构(即 OVERLAPPED 结构),系统则在后台开始执行 I/O 操作,并立即返回。应用程序可以稍后查询操作的状态,也可以借助事件句柄来获得完成通知 |
平台 | Unix/Linux | Windows |
核心机制 | 事件轮询、信号、回调 | OVERLAPPED 结构体 |
典型应用 | Nginx、Redis | IIS、Windows 高性能服务器 |
总之,异步通知 I/O 模型异步处理的并非 I/O,而是“通知”。而重叠 I/O 模型才是真正异步方式处理 I/O 的方法,通过非阻塞异步 I/O 函数和确认 I/O 完成状态的方法完成。
(2)请分析非阻塞 I/O、通知 I/O、重叠 I/O 之间的关系。
三者可以理解为从低级到高级的演进关系,但实现机制不同。
非阻塞 I/O 是一种简单的模式,它使得 I/O 调用在数据暂未就绪时立即返回,并由应用层主动轮询检测。
通知 I/O 则是在非阻塞基础上,通过信号、事件或回调的方式让内核主动告知应用程序数据就绪,从而避免了频繁的轮询。
重叠 I/O 是 Windows 特有的异步 I/O 实现方式,它通过 OVERLAPPED 结构以及多种内核通知机制(包括 IOCP、完成例程或事件对象)使得应用能够真正实现 I/O 与处理并行,从而大幅提高应用的并发性能和扩展性。
总的来说,这三种模型都是为了解决传统阻塞 I/O 带来的线程挂起与低 CPU 利用率的问题,从实现方式上可以看出:
-
非阻塞 I/O 仅改变调用返回方式;
-
通知 I/O 则把状态变化通过信号或事件反馈给应用;
-
重叠 I/O 则提供了完整的异步 I/O 框架,充分释放应用线程,使得 I/O 操作与计算能并行进行。
(3)阅读如下代码,请指出问题并给出解决方案。
while (1)
{
hRecvSock = accept(hLisnSock, (SOCKADDR *)&recvAdr, &recvAdrSz);
evObj = WSACreateEvent();
memset(&overlapped, 0, sizeof(overlapped));
overlapped.hEvent = evObj;
dataBuf.len = BUF_SIZE;
dataBuf.buf = buf;
WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, NULL);
}
虽然不完整,但足以通过这部分代码片段发现结构上存在的问题。
答:
代码存在的问题:
- 代码中通过传入 OVERLAPPED 结构调用了 WSARecv(),这表明是在发起一个异步接收操作。然而调用后没有调用 WSAWaitForMultipleEvents()、GetOverlappedResult() 或使用 I/O 完成端口(IOCP)等机制来等待或处理异步操作的完成,导致异步操作的结果“无人问津”。如果不等待或处理完成通知,可能会出现数据还未拷贝完毕就关闭连接或继续下一轮循环的问题。
- 每次循环中都调用 WSACreateEvent() 创建一个新事件句柄,但代码中没有相应的关闭(如调用 WSACloseEvent())操作。这会导致事件句柄不断累积,最终耗尽系统资源。
- 代码未对 accept、WSACreateEvent、以及 WSARecv 的返回值进行检查。如果出现错误,程序无法及时捕捉并做出处理。此外,接受到一个新连接后,也没有对 hRecvSock 做进一步的管理,如关闭连接或为每个连接启动独立的处理线程/机制;这种设计会导致后续连接的管理混乱。
- 同一个 buf 缓冲区、OVERLAPPED 结构以及事件句柄可能被重复使用而没有独立跟踪每个连接的状态。如果主循环中直接重复调用这一系列异步操作,而不区分每个连接的操作完成状态,则会造成数据覆盖和状态混乱问题。
改进方案:
- 为每个新 Socket (hRecvSock) 和事件对象 (evObj) 分配独立内存(如结构体),并在操作完成后释放。
- 使用 closesocket 关闭 Socket,WSACloseEvent 关闭事件对象。
完整代码:
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#define BUF_SIZE 1024
void ErrorHandling(char *msg);
int main(int argc, char *argv[])
{
WSADATA wsaData;
int iResult;
if (argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// 初始化 Winsock
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
// 创建监听 socket
SOCKET hLisnSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (hLisnSock == INVALID_SOCKET)
{
printf("socket failed: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 绑定地址
struct sockaddr_in servAddr;
ZeroMemory(&servAddr, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = INADDR_ANY;
servAddr.sin_port = htons(atoi(argv[1]));
if (bind(hLisnSock, (SOCKADDR *)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
ErrorHandling("bind() error");
if (listen(hLisnSock, 5) == SOCKET_ERROR)
ErrorHandling("listen() error");
while (1)
{
SOCKET hRecvSock;
struct sockaddr_in recvAdr;
int recvAdrSz = sizeof(recvAdr);
// 接受新的连接
hRecvSock = accept(hLisnSock, (struct sockaddr *)&recvAdr, &recvAdrSz);
if (hRecvSock == INVALID_SOCKET)
{
printf("accept failed: %d\n", WSAGetLastError());
continue; // 出错时跳过本次循环
}
printf("Accepted connection from %s:%d\n", inet_ntoa(recvAdr.sin_addr), ntohs(recvAdr.sin_port));
// 为当前连接创建一个事件对象用于异步操作
WSAEVENT evObj = WSACreateEvent();
if (evObj == WSA_INVALID_EVENT)
{
printf("WSACreateEvent failed: %d\n", WSAGetLastError());
closesocket(hRecvSock);
continue;
}
// 初始化 OVERLAPPED 结构
OVERLAPPED overlapped;
ZeroMemory(&overlapped, sizeof(overlapped));
overlapped.hEvent = evObj;
// 设置接收缓冲区并发起异步接收
char buf[BUF_SIZE];
WSABUF dataBuf;
dataBuf.len = BUF_SIZE;
dataBuf.buf = buf;
DWORD recvBytes = 0, flags = 0;
if (WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, NULL) == SOCKET_ERROR)
{
int err = WSAGetLastError();
if (err != WSA_IO_PENDING)
{
printf("WSARecv failed immediately: %d\n", err);
WSACloseEvent(evObj);
closesocket(hRecvSock);
continue;
}
// 异步操作已挂起,等待其完成
DWORD dwWait = WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, TRUE);
if (dwWait == WSA_WAIT_FAILED)
{
printf("WSAWaitForMultipleEvents failed: %d\n", WSAGetLastError());
WSACloseEvent(evObj);
closesocket(hRecvSock);
continue;
}
// 操作已完成,获取结果
BOOL bRet = WSAGetOverlappedResult(hRecvSock, &overlapped, &recvBytes, FALSE, &flags);
if (!bRet)
{
printf("WSAGetOverlappedResult failed: %d\n", WSAGetLastError());
WSACloseEvent(evObj);
closesocket(hRecvSock);
continue;
}
}
// 如果 WSARecv 返回非错误,则操作可能已经同步完成
printf("Received %d bytes: %.*s\n", recvBytes, recvBytes, buf);
// (此处可对接收到的数据进行处理,例如回显数据)
// 清理当前连接相关资源
WSACloseEvent(evObj);
closesocket(hRecvSock);
}
// 通常不会运行到这里
closesocket(hLisnSock);
WSACleanup();
return 0;
}
void ErrorHandling(char *msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
(4)请从源代码角度说明调用 WSASend 函数后如何验证 I/O 是否进入 Pending 状态。
WSASend 函数允许调用者在发起重叠 I/O 操作后立即返回,而不必等待操作完成。其典型调用过程通常如下:
- 准备一个 OVERLAPPED 结构(通常将其中的 hEvent 字段设置成一个事件句柄,以便后续等待)。
- 调用 WSASend 进行数据发送。此时有两种情况:
- 如果 WSAGetLastError 函数的返回值为 WSA_IO_PENDING,这表示操作已成功发起,但尚未完成,处于 Pending 状态;此时 I/O 操作将在后台执行,稍后会通过事件、回调或完成端口等机制通知调用者操作完成。
WSAGetLastError 函数原型:
#include<winsock2.h>
int WSAGetLastError(void); // 返回错误代码(表示错误原因)
示例代码:
DWORD sendBytes = 0;
DWORD flags = 0;
OVERLAPPED ol;
ZeroMemory(&ol, sizeof(ol));
// 可选:设置 ol.hEvent 为一个事件句柄,如果希望等待操作完成
ol.hEvent = hEvent; // hEvent 已通过 WSACreateEvent 创建
// 调用 WSASend 发送数据
int ret = WSASend(hSocket, &dataBuf, 1, &sendBytes, flags, &ol, NULL);
if (ret == SOCKET_ERROR) {
int err = WSAGetLastError();
if (err == WSA_IO_PENDING) {
// 说明 I/O 操作已进入 Pending 状态,WSASend 发起的异步发送操作正在后台执行
// 此时应用程序可以继续做其他事,稍后使用WSAWaitForMultipleEvents或者
// WSAGetOverlappedResult来等待并获得操作完成结果
} else {
// 其它错误,处理错误情况
}
} else {
// ret==0, 表示操作同步完成,此时 sendBytes 表示实际发送的字节数
}
(5)线程的“alertable wait 状态”的含义是什么?说出能使线程进入这种状态的 2 个函数。
alertable wait 状态是线程在等待一个或多个内核对象(比如事件、互斥体、信号量等)的同时处于可被异步过程调用中断和执行的状态。只有请求 I/O 的线程处于 alertable wait 状态时才能调用 Completion Routine 函数。
调用下列函数时进入该状态:
- WaitForSingleObjectEx
- WaitForMultipleObjectsEx
- WSAWaitForMultipleEvents
- SleepEx