TCP/IP 网络编程(二十二)---IOCP

90 阅读15分钟

通过重叠 I/O 理解 IOCP

  • 重叠 I/O 提供了异步 I/O 操作的基本支持机制。

  • IOCP 是在重叠 I/O 的基础上构建的,提供了高效的 I/O 操作管理和调度机制,特别适合需要处理大量并发连接的场景。

以纯重叠 I/O 方式实现回声服务器端

事实上,因为有 IOCP 模型,所以很少有人只用重叠 I/O 实现服务器端,但为了正确理解 IOCP,应当尝试用纯重叠 I/O 方式实现服务器端。

之前我们只介绍了执行重叠 I/O 的 SenderReceiver,要想实现基于重叠 I/O 的服务器端,必须具备非阻塞套接字。下面介绍创建非阻塞模式套接字的方法,在 Windows 中通过如下函数调用将套接字属性改为非阻塞模式:

SOCKET hLisnSock;
int mode = 1;
...
hListSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
ioctlsocket(hLisnSock, FIONBIO, &mode);

上述代码中调用的 ioctlsocket 函数负责控制套接字 I/O 方式,其调用具有如下含义:

“将 hLisnSock 句柄引用的套接字 I/O 模式(FIONBIO)改为变量 mode 中指定的形式。”

也就是说,FIONBIO 是用于更改套接字 I/O 模式的选项,该函数的第三个参数中换入的变量中若存有0,则说明套接字是阻塞模式的;如果存有非0值,则说明已将套接字模式改为非阻塞模式。改为非阻塞模式后,除了以非阻塞模式进行 I/O 外,还具有如下特点。

  • 如果在没有客户端连接请求的状态下调用 accept 函数,将直接返回 INVALID_SOCKET。调用 WSAGetLastError 函数时返回 WSAEWOULDBLOCK
  • 调用 accept 函数时创建的套接字同样具有非阻塞属性。

因此,针对非阻塞套接字调用 accept 函数并返回 INVALID_SOCKET 时,应该通过 WSAGetLastError 函数确认返回 INVALID_SOCKET 的理由,再进行适当处理。

下面是以纯重叠 I/O 方式实现回声服务器端的代码:

(一)服务器端代码

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#define BUF_SIZE 1024
void CALLBACK ReadCompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void CALLBACK WriteCompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void ErrorHandling(char *message);

typedef struct
{
	SOCKET hClntSock;
	char buf[BUF_SIZE];
	WSABUF wsaBuf;
} PER_IO_DATA, *LPPER_IO_DATA;

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hLisnSock, hRecvSock;	
	SOCKADDR_IN lisnAdr, recvAdr;
	LPWSAOVERLAPPED lpOvLp;
	DWORD recvBytes;
	LPPER_IO_DATA hbInfo;
	int mode=1, recvAdrSz, flagInfo=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);
	ioctlsocket(hLisnSock, FIONBIO, &mode);   // for non-blocking socket

	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);    

	while(1)
	{
		SleepEx(100, TRUE);    // for alertable wait state
		hRecvSock=accept(hLisnSock, (SOCKADDR*)&recvAdr,&recvAdrSz);
		if(hRecvSock==INVALID_SOCKET)
		{
			if(WSAGetLastError()==WSAEWOULDBLOCK)
				continue;
			else
				ErrorHandling("accept() error");
		}
		puts("Client connected.....");

		lpOvLp=(LPWSAOVERLAPPED)malloc(sizeof(WSAOVERLAPPED));
		memset(lpOvLp, 0, sizeof(WSAOVERLAPPED));

		hbInfo=(LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
		hbInfo->hClntSock=(DWORD)hRecvSock;
		(hbInfo->wsaBuf).buf=hbInfo->buf;
		(hbInfo->wsaBuf).len=BUF_SIZE;

		lpOvLp->hEvent=(HANDLE)hbInfo;
		WSARecv(hRecvSock, &(hbInfo->wsaBuf), 
			1, &recvBytes, &flagInfo, lpOvLp, ReadCompRoutine);
	}
	closesocket(hRecvSock);
	closesocket(hLisnSock);
	WSACleanup();
	return 0;
}

