理解重叠 I/O 模型
重叠 I/O(Overlapped I/O) 这个名称来自于它允许多个 I/O 操作 "重叠" 执行,而不必等待前一个操作完成。这种方式让应用程序可以同时发起多个 I/O 操作,而不阻塞调用线程。
重叠 I/O 和异步 I/O 的关系:
- 重叠 I/O 是 Windows 实现异步 I/O 的一种方式。在 Windows 系统中,重叠 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, // 地址族,例如 AF_INET(IPv4)
int type, // 套接字类型,例如 SOCK_STREAM(TCP),SOCK_DGRAM(UDP)
int protocol, // 协议,例如 IPPROTO_TCP 或 IPPROTO_UDP
LPWSAPROTOCOL_INFO lpProtocolInfo, // 协议信息,一般传 NULL
GROUP g, // 多播组ID,通常为 0
DWORD dwFlags // 特殊标志,如 WSA_FLAG_OVERLAPPED 表示支持重叠I/O
);
-
af(地址族) :
-
指定使用的协议族(协议地址簇)。
-
常见的值:
AF_INET
: IPv4 地址族AF_INET6
: IPv6 地址族AF_UNSPEC
: 不指定具体协议族
-
-
type(套接字类型) :
-
指定套接字的类型。
-
常见的值:
SOCK_STREAM
: 流式套接字(TCP)SOCK_DGRAM
: 数据报套接字(UDP)SOCK_RAW
: 原始套接字(允许对网络层进行直接操作)
-
-
protocol(协议) :
-
指定使用的协议。
-
常见的值:
IPPROTO_TCP
: TCP 协议IPPROTO_UDP
: UDP 协议IPPROTO_IP
: 原始 IP 协议
-
-
lpProtocolInfo(协议信息) :
- 可选的协议信息结构指针,通常为
NULL
,除非需要与特定的协议相关联。
- 可选的协议信息结构指针,通常为
-
g(组 ID) :
- 多播组的标识符,通常传入 0。
-
dwFlags(标志) :
-
指定套接字的额外特性。常见的值:
WSA_FLAG_OVERLAPPED
: 套接字支持重叠 I/O。WSA_FLAG_NO_HANDLE_INHERIT
: 不允许句柄继承。WSA_FLAG_REGISTERED_IO
: 为 Windows 注册 I/O 用。
-
简而言之,可以通过如下语句创建出可以进行重叠 I/O 的非阻塞模式的套接字:
WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
执行重叠 I/O 的 WSASend
函数
#include <winsock2.h>
int WSASend(
SOCKET s, // 已连接的套接字
LPWSABUF lpBuffers, // 指向缓冲区的数组,包含要发送的数据
DWORD dwBufferCount, // 缓冲区数组中的缓冲区个数
LPDWORD lpNumberOfBytesSent, // 成功发送的字节数
DWORD dwFlags, // 发送的标志,如 MSG_OOB
LPWSAOVERLAPPED lpOverlapped, // 指向 WSAOVERLAPPED 结构,用于异步操作
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 完成例程指针(可选)
);
-
s(已连接的套接字) :
- 指定要发送数据的已连接套接字。
-
lpBuffers(指向缓冲区的数组) :
-
指向
WSABUF
结构的数组,该结构包含要发送的数据。 -
WSABUF
结构定义如下:typedef struct _WSABUF { ULONG len; // 缓冲区的长度(字节数) CHAR *buf; // 指向实际数据缓冲区的指针 } WSABUF, *LPWSABUF;
-
-
dwBufferCount(缓冲区个数) :
- 缓冲区数组中缓冲区的个数,即
lpBuffers
数组的大小。
- 缓冲区数组中缓冲区的个数,即
-
lpNumberOfBytesSent(成功发送的字节数) :
- 指向一个变量,用于接收成功发送的数据字节数。如果执行异步操作,该值为未定义,可以传
NULL
。
- 指向一个变量,用于接收成功发送的数据字节数。如果执行异步操作,该值为未定义,可以传
-
dwFlags(发送标志) :
-
发送操作的标志。常见值有:
MSG_OOB
: 发送OOB模式的数据
-
-
lpOverlapped(重叠结构) :
- 指向
WSAOVERLAPPED
结构,用于异步 I/O 操作。如果是同步操作(非重叠模式),此参数应为NULL
。 WSAOVERLAPPED
结构允许执行异步的非阻塞操作。WSAOVERLAPPED
结构定义如下:typedef struct _WSAOVERLAPPED { DWORD Internal; // 保留用于系统的内部使用,用户不应该直接修改。 DWORD InternalHigh; // 保留用于系统的内部使用,用户不应该直接修改。 DWORD Offset; // 文件操作时的字节偏移量,对于网络 I/O 通常是 0。 DWORD OffsetHigh; // 文件操作时的字节偏移量(高位),对于网络 I/O 通常是 0。 WSAEVENT hEvent; // 当 I/O 操作完成时设置的事件对象,用于通知操作完成。 } WSAOVERLAPPED, *LPWSAOVERLAPPED;
- 指向
-
lpCompletionRoutine(完成例程指针) :
- 一个回调函数指针,当 I/O 操作完成时调用。如果使用重叠 I/O 且未指定完成例程,则该参数可以为
NULL
。
- 一个回调函数指针,当 I/O 操作完成时调用。如果使用重叠 I/O 且未指定完成例程,则该参数可以为
调用 WSAOVERLAPPED
函数需要注意:
① 为了进行重叠 I/O,WSASend
函数的 lpOverlapped
参数中应该传递有效的结构体变量地址,而不是 NULL。 如果向lpOverlapped
传递 NULL,WSASend
函数的第一个参数中的句柄所指的套接字将以阻塞模式工作。
② “利用 WSASend
函数同时向多个目标传输数据时,需要分别构建传入第六个参数的 WSAOVERLAPPED
结构体变量。”
③ 通过调用 WSAGetOverlappedResult
函数得到传输的数据大小。
BOOL WSAGetOverlappedResult(
SOCKET s, // 与重叠操作关联的套接字
LPWSAOVERLAPPED lpOverlapped, // 指向 WSAOVERLAPPED 结构的指针
LPDWORD lpcbTransfer, // 用于存储传输的字节数
BOOL fWait, // 是否等待操作完成
LPDWORD lpdwFlags // 指向标志的指针,用于接收操作状态
);
-
s:
- 关联重叠操作的套接字句柄,即异步操作所使用的套接字。
-
lpOverlapped:
- 指向之前传递给异步 I/O 操作(如
WSASend
或WSARecv
)的WSAOVERLAPPED
结构的指针。该结构保存了异步操作的状态。
- 指向之前传递给异步 I/O 操作(如
-
lpcbTransfer:
- 指向一个
DWORD
变量的指针,用于存储操作完成时实际传输的字节数(如发送或接收的字节数)。
- 指向一个
-
fWait:
- 如果设置为
TRUE
,WSAGetOverlappedResult
会等待操作完成;如果为FALSE
,函数立即返回,且不会等待操作完成。
- 如果设置为
-
lpdwFlags:
- 指向一个
DWORD
变量的指针,用于接收操作的状态标志。这些标志可能包括MSG_PARTIAL
,用于指示是否接收到部分消息。
- 指向一个
-
返回值:
- 如果函数成功,返回值为非零 (
TRUE
),表示异步 I/O 操作已经完成,并且lpcbTransfer
和lpdwFlags
被正确填充。 - 如果函数失败,返回值为 0 (
FALSE
),并且可以调用WSAGetLastError
来获取错误代码。
- 如果函数成功,返回值为非零 (
执行重叠 I/O 的 WSARecv
函数
int WSARecv(
SOCKET s, // 与接收操作关联的套接字
LPWSABUF lpBuffers, // 指向 WSABUF 结构的指针数组,用于存储接收到的数据
DWORD dwBufferCount, // 缓冲区数组中的缓冲区数量
LPDWORD lpNumberOfBytesRecvd, // 指向 DWORD 变量的指针,存储实际接收到的字节数
LPDWORD lpFlags, // 指向 DWORD 变量的指针,存储接收操作的标志
LPWSAOVERLAPPED lpOverlapped, // 指向重叠结构的指针,用于异步操作
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
// 指向完成例程的指针,如果指定,则在完成时调用
);
重叠 I/O 的 I/O 完成确认
重叠 I/O 中有2种方法确认 I/O 的完成并获取结果:
① 利用 WSASend
、WSARecv
函数的第六个参数,基于事件对象。
② 利用 WSASend
、WSARecv
函数的第七个参数,基于 Completion Routine。
(一)利用第六个参数
上面介绍过 WSASend
、WSARecv
函数的第六个参数——WSAOVERLAPPED
结构体,下面给出示例,验证如下2点:
① 完成 I/O 时,WSAOVERLAPPED
结构体变量引用的事件对象将变为 signaled 状态。
② 为了验证 I/O 的完成和完成结果,需要调用 WSAGetOverlappedResult
函数。
示例(OverlappedSend_win.c
):
#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);
}
代码解析:
-
初始化 Winsock 环境:
if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0) ErrorHandling("WSAStartup() error!");
调用了
WSAStartup
初始化 Winsock。 -
创建一个重叠 I/O 套接字:
hSocket = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
使用
WSASocket
创建带有WSA_FLAG_OVERLAPPED
标志的套接字,表示这个套接字将用于异步 I/O 操作。 -
连接到服务器:
if(connect(hSocket, (SOCKADDR*)&sendAdr, sizeof(sendAdr))==SOCKET_ERROR) ErrorHandling("connect() error!");
使用
connect
函数连接到服务器。 -
创建事件对象和重叠结构体:
evObj = WSACreateEvent(); memset(&overlapped, 0, sizeof(overlapped)); overlapped.hEvent = evObj;
创建一个事件对象
evObj
,并将它关联到WSAOVERLAPPED
结构的hEvent
成员。这样,当发送完成时,可以通过evObj
事件获知。 -
异步发送数据:
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"); } }
WSASend
函数若不返回SOCKET_ERROR
,则说明数据传输完成,即sendBytes
中的值有意义。调用WSASend
以异步方式发送数据,若返回SOCKET_ERROR
,则调用WSAGetLastError()
函数获取错误信息,如果返回WSA_IO_PENDING
,表示操作未立即完成,而是在后台继续执行。通过WSAWaitForMultipleEvents
函数等待事件evObj
触发,表示发送操作已完成。之后通过WSAGetOverlappedResult
获取操作结果,即发送的字节数。 -
清理资源:
WSACloseEvent(evObj); closesocket(hSocket); WSACleanup();
关闭事件、套接字,并清理 Winsock 库。
示例(OverlappedRecv_win.c
):
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#define BUF_SIZE 1024
void ErrorHandling(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];
int 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 *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
代码解析:
-
初始化 WSA 和创建监听套接字:
WSAStartup(MAKEWORD(2, 2), &wsaData); hLisnSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
WSAStartup()
初始化 Winsock 库。WSASocket()
创建一个支持重叠 I/O 的套接字,使用WSA_FLAG_OVERLAPPED
标志。
-
绑定并监听端口:
bind(hLisnSock, (SOCKADDR*)&lisnAdr, sizeof(lisnAdr)); listen(hLisnSock, 5);
- 绑定套接字到指定端口,并开始监听进入的连接请求。
-
接受客户端连接:
hRecvSock = accept(hLisnSock, (SOCKADDR*)&recvAdr, &recvAdrSz);
accept()
阻塞地等待客户端的连接请求并返回一个新的套接字hRecvSock
,用于与客户端通信。
-
创建事件对象和重叠结构:
evObj = WSACreateEvent(); memset(&overlapped, 0, sizeof(overlapped)); overlapped.hEvent = evObj;
- 使用
WSACreateEvent()
创建一个事件对象,并将其赋值给overlapped.hEvent
以在 I/O 操作完成时触发该事件。
- 使用
-
异步接收数据:
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"); } }
WSARecv()
以非阻塞的方式异步接收数据。如果返回WSA_IO_PENDING
,说明接收操作尚未完成,将在后台进行。随后,程序通过WSAWaitForMultipleEvents()
函数等待事件evObj
的触发,表示接收操作完成。WSAGetOverlappedResult()
用来确认重叠 I/O 的结果并获取接收到的数据长度。
-
处理接收到的数据:
printf("Received message: %s \n", buf);
- 数据接收完成后,输出接收到的消息。
-
清理资源:
WSACloseEvent(evObj); closesocket(hRecvSock); closesocket(hLisnSock); WSACleanup();
- 关闭事件对象和套接字,并释放 Winsock 库资源。
(二)利用第七个参数:使用 Completion Routine 函数
Completion Routine(完成例程) 是一种处理异步 I/O 操作完成的方式。当使用 WSASend
或 WSARecv
等函数时,可以通过指定完成例程函数,使得在 I/O 操作完成时系统自动调用这个回调函数,而无需等待事件对象的触发。
I/O 完成时调用注册过的函数进行事后处理,这就是 Completion Routine 的运作方式。如果执行重要任务时突然调用 Completion Routine,则有可能破坏程序的正常执行流。因此操作系统通常会预先定义规则:
“只有请求 I/O 的线程处于 alertable wait 状态时才能调用 Completion Routine 函数!”
“alertable wait
状态” 是等待接收操作系统消息的线程状态。调用下列函数时进入 alertable wait
状态:
① WaitForSingleObjectEx
② WaitForMultipleObjectsEx
③ WSAWaitForMultipleEvents
④ SleepEx
启动 I/O 任务后,执行完紧急任务时可以调用上述任一函数验证 I/O 完成与否。此时操作系统知道线程进入 alertable wait
状态,如果有已完成的 I/O,则调用相应的 Completion Routine 函数。调用后,上述函数将全部返回 WAIT_IO_COMPLETION
,并开始执行接下来的程序。
下面的示例代码演示了如何使用 重叠 I/O 和 完成例程 处理异步网络 I/O 操作:
示例代码(CmplRoutinesRecv_win.c
):
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#define BUF_SIZE 1024
void CALLBACK CompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void ErrorHandling(char *message);
WSABUF dataBuf;
char buf[BUF_SIZE];
int recvBytes=0;
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hLisnSock, hRecvSock;
SOCKADDR_IN lisnAdr, recvAdr;
WSAOVERLAPPED overlapped;
WSAEVENT evObj;
int idx, recvAdrSz, 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);
if(hRecvSock==INVALID_SOCKET)
ErrorHandling("accept() error");
memset(&overlapped, 0, sizeof(overlapped));
dataBuf.len=BUF_SIZE;
dataBuf.buf=buf;
evObj=WSACreateEvent(); //Dummy event object
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 // If error occurred!
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 *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
代码解析:
-
初始化和创建套接字
WSADATA wsaData; SOCKET hLisnSock, hRecvSock; SOCKADDR_IN lisnAdr, recvAdr;
WSADATA wsaData
:用于初始化 WinSock。SOCKET hLisnSock
:监听套接字。SOCKET hRecvSock
:接收套接字。SOCKADDR_IN lisnAdr, recvAdr
:存储监听和接收的地址信息。
-
设置和绑定监听套接字
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");
- 使用
WSASocket
创建一个支持重叠 I/O 的套接字。 - 绑定监听套接字到指定的端口,并开始监听连接。
- 使用
-
接受连接
recvAdrSz=sizeof(recvAdr); hRecvSock=accept(hLisnSock, (SOCKADDR*)&recvAdr, &recvAdrSz); if(hRecvSock==INVALID_SOCKET) ErrorHandling("accept() error");
- 使用
accept
接受客户端的连接请求,并获取用于数据接收的套接字hRecvSock
。
- 使用
-
设置重叠结构和事件
memset(&overlapped, 0, sizeof(overlapped)); dataBuf.len=BUF_SIZE; dataBuf.buf=buf; evObj=WSACreateEvent(); // Dummy event object
- 初始化
WSAOVERLAPPED
结构体overlapped
和WSABUF
结构体dataBuf
。 - 创建一个事件对象
evObj
用于等待 I/O 操作完成。
- 初始化
-
开始异步接收
if(WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, CompRoutine) == SOCKET_ERROR) { if(WSAGetLastError() == WSA_IO_PENDING) puts("Background data receive"); }
- 调用
WSARecv
启动异步接收操作,传递完成例程CompRoutine
来处理数据接收完成后的操作。 - 如果
WSARecv
返回WSA_IO_PENDING
,说明 I/O 操作正在后台进行。
- 调用
-
等待异步 I/O 完成
idx = WSAWaitForMultipleEvents(1, &evObj, FALSE, WSA_INFINITE, TRUE); if(idx == WAIT_IO_COMPLETION) puts("Overlapped I/O Completed"); else // If error occurred! ErrorHandling("WSARecv() error");
- 使用
WSAWaitForMultipleEvents
等待事件对象evObj
变为 signaled,这意味着异步 I/O 操作已完成。 - 同时,此函数通过最后一个参数
TRUE
使调用它的线程进入 alertable wait 状态,使能够触发完成例程。
- 使用
-
处理完成例程
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); } }
- 当异步接收操作完成时,
CompRoutine
会被调用。 - 检查
dwError
是否为 0 来确认操作是否成功,并打印接收到的数据。
- 当异步接收操作完成时,
Completion Routine 函数原型如下:
void CALLBACK CompletionRoutine(
DWORD dwError, // 错误代码
DWORD dwNumberOfBytesTransferred, // 实际传输的字节数
LPWSAOVERLAPPED lpOverlapped, // 指向 WSAOVERLAPPED 结构的指针
DWORD dwFlags // 操作相关的标志
);
-
dwError
: 表示完成 I/O 操作的结果状态。如果操作成功,该值为0
,否则为错误代码。 -
dwNumberOfBytesTransferred
: 实际传输的字节数。例如,如果是WSARecv
,则表示接收到的数据量。 -
lpOverlapped
: 指向与该异步操作关联的WSAOVERLAPPED
结构的指针。 -
dwFlags
: 表示 I/O 操作完成时的一些附加信息,通常用于表示操作的具体状态或标志(例如是否还有数据可读等)。
返回值类型 void 后插入的 CALLBACK 关键字与 main 函数中声明的关键字 WINAPI 相同,都是用于声明函数的调用规范,所以定义 Completion Routine 时必须添加。
问答
(一)异步通知 I/O 模型与重叠 I/O 模型在异步处理方面有哪些区别?
-
I/O 操作的发起方式:
- 异步通知 I/O:通过
WSAEventSelect
关联事件对象,操作的完成通过事件通知。 - 重叠 I/O:通过
WSASend
、WSARecv
等函数进行异步操作,返回时并不等待 I/O 完成,操作完成通过WSAOVERLAPPED
和事件或完成例程通知。
- 异步通知 I/O:通过
-
通知机制:
- 异步通知 I/O:依赖事件对象,应用程序主动等待事件对象变为 signaled 状态。
- 重叠 I/O:不仅可以使用事件对象,还可以通过完成例程自动处理 I/O 结果,减少主动轮询。
-
灵活性和效率:
- 异步通知 I/O:较为简单,但在大量 I/O 操作时可能会遇到性能瓶颈。
- 重叠 I/O:更加灵活且高效,特别是在结合 IOCP 时,可以处理大量并发 I/O 操作,极大提高性能。
-
多线程支持:
- 异步通知 I/O:通常是在单线程中处理,线程主动轮询事件。
- 重叠 I/O:结合 IOCP 时,可以将操作分配给多个线程,提升并发能力。
(二)请分析非阻塞 I/O、异步 I/O、重叠 I/O 之间的关系。
-
非阻塞 I/O 与异步 I/O:
- 共同点:两者都不会阻塞调用线程,都允许应用程序继续处理其他任务。
- 区别:非阻塞 I/O 需要应用程序主动轮询(通过
select
、poll
或epoll
等机制)来检查操作的完成情况;异步 I/O 不需要主动轮询,而是通过系统的通知机制(如事件或完成例程)处理 I/O 操作的完成。
-
异步 I/O 与重叠 I/O:
- 共同点:重叠 I/O 是 Windows 系统中异步 I/O 的一种实现形式,允许 I/O 操作立即返回,后台继续处理,并通过事件或完成例程通知操作的完成。
- 区别:重叠 I/O 特定于 Windows 系统,使用
WSAOVERLAPPED
结构,结合事件通知和完成例程的方式;而异步 I/O 是更广泛的概念,在 Windows 和类 Unix 系统中都可以实现(Linux 中的aio_read
等)。
-
非阻塞 I/O 与重叠 I/O:
- 共同点:都允许操作立即返回,而不阻塞当前线程。
- 区别:非阻塞 I/O 依赖应用程序轮询操作的状态,重叠 I/O 则通过系统事件或完成例程通知,避免了频繁轮询的开销。
(三)调用 WSASend
函数后如何验证 I/O 是否进入 pending 状态?
-
当
WSASend
返回SOCKET_ERROR
时,使用WSAGetLastError()
获取错误码。 -
如果错误码为
WSA_IO_PENDING
,则表示 I/O 操作已经成功排队并进入 pending 状态,稍后系统会通过事件通知或完成例程通知其完成。 -
如果
WSASend
立即成功(返回 0),则操作已经完成,数据已经发送。
(四)指出下面代码存在的问题,并给出解决方案
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);
}
- 未处理
accept
和WSARecv
的错误
在每次调用 accept
和 WSARecv
后,程序没有检查返回值以处理潜在的错误。特别是,WSARecv
可能会返回 SOCKET_ERROR
,并且 WSAGetLastError()
可能返回 WSA_IO_PENDING
,表明接收操作挂起。应当在每次调用后检查返回值并处理错误。
WSARecv
可能会导致高 CPU 占用
由于代码在循环中没有任何等待或延迟,可能会导致过多的 WSARecv
调用而没有适当地处理每次接收的完成情况。这可能导致 CPU 占用率过高,因为每次循环都会立即处理下一次 accept
。
- 事件对象泄漏
每次循环都会调用 WSACreateEvent
创建一个事件对象,但没有在后续的代码中关闭这些事件对象。如果没有适当地关闭,可能会导致事件对象的资源泄漏。因此,在每次接收完成或操作完成后,应该调用 WSACloseEvent
来释放事件对象。
- 没有处理完成的异步操作
虽然设置了 overlapped
结构体,但是没有代码来等待操作的完成或处理异步操作的结果。在异步 I/O 模型中,完成端口或者事件通知机制需要跟踪每个 I/O 操作的完成情况。
recvBytes
和flags
未初始化
这两个变量在每次 WSARecv
调用前应该被初始化。
- 改进后代码:
while (1)
{
// 接受新的客户端连接
hRecvSock = accept(hLisnSock, (SOCKADDR*)&recvAdr, &recvAdrSz);
if (hRecvSock == INVALID_SOCKET)
{
printf("accept() failed with error: %d\n", WSAGetLastError());
continue;
}
// 创建事件对象
evObj = WSACreateEvent();
if (evObj == WSA_INVALID_EVENT)
{
printf("WSACreateEvent() failed with error: %d\n", WSAGetLastError());
closesocket(hRecvSock);
continue;
}
// 清空 overlapped 结构体
memset(&overlapped, 0, sizeof(overlapped));
overlapped.hEvent = evObj;
// 设置缓冲区
dataBuf.len = BUF_SIZE;
dataBuf.buf = buf;
// 初始化 flags 和 recvBytes
flags = 0;
recvBytes = 0;
// 调用 WSARecv
if (WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, NULL) == SOCKET_ERROR)
{
int err = WSAGetLastError();
if (err != WSA_IO_PENDING)
{
// 如果不是 WSA_IO_PENDING 错误,输出错误信息并清理资源
printf("WSARecv() failed with error: %d\n", err);
WSACloseEvent(evObj);
closesocket(hRecvSock);
continue;
}
// WSA_IO_PENDING 表示操作挂起,稍后会通过事件对象通知完成
printf("WSARecv operation is pending...\n");
// 等待事件完成(此处可根据设计使用事件等待或其他机制)
WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
// 获取完成结果
if (WSAGetOverlappedResult(hRecvSock, &overlapped, &recvBytes, FALSE, &flags))
{
printf("Received %d bytes: %s\n", recvBytes, buf);
}
else
{
printf("WSAGetOverlappedResult() failed with error: %d\n", WSAGetLastError());
}
}
else
{
// WSARecv 立即成功
printf("WSARecv completed immediately, received %d bytes: %s\n", recvBytes, buf);
}
// 清理资源
WSACloseEvent(evObj);
closesocket(hRecvSock);
}