TCP/IP 网络编程(二十一)---重叠 I/O 模型

23 阅读19分钟

理解重叠 I/O 模型

重叠 I/O(Overlapped I/O) 这个名称来自于它允许多个 I/O 操作 "重叠" 执行,而不必等待前一个操作完成。这种方式让应用程序可以同时发起多个 I/O 操作,而不阻塞调用线程。

重叠 I/O 和异步 I/O 的关系:

  1. 重叠 I/O 是 Windows 实现异步 I/O 的一种方式。在 Windows 系统中,重叠 I/O 是异步 I/O 的一种具体实现机制,允许应用程序在发起 I/O 操作时不会阻塞线程。操作可以在后台执行,完成后通知应用程序。
  2. 异步 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

调用 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                 // 指向标志的指针,用于接收操作状态
);
  1. s:

    • 关联重叠操作的套接字句柄,即异步操作所使用的套接字。
  2. lpOverlapped:

    • 指向之前传递给异步 I/O 操作(如 WSASendWSARecv)的 WSAOVERLAPPED 结构的指针。该结构保存了异步操作的状态。
  3. lpcbTransfer:

    • 指向一个 DWORD 变量的指针,用于存储操作完成时实际传输的字节数(如发送或接收的字节数)。
  4. fWait:

    • 如果设置为 TRUEWSAGetOverlappedResult 会等待操作完成;如果为 FALSE,函数立即返回,且不会等待操作完成。
  5. lpdwFlags:

    • 指向一个 DWORD 变量的指针,用于接收操作的状态标志。这些标志可能包括 MSG_PARTIAL,用于指示是否接收到部分消息。
  6. 返回值:

    • 如果函数成功,返回值为非零 (TRUE),表示异步 I/O 操作已经完成,并且 lpcbTransferlpdwFlags 被正确填充。
    • 如果函数失败,返回值为 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 的完成并获取结果

① 利用 WSASendWSARecv 函数的第六个参数,基于事件对象。

② 利用 WSASendWSARecv 函数的第七个参数,基于 Completion Routine。

(一)利用第六个参数

上面介绍过 WSASendWSARecv 函数的第六个参数——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 操作完成的方式。当使用 WSASendWSARecv 等函数时,可以通过指定完成例程函数,使得在 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 结构体 overlappedWSABUF 结构体 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:通过 WSASendWSARecv 等函数进行异步操作,返回时并不等待 I/O 完成,操作完成通过 WSAOVERLAPPED 和事件或完成例程通知。
  • 通知机制

    • 异步通知 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 需要应用程序主动轮询(通过 selectpollepoll 等机制)来检查操作的完成情况;异步 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);
}
  1. 未处理 acceptWSARecv 的错误

在每次调用 acceptWSARecv 后,程序没有检查返回值以处理潜在的错误。特别是,WSARecv 可能会返回 SOCKET_ERROR,并且 WSAGetLastError() 可能返回 WSA_IO_PENDING,表明接收操作挂起。应当在每次调用后检查返回值并处理错误。

  1. WSARecv 可能会导致高 CPU 占用

由于代码在循环中没有任何等待或延迟,可能会导致过多的 WSARecv 调用而没有适当地处理每次接收的完成情况。这可能导致 CPU 占用率过高,因为每次循环都会立即处理下一次 accept

  1. 事件对象泄漏

每次循环都会调用 WSACreateEvent 创建一个事件对象,但没有在后续的代码中关闭这些事件对象。如果没有适当地关闭,可能会导致事件对象的资源泄漏。因此,在每次接收完成或操作完成后,应该调用 WSACloseEvent 来释放事件对象。

  1. 没有处理完成的异步操作

虽然设置了 overlapped 结构体,但是没有代码来等待操作的完成或处理异步操作的结果。在异步 I/O 模型中,完成端口或者事件通知机制需要跟踪每个 I/O 操作的完成情况。

  1. recvBytesflags 未初始化

这两个变量在每次 WSARecv 调用前应该被初始化。

  1. 改进后代码:
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);
}