void CALLBACK ReadCompRoutine(
	DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
	LPPER_IO_DATA hbInfo=(LPPER_IO_DATA)(lpOverlapped->hEvent);
	SOCKET hSock=hbInfo->hClntSock;
	LPWSABUF bufInfo=&(hbInfo->wsaBuf);
	DWORD sentBytes;

	if(szRecvBytes==0)
	{
		closesocket(hSock);
		free(lpOverlapped->hEvent); free(lpOverlapped);
		puts("Client disconnected.....");
	}
	else    // echo!
	{
		bufInfo->len=szRecvBytes;
		WSASend(hSock, bufInfo, 1, &sentBytes, 0, lpOverlapped, WriteCompRoutine);
	}
}

void CALLBACK WriteCompRoutine(
	DWORD dwError, DWORD szSendBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
	LPPER_IO_DATA hbInfo=(LPPER_IO_DATA)(lpOverlapped->hEvent);
	SOCKET hSock=hbInfo->hClntSock;
	LPWSABUF bufInfo=&(hbInfo->wsaBuf);
	DWORD recvBytes;
	int flagInfo=0;
	WSARecv(hSock, bufInfo,
		1, &recvBytes, &flagInfo, lpOverlapped, ReadCompRoutine);
}

void ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

代码解析:

  1. 服务器基本架构

服务器使用了 WSASocket 创建一个支持重叠 I/O 的套接字(WSA_FLAG_OVERLAPPED),并使用 ioctlsocket 将其设置为非阻塞模式(FIONBIO),这样 accept 操作不会阻塞主线程。

  1. 事件通知与 alertable wait 状态
SleepEx(100, TRUE); // alertable wait state

SleepEx 函数将线程置于一个可以接受完成例程调用的 alertable wait 状态。TRUE 参数表示线程可以在该状态下接收 I/O 完成例程的通知。当线程处于这种状态时,如果任何异步操作完成,Windows 会调用对应的完成例程,而无需等待事件手动触发。

  1. 异步接收数据:WSARecv

当有客户端连接时,代码使用 WSARecv 异步接收数据:

WSARecv(hRecvSock, &(hbInfo->wsaBuf), 1, &recvBytes, &flagInfo, lpOvLp, ReadCompRoutine);

这里指定了 ReadCompRoutine 作为完成例程,因此当数据接收完成时,系统会自动调用 ReadCompRoutine 函数。

  1. ReadCompRoutine 完成例程
void CALLBACK ReadCompRoutine(
    DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
    // 获取保存 I/O 操作信息的结构体
    LPPER_IO_DATA hbInfo=(LPPER_IO_DATA)(lpOverlapped->hEvent);
    SOCKET hSock=hbInfo->hClntSock;
    LPWSABUF bufInfo=&(hbInfo->wsaBuf);
    DWORD sentBytes;

    if(szRecvBytes == 0)  // 客户端断开连接
    {
        closesocket(hSock);
        free(lpOverlapped->hEvent); free(lpOverlapped);
        puts("Client disconnected.....");
    }
    else  // 回显接收到的数据
    {
        bufInfo->len = szRecvBytes;
        WSASend(hSock, bufInfo, 1, &sentBytes, 0, lpOverlapped, WriteCompRoutine);
    }
}

这个完成例程在接收到数据时被调用,处理如下:

  • 如果接收的字节数为 0,意味着客户端断开连接,关闭套接字并释放内存。
  • 如果接收到了有效数据,程序调用 WSASend 将数据回显给客户端,并将 WriteCompRoutine 设为发送操作的完成例程。
  1. WriteCompRoutine 完成例程

当发送操作完成时,WriteCompRoutine 会被调用:

void CALLBACK WriteCompRoutine(
    DWORD dwError, DWORD szSendBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
    LPPER_IO_DATA hbInfo = (LPPER_IO_DATA)(lpOverlapped->hEvent);
    SOCKET hSock = hbInfo->hClntSock;
    LPWSABUF bufInfo = &(hbInfo->wsaBuf);
    DWORD recvBytes;
    int flagInfo = 0;

    // 继续异步接收数据
    WSARecv(hSock, bufInfo, 1, &recvBytes, &flagInfo, lpOverlapped, ReadCompRoutine);
}

