通过重叠 I/O 理解 IOCP
-
重叠 I/O 提供了异步 I/O 操作的基本支持机制。
-
IOCP 是在重叠 I/O 的基础上构建的,提供了高效的 I/O 操作管理和调度机制,特别适合需要处理大量并发连接的场景。
以纯重叠 I/O 方式实现回声服务器端
事实上,因为有 IOCP 模型,所以很少有人只用重叠 I/O 实现服务器端,但为了正确理解 IOCP,应当尝试用纯重叠 I/O 方式实现服务器端。
之前我们只介绍了执行重叠 I/O 的 Sender
和 Receiver
,要想实现基于重叠 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);
}
代码解析:
- 服务器基本架构
服务器使用了 WSASocket 创建一个支持重叠 I/O 的套接字(WSA_FLAG_OVERLAPPED
),并使用 ioctlsocket 将其设置为非阻塞模式(FIONBIO
),这样 accept
操作不会阻塞主线程。
- 事件通知与 alertable wait 状态
SleepEx(100, TRUE); // alertable wait state
SleepEx
函数将线程置于一个可以接受完成例程调用的 alertable wait 状态。TRUE
参数表示线程可以在该状态下接收 I/O 完成例程的通知。当线程处于这种状态时,如果任何异步操作完成,Windows 会调用对应的完成例程,而无需等待事件手动触发。
- 异步接收数据:WSARecv
当有客户端连接时,代码使用 WSARecv 异步接收数据:
WSARecv(hRecvSock, &(hbInfo->wsaBuf), 1, &recvBytes, &flagInfo, lpOvLp, ReadCompRoutine);
这里指定了 ReadCompRoutine
作为完成例程,因此当数据接收完成时,系统会自动调用 ReadCompRoutine
函数。
- 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
设为发送操作的完成例程。
- 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
设置为完成例程。
- 总结
- 非阻塞套接字 和 alertable wait:通过
ioctlsocket
将套接字设为非阻塞,使用SleepEx
使线程进入alertable wait
状态,这样线程既不会阻塞在等待事件的操作上,也能在 I/O 操作完成时自动调用完成例程。 - 完成例程处理 I/O:使用完成例程 (
ReadCompRoutine
和WriteCompRoutine
) 处理 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
,则会创建一个新的完成端口,但不会立即将文件或套接字关联到这个完成端口。
- 要与 I/O 完成端口关联的文件、套接字句柄。如果该值为
-
ExistingCompletionPort:
- 指定现有的完成端口句柄。通常在第一次调用时,该值为
NULL
,表示创建新的完成端口;以后可以传入现有的完成端口来与更多文件或套接字关联。
- 指定现有的完成端口句柄。通常在第一次调用时,该值为
-
CompletionKey:
- 关联到文件或套接字的完成键(通常是用于标识客户端的指针或标识符)。当异步 I/O 操作完成时,系统会通过
GetQueuedCompletionStatus
返回这个键值,让你知道哪个 I/O 操作完成。创建 CP 对象时传递0。
- 关联到文件或套接字的完成键(通常是用于标识客户端的指针或标识符)。当异步 I/O 操作完成时,系统会通过
-
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
函数的第四个参数得到的是调用 WSASend
、WSARecv
函数时传入的 WSAOVERLAPPED
结构体变量的地址值。”
IOCP 中将创建全职 I/O 线程,由该线程针对所有客户端进行 I/O。程序员自行创建调用 WSASend
、WSARecv
等 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);
}
代码解析:
- I/O 完成端口的创建
hComPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
CreateIoCompletionPort
函数用于创建一个新的 I/O 完成端口。传递 INVALID_HANDLE_VALUE
表示这是一个新的完成端口对象。
- 创建工作线程
for (i = 0; i < sysInfo.dwNumberOfProcessors; i++)
_beginthreadex(NULL, 0, EchoThreadMain, (LPVOID)hComPort, 0, NULL);
这里使用了 _beginthreadex
创建多个工作线程,每个线程都将等待 I/O 完成事件。线程的数量等于处理器的核心数,目的是最大化 CPU 的使用效率。
- 套接字与完成端口关联
CreateIoCompletionPort((HANDLE)hClntSock, hComPort, (DWORD)handleInfo, 0);
当服务器接受客户端的连接后,将客户端的套接字 hClntSock
与之前创建的完成端口 hComPort
关联。通过这样做,当这个套接字上的 I/O 操作完成时,系统会将完成事件排队到完成端口。
- 提交异步 I/O 请求
复制代码
WSARecv(handleInfo->hClntSock, &(ioInfo->wsaBuf), 1, &recvBytes, &flags, &(ioInfo->overlapped), NULL);
一旦客户端连接成功,服务器会调用 WSARecv
函数提交异步读取操作。这个函数不会阻塞,读取完成后,I/O 完成事件将通过完成端口通知工作线程。
- 工作线程处理 I/O 完成事件
GetQueuedCompletionStatus(hComPort, &bytesTrans, (LPDWORD)&handleInfo, (LPOVERLAPPED*)&ioInfo, INFINITE);
在每个工作线程中,调用 GetQueuedCompletionStatus
函数等待 I/O 完成事件。当某个 I/O 操作(比如 WSARecv
或 WSASend
)完成时,这个函数会返回,线程可以根据事件的结果处理相应的逻辑。
- 处理读取和写入事件
当 I/O 完成后,线程会根据 ioInfo->rwMode
来判断是读取操作还是写入操作:
-
读取操作 (
READ
) :- 服务器收到消息后,将其打印出来,并准备将消息回送给客户端。
- 调用
WSASend
函数发送消息,异步提交发送操作。 - 再次调用
WSARecv
函数准备下一次接收操作。
-
写入操作 (
WRITE
) :- 当消息成功发送后,释放内存资源。
- 内存管理
服务器为每个客户端连接分配了两种数据结构:
PER_HANDLE_DATA
: 保存每个客户端的套接字和地址信息。PER_IO_DATA
: 用于保存异步 I/O 操作的缓冲区和操作模式(读取或写入)。
每个 I/O 操作完成后,服务器都会相应地释放不再需要的内存资源。
上述示例可结合前面的 StableEchoClnt_win.c
运行。