发送数据后,它重新调用 WSARecv 来继续接收客户端的数据,并将 ReadCompRoutine 设置为完成例程。

  1. 总结
  • 非阻塞套接字alertable wait:通过 ioctlsocket 将套接字设为非阻塞,使用 SleepEx 使线程进入 alertable wait 状态,这样线程既不会阻塞在等待事件的操作上,也能在 I/O 操作完成时自动调用完成例程。
  • 完成例程处理 I/O:使用完成例程 (ReadCompRoutineWriteCompRoutine) 处理 I/O 操作的完成。每次 I/O 操作完成时,Windows 会自动调用对应的例程,这大大简化了应用程序对 I/O 结果的处理。

(二)客户端代码(StableEchoClnt_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 hSocket;
	SOCKADDR_IN servAdr;
	char message[BUF_SIZE];
	int strLen, readLen;

	if(argc!=3) {
		printf("Usage: %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
		ErrorHandling("WSAStartup() error!"); 

	hSocket=socket(PF_INET, SOCK_STREAM, 0);   
	if(hSocket==INVALID_SOCKET)
		ErrorHandling("socket() error");
	
	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family=AF_INET;
	servAdr.sin_addr.s_addr=inet_addr(argv[1]);
	servAdr.sin_port=htons(atoi(argv[2]));
	
	if(connect(hSocket, (SOCKADDR*)&servAdr, sizeof(servAdr))==SOCKET_ERROR)
		ErrorHandling("connect() error!");
	else
		puts("Connected...........");
	
	while(1) 
	{
		fputs("Input message(Q to quit): ", stdout);
		fgets(message, BUF_SIZE, stdin);
		if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
			break;

		strLen=strlen(message);
		send(hSocket, message, strLen, 0);

		readLen=0;
		while(1)
		{
			readLen+=recv(hSocket, &message[readLen], BUF_SIZE-1, 0);
			if(readLen>=strLen)
				break;
		}
		message[strLen]=0;
		printf("Message from server: %s", message);
	}
	
	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

从重叠 I/O 模型到 IOCP 模型

下面分析重叠 I/O 模型回声服务器端的缺点:

“重复调用非阻塞模式的 accept 函数和以进入 alertable wait 状态为目的的 SleepEx 函数将影响性能。”

① 非阻塞 accept 函数的影响

非阻塞模式意味着当调用 accept 函数时,如果没有新的客户端连接请求,它不会阻塞等待,而是立即返回一个 INVALID_SOCKET,并设置错误码为 WSAEWOULDBLOCK。你代码中的逻辑是通过循环反复调用 accept,判断是否有新的客户端连接。

  • 性能开销:由于 accept 是非阻塞的,它每次返回后,主线程会立刻再进入下一次 accept 调用。这样就会在没有客户端连接的情况下,主线程频繁调用 accept 函数,浪费 CPU 资源,增加不必要的系统调用。
  • 高 CPU 占用:当没有连接时,accept 函数会不断失败返回,使得主线程忙碌于重复调用,而没有执行任何有意义的工作,这会导致 CPU 占用率上升。

SleepEx 函数的影响

SleepEx 函数用于让当前线程进入睡眠并进入 alertable wait 状态。在你的代码中,你调用了 SleepEx(100, TRUE),这会使线程进入 alertable wait 状态 100 毫秒。

  • 延迟增加SleepEx 的存在意味着即便有新的 I/O 完成,主线程也必须等到睡眠时间结束(即 100 毫秒)才能处理新连接或执行其他任务,这会导致一些 I/O 操作被延迟处理。
  • 潜在资源浪费:在一个快速响应的服务器应用中,依赖 SleepEx 定时器会导致线程的空闲等待时间增加。虽然它能降低 CPU 占用,但同时也降低了系统响应的及时性。

③ 频繁轮询的低效性

结合上面的两点,你的程序实际上在忙轮询,即主线程在不停地重复调用 accept,但又使用 SleepEx 来让线程在没有任务时暂时挂起。这两者结合起来会导致以下问题:

  • 低效的资源利用:频繁的 accept 调用意味着主线程不断尝试检查是否有新的连接,这是一种轮询(polling)操作,浪费 CPU 资源。而 SleepEx 又引入了延迟,导致系统响应不够及时。
  • 较差的扩展性:对于高并发服务器,这种模式会显著影响扩展性。因为不断轮询和等待状态会限制 CPU 和 I/O 资源的高效使用,不能充分利用现代多核处理器和 I/O 完成端口等机制带来的并发处理优势。

上述属于重叠 I/O 结构固有的缺陷,可以考虑如下方法:

“让 main 线程(在 main 函数内部)调用 accept 函数,再单独创建1个线程负责客户端 I/O。”

这就是 IOCP 中采用的服务器端模型。换言之,IOCP 将创建专用的 I/O 线程,该线程负责与所有客户端进行 I/O。

分阶段实现 IOCP 程序

(一)创建 “完成端口”

IOCP 中已完成的 I/O 信息将注册到完成端口对象(Completion Port,简称 CP 对象),即:

“该套接字的 I/O 完成时,请把状态信息注册到指定 CP 对象。”

完成端口的创建通过下面的函数完成:

#include <windows.h>

HANDLE CreateIoCompletionPort(
  HANDLE    FileHandle,
  HANDLE    ExistingCompletionPort,
  ULONG_PTR CompletionKey,
  DWORD     NumberOfConcurrentThreads
);
  • FileHandle:

    • 要与 I/O 完成端口关联的文件、套接字句柄。如果该值为 INVALID_HANDLE_VALUE,则会创建一个新的完成端口,但不会立即将文件或套接字关联到这个完成端口。
  • ExistingCompletionPort:

    • 指定现有的完成端口句柄。通常在第一次调用时,该值NULL,表示创建新的完成端口;以后可以传入现有的完成端口来与更多文件或套接字关联。
  • CompletionKey:

    • 关联到文件或套接字的完成键(通常是用于标识客户端的指针或标识符)。当异步 I/O 操作完成时,系统会通过 GetQueuedCompletionStatus 返回这个键值,让你知道哪个 I/O 操作完成。创建 CP 对象时传递0。
  • NumberOfConcurrentThreads:

    • 指定同时可以执行的最大工作线程数。通常设置为 0 或系统中的 CPU 核心数量,以让系统自动管理并发线程数**。**

以创建 CP 对象为目的的调用上述函数时,只有最后一个参数才真正具有含义,其指定 CP 对象用于处理 I/O 的线程数。

(二)连接完成端口对象和套接字

创建 CP 对象后,就要将该对象连接到套接字,只有这样才能使已完成的套接字 I/O 信息注册到 CP 对象。完成此功能的函数仍然是 CreateIoCompletionPort。上面已经介绍,这里不再赘述。

#include <windows.h>

HANDLE CreateIoCompletionPort(
  HANDLE    FileHandle,
  HANDLE    ExistingCompletionPort,
  ULONG_PTR CompletionKey,
  DWORD     NumberOfConcurrentThreads
);

FileHandle 句柄指向的套接字和 ExistingCompletionPort 指向的 CP 对象相连。函数调用方式如下:

HANDLE hCpObject;
SOCKET hSock;
...
CreateIoCompletionPort((HANDLE)hSock, hCpObject, (DWORD)ioInfo, 0);

调用 CreateIoCompletionPort 函数后,只要针对 hSock 的 I/O 完成,相关信息就将注册到 hCpObject 指向的 CP 对象。

(三)确认完成端口 已完成的 I/O 和线程的 I/O 处理

下面的函数用于确认 CP 中注册的已完成的 I/O:

#include <windows.h>

BOOL GetQueuedCompletionStatus(
  HANDLE       CompletionPort,
  LPDWORD      lpNumberOfBytes,
  PULONG_PTR   lpCompletionKey,
  LPOVERLAPPED *lpOverlapped,
  DWORD        dwMilliseconds
);
  • CompletionPort:

    • 这是 CreateIoCompletionPort 返回的完成端口句柄,表示当前操作的 I/O 完成端口。
  • lpNumberOfBytes:

    • 一个指向 DWORD 的指针。函数返回时,该指针指向的是完成 I/O 操作传输的字节数。
  • lpCompletionKey:

    • 一个指向 ULONG_PTR 的指针。它返回关联的完成键(CompletionKey),这是在调用 CreateIoCompletionPort 关联句柄时传入的值,通常用于标识哪个客户端或哪个套接字完成了操作。
  • lpOverlapped:

    • 一个指向 OVERLAPPED 结构的指针。该结构包含与已完成的异步 I/O 操作相关的信息。你可以通过它来确定哪个 I/O 操作完成了。
  • dwMilliseconds:

    • 等待超时的时间(毫秒)。如果设置为 INFINITE,则函数将无限期等待,直到有完成的 I/O 事件。

要点:

“通过 GetQueuedCompletionStatus 函数的第三个参数得到的是以连接套接字和 CP 对象为目的而调用的 CreateIoCompletionPort 函数的第三个参数值。”

“通过 GetQueuedCompletionStatus 函数的第四个参数得到的是调用 WSASendWSARecv 函数时传入的 WSAOVERLAPPED 结构体变量的地址值。”

IOCP 中将创建全职 I/O 线程,由该线程针对所有客户端进行 I/O。程序员自行创建调用 WSASendWSARecv 等 I/O 函数的线程,该线程为了确认 I/O 的完成会调用 GetQueuedCompletionStatus 函数。

(四)实现基于 IOCP 的回声服务器端

#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <winsock2.h>
#include <windows.h>

#define BUF_SIZE 100
#define READ	3
#define	WRITE	5

typedef struct    // socket info
{
	SOCKET hClntSock;
	SOCKADDR_IN clntAdr;
} PER_HANDLE_DATA, *LPPER_HANDLE_DATA;

typedef struct    // buffer info
{
	OVERLAPPED overlapped;
	WSABUF wsaBuf;
	char buffer[BUF_SIZE];
	int rwMode;    // READ or WRITE
} PER_IO_DATA, *LPPER_IO_DATA;

DWORD WINAPI EchoThreadMain(LPVOID CompletionPortIO);
void ErrorHandling(char *message);

int main(int argc, char* argv[])
{
	WSADATA	wsaData;
	HANDLE hComPort;	
	SYSTEM_INFO sysInfo;
	LPPER_IO_DATA ioInfo;
	LPPER_HANDLE_DATA handleInfo;

	SOCKET hServSock;
	SOCKADDR_IN servAdr;
	int recvBytes, i, flags=0;

	if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error!"); 

	hComPort=CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
	GetSystemInfo(&sysInfo);

	for(i=0; i<sysInfo.dwNumberOfProcessors; i++)
		_beginthreadex(NULL, 0, EchoThreadMain, (LPVOID)hComPort, 0, NULL);

	hServSock=WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family=AF_INET;
	servAdr.sin_addr.s_addr=htonl(INADDR_ANY);
	servAdr.sin_port=htons(atoi(argv[1]));

	bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr));
	listen(hServSock, 5);
	
	while(1)
	{	
		SOCKET hClntSock;
		SOCKADDR_IN clntAdr;		
		int addrLen=sizeof(clntAdr);
		
		hClntSock=accept(hServSock, (SOCKADDR*)&clntAdr, &addrLen);		  

		handleInfo=(LPPER_HANDLE_DATA)malloc(sizeof(PER_HANDLE_DATA));		
		handleInfo->hClntSock=hClntSock;
		memcpy(&(handleInfo->clntAdr), &clntAdr, addrLen);

		CreateIoCompletionPort((HANDLE)hClntSock, hComPort, (DWORD)handleInfo, 0);
		
		ioInfo=(LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
		memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));		
		ioInfo->wsaBuf.len=BUF_SIZE;
		ioInfo->wsaBuf.buf=ioInfo->buffer;
		ioInfo->rwMode=READ;

		WSARecv(handleInfo->hClntSock,	&(ioInfo->wsaBuf),	
			1, &recvBytes, &flags, &(ioInfo->overlapped), NULL);			
	}
	return 0;
}

DWORD WINAPI EchoThreadMain(LPVOID pComPort)
{
	HANDLE hComPort=(HANDLE)pComPort;
	SOCKET sock;
	DWORD bytesTrans;
	LPPER_HANDLE_DATA handleInfo;
	LPPER_IO_DATA ioInfo;
	DWORD flags=0;
	
	while(1)
	{ 
		GetQueuedCompletionStatus(hComPort, &bytesTrans, 
			(LPDWORD)&handleInfo, (LPOVERLAPPED*)&ioInfo, INFINITE);
		sock=handleInfo->hClntSock;

		if(ioInfo->rwMode==READ)
		{
			puts("message received!");
			if(bytesTrans==0)    // EOF 
			{
				closesocket(sock);
				free(handleInfo); free(ioInfo);
				continue;		
			}

			memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));			
			ioInfo->wsaBuf.len=bytesTrans;
			ioInfo->rwMode=WRITE;
			WSASend(sock, &(ioInfo->wsaBuf), 
				1, NULL, 0, &(ioInfo->overlapped), NULL);

			ioInfo=(LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
			memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));
			ioInfo->wsaBuf.len=BUF_SIZE;
			ioInfo->wsaBuf.buf=ioInfo->buffer;
			ioInfo->rwMode=READ;
			WSARecv(sock, &(ioInfo->wsaBuf), 
				1, NULL, &flags, &(ioInfo->overlapped), NULL);
		}
		else
		{
			puts("message sent!");
			free(ioInfo);
		}
	}
	return 0;
}

void ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

代码解析:

  1. I/O 完成端口的创建
hComPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

CreateIoCompletionPort 函数用于创建一个新的 I/O 完成端口。传递 INVALID_HANDLE_VALUE 表示这是一个新的完成端口对象。

  1. 创建工作线程
for (i = 0; i < sysInfo.dwNumberOfProcessors; i++)
    _beginthreadex(NULL, 0, EchoThreadMain, (LPVOID)hComPort, 0, NULL);

这里使用了 _beginthreadex 创建多个工作线程,每个线程都将等待 I/O 完成事件。线程的数量等于处理器的核心数,目的是最大化 CPU 的使用效率。

  1. 套接字与完成端口关联
CreateIoCompletionPort((HANDLE)hClntSock, hComPort, (DWORD)handleInfo, 0);

当服务器接受客户端的连接后,将客户端的套接字 hClntSock 与之前创建的完成端口 hComPort 关联。通过这样做,当这个套接字上的 I/O 操作完成时,系统会将完成事件排队到完成端口。

  1. 提交异步 I/O 请求
复制代码
WSARecv(handleInfo->hClntSock, &(ioInfo->wsaBuf), 1, &recvBytes, &flags, &(ioInfo->overlapped), NULL);

一旦客户端连接成功,服务器会调用 WSARecv 函数提交异步读取操作。这个函数不会阻塞,读取完成后,I/O 完成事件将通过完成端口通知工作线程。

  1. 工作线程处理 I/O 完成事件
GetQueuedCompletionStatus(hComPort, &bytesTrans, (LPDWORD)&handleInfo, (LPOVERLAPPED*)&ioInfo, INFINITE);

在每个工作线程中,调用 GetQueuedCompletionStatus 函数等待 I/O 完成事件。当某个 I/O 操作(比如 WSARecvWSASend)完成时,这个函数会返回,线程可以根据事件的结果处理相应的逻辑。

  1. 处理读取和写入事件

当 I/O 完成后,线程会根据 ioInfo->rwMode 来判断是读取操作还是写入操作:

  • 读取操作 (READ)

    • 服务器收到消息后,将其打印出来,并准备将消息回送给客户端。
    • 调用 WSASend 函数发送消息,异步提交发送操作。
    • 再次调用 WSARecv 函数准备下一次接收操作。
  • 写入操作 (WRITE)

    • 当消息成功发送后,释放内存资源。
  1. 内存管理

服务器为每个客户端连接分配了两种数据结构:

  • PER_HANDLE_DATA: 保存每个客户端的套接字和地址信息。
  • PER_IO_DATA: 用于保存异步 I/O 操作的缓冲区和操作模式(读取或写入)。

每个 I/O 操作完成后,服务器都会相应地释放不再需要的内存资源。

上述示例可结合前面的 StableEchoClnt_win.c 运行。