文章目录
一、CreateIoCompletionPort
createIoCompletionPort 函数 (ioapiset.h) 该接口可以仅创建 IO 完成端口,也可以将现有 IO 完成端口与支持重叠 IO 的任何句柄(TCP 套接字,文件,命名管道 等)相关联。
函数作用:
- 创建一个新的 IOCP(I/O 完成端口)对象
- 把一个文件(或 socket)句柄绑定到一个 IOCP 上
也就是说,第一次调用是创建 IOCP,以后再调用是绑定句柄到这个 IOCP 上。
1.1 函数原型
// 头文件:ioapiset.h
HANDLE CreateIoCompletionPort(
HANDLE FileHandle, // 要绑定的文件/套接字句柄
HANDLE ExistingCompletionPort, // 要绑定到的 IOCP(第一次创建时为 NULL)
ULONG_PTR CompletionKey, // 自定义标识键
DWORD NumberOfConcurrentThreads // 并发线程数(仅创建时有效)
);
1.2 参数详解
1. FileHandle(要绑定的文件或设备)
-
这个是绑定到 IOCP 的文件句柄
-
通常是一个 socket、管道、文件、串口等等支持异步 I/O 的句柄
-
如果只是创建 IOCP 而不想立刻绑定任何设备,就传
INVALID_HANDLE_VALUE
// 绑定 socket 到 IOCP
CreateIoCompletionPort(socket, hIOCP, (ULONG_PTR)clientID, 0);
2. ExistingCompletionPort(已有的 IOCP 句柄)
-
如果想创建一个新的 IOCP,传入
NULL
-
如果想绑定句柄到已有的 IOCP,就传这个 IOCP 的句柄
// 第一次创建 IOCP(传 NULL)
HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
3. CompletionKey(完成键,自定义数据)
-
这是一个用户自定义的值,称为 完成键(CompletionKey)
-
当一个 I/O 完成时,我们从 IOCP 队列中取出这个完成事件时,系统会把这个键也返回来
-
可以用它来标识“这个事件属于哪个客户端/连接/资源”
// 传一个客户端对象指针或ID
CreateIoCompletionPort(socket, hIOCP, (ULONG_PTR)pClientInfo, 0);
⚠️ 传进去什么类型都可以,只要能转成 ULONG_PTR
(通常是指针或整型)
4. NumberOfConcurrentThreads(并发线程数)
-
这个值只在创建新的 IOCP 时有效
-
它告诉系统:这个 IOCP 最多能同时唤醒多少个线程去处理 I/O 完成通知
-
默认写 0 就行,表示使用系统默认值(一般是 CPU 核心数 × 2)
二、AcceptEx
AcceptEx 函数 (mswsock.h)AcceptEx 函数接受新连接,返回本地和远程地址,并接收客户端应用程序发送的第一个数据块。
AcceptEx
是 Windows 提供的扩展异步 accept 函数,专门用于高性能网络服务器,常与 IOCP + Overlapped I/O 结合使用。
相比传统的 accept()
,它具备以下优势:
-
可以异步接受连接(不阻塞)
-
可以在接受连接的同时收数据(减少一次 recv 调用)
-
可以结合重叠 I/O 与完成端口使用,更高效
2.1 函数原型
BOOL AcceptEx(
SOCKET sListenSocket, // 监听套接字
SOCKET sAcceptSocket, // 准备好的接受套接字
PVOID lpOutputBuffer, // 用于接收数据 + 地址的缓冲区
DWORD dwReceiveDataLength, // 想要在 accept 时就接收的字节数
DWORD dwLocalAddressLength, // 本地地址部分长度
DWORD dwRemoteAddressLength, // 远端地址部分长度
LPDWORD lpdwBytesReceived, // 实际收到的字节数(完成后填充)
LPOVERLAPPED lpOverlapped // 指向 OVERLAPPED 的指针(用于异步)
);
2.2 参数详解
1. sListenSocket
(监听 socket)
- 这是用于监听客户端连接的 socket。
- 它必须是已经 bind+listen 的 socket。
2. sAcceptSocket
(接收 socket)
- 提前准备好的空 socket
- 系统在有连接时会把这个 socket 填充成“已连接状态”
- 必须用
WSASocket
创建,并设置为OVERLAPPED
模式
SOCKET clientSocket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
3. lpOutputBuffer
(输出缓冲区)
- 系统会把接收到的数据 + 地址信息写进这个缓冲区
- 大小必须大于等于:dwReceiveDataLength + dwLocalAddressLength + dwRemoteAddressLength
- 缓冲区前面是接收到的数据,后面是地址信息(地址格式特定)
4. dwReceiveDataLength
(接收数据长度)
- 表示在连接建立的同时就读取多少数据(比如握手信息)
- 如果只想接收连接而不收数据,就设为 0
5. dwLocalAddressLength
、dwRemoteAddressLength
- 这是为获取本地/远程地址预留的缓冲区大小
- 通常每个设为
sizeof(SOCKADDR_IN) + 16
比较稳妥
6. lpdwBytesReceived
- IO 完成后系统会填写实际收到的字节数
- 在调用时可以传 NULL(因为我们用 IOCP 异步通知)
7. lpOverlapped
- 指向一个
OVERLAPPED
结构体 - 表示这次操作是“异步 I/O”,配合 IOCP 使用
- 系统在连接建立并数据准备好后,会通知 IOCP,并附带这个结构体
2.3 使用流程
// 第一步:准备空 socket 和 overlapped
SOCKET clientSocket = WSASocket(..., WSA_FLAG_OVERLAPPED);
OVERLAPPED overlapped = { 0 };
// 第二步:创建缓冲区
char buffer[2 * (sizeof(SOCKADDR_IN) + 16) + 1024]; // 地址 + 数据
// 第三步:异步调用 AcceptEx
AcceptEx(
listenSocket,
clientSocket,
buffer,
0, // 不立即收数据
sizeof(SOCKADDR_IN) + 16,
sizeof(SOCKADDR_IN) + 16,
NULL, // 不需要立即返回字节数
&overlapped
);
// 第四步:等待 IOCP 通知,处理连接
// 在 GetQueuedCompletionStatus 里取出 overlapped,知道哪一个连接建立了
三、ConnectEx
LPFN_CONNECTEX (mswsock.h)ConnectEx 函数与指定的套接字建立连接,并可以选择在建立连接后发送数据。 仅在面向连接的套接字上支持 ConnectEx 函数。
ConnectEx
是什么?
- 它是
connect()
函数的异步版本,用于建立一个 TCP 连接 - 能与 IOCP 和 Overlapped I/O 模型配合,实现非阻塞连接
- 在连接成功的同时,可以顺便发送一小段数据(节省一次调用)
3.1 函数原型
BOOL ConnectEx(
SOCKET s, // 未连接的 socket
const sockaddr *name, // 目标地址
int namelen, // 地址长度
PVOID lpSendBuffer, // 要发送的数据(可选)
DWORD dwSendDataLength, // 要发送的数据长度
LPDWORD lpdwBytesSent, // 实际发送的字节数
LPOVERLAPPED lpOverlapped // 用于异步操作的 OVERLAPPED 结构体
);
3.2 参数详解
1. s
(Socket)
- 要发起连接的 socket,必须是用
WSASocket
创建的,并带有WSA_FLAG_OVERLAPPED
标志 - 注意:调用
ConnectEx
之前,这个 socket 必须先用bind()
绑定一个本地地址(即使是 0.0.0.0 也行),否则会失败!
sockaddr_in localAddr = { ... }; // 0.0.0.0 任意端口
bind(sock, (sockaddr*)&localAddr, sizeof(localAddr));
2. name
(远程地址)
- 这是你要连接的目标服务器地址,类型为
sockaddr*
- 通常是服务器的 IP 和端口信息
3. namelen
(远程地址长度)
- 就是
name
指向的地址结构的大小,一般写sizeof(sockaddr_in)
4. lpSendBuffer
(要发送的数据)
- 可选参数,你可以指定一块缓冲区,连接成功后立即将它发出去
- 如果你不想立即发数据,就传
NULL
5. dwSendDataLength
(要发的数据长度)
- 上面缓冲区的字节数
- 如果不发送,就写 0
6. lpdwBytesSent
(发送字节数)
- 系统写入的实际发送字节数
- 如果你用的是 IOCP/重叠IO,通常可以设为 NULL,因为结果会在 IO 完成时返回
7. lpOverlapped
(重叠结构体)
- 表示这是一次异步 I/O 操作
- 当连接成功时,系统会通过 IOCP 通知我们,附带这个结构体
3.3 使用流程
// 1. 创建 socket
SOCKET clientSock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
// 2. bind 到本地任意地址(必须!)
sockaddr_in localAddr = {0};
localAddr.sin_family = AF_INET;
localAddr.sin_addr.s_addr = htonl(INADDR_ANY);
localAddr.sin_port = 0;
bind(clientSock, (sockaddr*)&localAddr, sizeof(localAddr));
// 3. 准备目标地址
sockaddr_in serverAddr = { ... }; // 服务器地址和端口
// 4. 创建 OVERLAPPED
OVERLAPPED overlapped = {0};
// 5. 发起连接请求(异步)
ConnectEx(
clientSock,
(sockaddr*)&serverAddr,
sizeof(serverAddr),
NULL, // 不立即发数据
0,
NULL,
&overlapped
);
// 6. 等待 IOCP 通知,连接成功或失败
// 在 GetQueuedCompletionStatus 中获取结果
3.4 注意事项
细节 | 说明 |
---|---|
必须先 bind() | 不像传统 connect() ,ConnectEx 在调用前必须先 bind() |
ConnectEx 不是默认 API | 需要通过 WSAIoctl() 获取函数指针 |
只能用于 TCP | 不支持 UDP,且只能用于流式 socket |
IOCP 推荐搭配使用 | 最佳性能在 IOCP 模型中体现 |
发送数据可选 | 我们可以在连接时顺带发数据,省一次系统调用 |
获取 ConnectEx 函数指针的方法:
LPFN_CONNECTEX lpConnectEx = NULL;
GUID guidConnectEx = WSAID_CONNECTEX;
DWORD bytes;
WSAIoctl(sock, SIO_GET_EXTENSION_FUNCTION_POINTER,
&guidConnectEx, sizeof(guidConnectEx),
&lpConnectEx, sizeof(lpConnectEx),
&bytes, NULL, NULL);
四、DisconnectEx
LPFN_DISCONNECTEXDisconnectEx 函数关闭套接字上的连接,并允许重用套接字句柄。
DisconnectEx
用于在不关闭 socket 的情况下断开一个已连接的 socket,通常用于服务端在处理完客户端连接后进行复用(重用 socket)而不是销毁它。适合使用 AcceptEx 接收连接后,在处理完后再将 socket“断开”,以便下次复用,提高性能,减少 socket 创建/销毁带来的开销。
4.1 函数原型
BOOL DisconnectEx(
SOCKET s, // 要断开的 socket
LPOVERLAPPED lpOverlapped, // 异步操作结构体
DWORD dwFlags, // 标志位(可选)
DWORD dwReserved // 保留字段(必须为 0)
);
4.2 参数详解
参数名 | 说明 |
---|---|
s | 已连接的 socket(通常是通过 AcceptEx 接收到的) |
lpOverlapped | OVERLAPPED 异步结构体,支持异步断开 |
dwFlags | 目前支持的值为:TF_REUSE_SOCKET 表示断开后 socket 可重用 |
dwReserved | 保留字段,必须设为 0 |
4.3 示例代码
BOOL DisconnectSocket(SOCKET clientSock)
{
OVERLAPPED overlapped = {0};
// flags: 是否打算重用这个 socket
DWORD flags = TF_REUSE_SOCKET;
BOOL result = DisconnectEx(
clientSock,
&overlapped,
flags,
0 // reserved,一定要设为 0
);
if (!result)
{
if (WSAGetLastError() != ERROR_IO_PENDING)
{
printf("DisconnectEx failed: %d\n", WSAGetLastError());
return FALSE;
}
}
// 断开是异步的,完成时会收到 IOCP 通知
return TRUE;
}
4.4 注意事项
项目 | 说明 |
---|---|
必须用 WSASocket 创建 socket | 并带有 WSA_FLAG_OVERLAPPED |
必须是 TCP socket | 适用于 SOCK_STREAM 类型 |
必须使用 AcceptEx 接收的 socket | 才可以传给 DisconnectEx |
TF_REUSE_SOCKET 是关键 | 否则 socket 断开后不能复用 |
如果异步断开,必须用 IOCP 获取完成通知 | 断开完成后才可以重用或关闭 socket |
五、WSARecv
WSARecv 函数 (winsock2.h)WSARecv 函数从连接的套接字或绑定的无连接套接字接收数据。
5.1 函数原型
int WSAAPI WSARecv(
SOCKET s, // socket
LPWSABUF lpBuffers, // 缓冲区数组
DWORD dwBufferCount, // 缓冲区个数
LPDWORD lpNumberOfBytesRecvd, // 实际收到的字节数
LPDWORD lpFlags, // 标志位
LPWSAOVERLAPPED lpOverlapped, // OVERLAPPED结构体
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 完成回调(可为 NULL)
);
5.2 参数详解
参数详细解释(通俗版)
参数名 | 说明 |
---|---|
s | 套接字(socket),要接收数据的 TCP 连接 |
lpBuffers | 指向缓冲区数组的指针(一个 WSABUF 数组,至少有一项) |
dwBufferCount | 上面数组的长度,通常为 1 |
lpNumberOfBytesRecvd | 实际接收到的字节数(异步操作时通常为 NULL) |
lpFlags | 标志位(如 MSG_PARTIAL ),通常设为 0 |
lpOverlapped | 异步结构体,传入就表示执行异步重叠 I/O |
lpCompletionRoutine | 接收完成后的回调函数(配合事件通知模型),IOCP 不用这个,设为 NULL |
5.3 用法示例
WSABUF buffer;
char recvBuf[1024];
buffer.buf = recvBuf;
buffer.len = sizeof(recvBuf);
DWORD flags = 0;
OVERLAPPED* pOverlapped = new OVERLAPPED{};
memset(pOverlapped, 0, sizeof(OVERLAPPED));
int ret = WSARecv(
clientSocket, // 已连接 socket
&buffer, // 数据缓冲区
1, // 缓冲区数量
NULL, // 异步调用,不关心此处
&flags, // 标志位
pOverlapped, // 异步结构体
NULL // IOCP 模型中设为 NULL
);
if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) {
// 错误处理
}
WSABUF
是什么?
typedef struct _WSABUF {
ULONG len; // 缓冲区长度
CHAR FAR *buf; // 指向数据缓冲区的指针
} WSABUF, *LPWSABUF;
一个 WSABUF
就是一个“装数据的盒子”。我们可以用一个数组存多个 WSABUF
,即可以分散/聚集式 IO(Scatter-Gather I/O)。
IOCP 中如何接收通知?
当使用 WSARecv
并传入了 OVERLAPPED
,我们希望用异步方式接收数据。此时不去等待它返回数据,而是:
- 异步返回,
WSARecv
函数立即返回(值为 0 或 SOCKET_ERROR + WSA_IO_PENDING) - 数据真正收到后,Windows 会将完成事件加入 IOCP 的完成队列
- 正在
GetQueuedCompletionStatus
中阻塞等待的线程会收到这个完成通知 - 可以从
OVERLAPPED
结构体中拿到上下文(比如哪个客户端、哪个缓冲区)
返回值说明
- 成功立即完成:返回 0,
lpNumberOfBytesRecvd
中写入接收字节数 - 异步提交成功:返回
SOCKET_ERROR
,错误码为WSA_IO_PENDING
- 失败:返回
SOCKET_ERROR
,错误码为其他值
六、WSASend
WSASend 函数 (winsock2.h)WSASend 函数在连接的套接字上发送数据。
6.1 函数原型
int WSAAPI WSASend(
SOCKET s, // 套接字
LPWSABUF lpBuffers, // 数据缓冲区数组
DWORD dwBufferCount, // 缓冲区数量
LPDWORD lpNumberOfBytesSent, // 实际发送的字节数
DWORD dwFlags, // 标志位
LPWSAOVERLAPPED lpOverlapped, // OVERLAPPED结构体(异步)
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 完成通知(可为 NULL)
);
6.2 参数详解
参数名 | 说明 |
---|---|
s | 套接字(Socket),你要往哪个连接发数据 |
lpBuffers | 你要发送的数据缓冲区数组(WSABUF 类型) |
dwBufferCount | 缓冲区数量,一般就是 1 |
lpNumberOfBytesSent | 实际发送的字节数(异步时不需要,设为 NULL) |
dwFlags | 发送标志(一般为 0) |
lpOverlapped | 指向 OVERLAPPED 结构体的指针,用于异步操作 |
lpCompletionRoutine | 异步发送完成后的回调函数,如果是 IOCP 模型,设为 NULL |
6.3 示例代码
WSABUF wsaBuf;
char sendBuf[] = "Hello, client!";
wsaBuf.buf = sendBuf;
wsaBuf.len = strlen(sendBuf);
OVERLAPPED* pOverlapped = new OVERLAPPED{};
memset(pOverlapped, 0, sizeof(OVERLAPPED));
int ret = WSASend(
clientSocket, // 连接的 socket
&wsaBuf, // 数据缓冲区
1, // 只有一个缓冲区
NULL, // 异步时这个参数可设为 NULL
0, // 标志位
pOverlapped, // OVERLAPPED结构体
NULL // IOCP 模型中不使用回调函数
);
if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) {
// 错误处理
}
返回值说明
- 成功(同步完成):返回 0,数据立即发送完毕
- 成功(异步提交):返回 SOCKET_ERROR,
WSAGetLastError() == WSA_IO_PENDING
- 失败:返回 SOCKET_ERROR,错误码为其他值
IOCP 模型下如何处理发送?
- 调用
WSASend
并传入OVERLAPPED
,提交异步发送请求 - 如果数据没有立即发送,操作系统会排队
- 数据真正发送完毕后,会把“发送完成”事件放进 IOCP 的完成队列
- 工作线程调用
GetQueuedCompletionStatus
等待这些事件 - 系统唤醒线程,线程就知道“发送完成了”,可以继续下一步操作
WSABUF
typedef struct _WSABUF {
ULONG len; // 缓冲区长度
CHAR FAR *buf; // 数据指针
} WSABUF, *LPWSABUF;
我们可以定义多个 WSABUF
(比如发送多个数据块),也可以就用一个。
七、GetQueuedCompletionStatus
GetQueuedCompletionStatus 函数(ioapiset.h)尝试从指定的 I/O 完成端口取消对 I/O 完成数据包的排队。 如果没有完成数据包排队,函数将等待与完成端口关联的挂起 I/O 操作完成。 此函数将线程与指定的完成端口相关联。 一个线程最多可以与一个完成端口相关联。
7.1 函数原型
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort, // IOCP 句柄
LPDWORD lpNumberOfBytesTransferred, // 实际传输的字节数
PULONG_PTR lpCompletionKey, // 与设备句柄绑定的Key
LPOVERLAPPED *lpOverlapped, // 对应的重叠结构体
DWORD dwMilliseconds // 超时时间(等待多久)
);
7.2 参数详解
参数名 | 用法 | 解释 |
---|---|---|
CompletionPort | IOCP的句柄 | 我们传入之前用 CreateIoCompletionPort 创建的 |
lpNumberOfBytesTransferred | 实际收发字节数 | 内核告诉我们这次到底传输了多少数据 |
lpCompletionKey | 自定义key | 我们自己设置的标识,比如标记哪个连接 |
lpOverlapped | 原来投递 I/O 时的结构 | 帮助我们知道是哪一笔 I/O 操作完成了 |
dwMilliseconds | 等待时长 | 等待多长时间,如果传 INFINITE 就一直等下去 |
7.3 代码示例
工作流程
1. 我们发起一个异步I/O操作,比如 WSARecv 或 WSASend,传入一个 OVERLAPPED
2. 操作系统在后台完成这个I/O任务
3. 一旦完成,系统就把“完成通知”放进 IOCP 的队列中
4. 我们的工作线程用 GetQueuedCompletionStatus 挂起等待
5. 系统通知我们:某个任务完成了,线程被唤醒
6. 我们拿到 OVERLAPPED、Key、传输字节数,就能开始处理业务了
DWORD bytesTransferred = 0;
ULONG_PTR completionKey = 0;
LPOVERLAPPED pOverlapped = nullptr;
BOOL result = GetQueuedCompletionStatus(
hCompletionPort, // 我们的IOCP
&bytesTransferred, // 传输字节数
&completionKey, // 哪个socket或任务
&pOverlapped, // 哪个I/O任务完成了
INFINITE // 等待时间,这里设为无限等待
);
if (result) {
// 成功!我们可以继续处理这个完成的I/O任务
} else {
if (pOverlapped == nullptr) {
// 超时了,没有任何任务完成
} else {
// 有任务完成但出错了
DWORD err = GetLastError();
// 我们要根据错误码来处理,比如连接被断开等
}
}
7.4 注意事项
事项 | 原因 |
---|---|
OVERLAPPED 不能是栈变量 | 因为它在异步执行时必须还在内存中 |
CompletionKey 是我们识别连接的关键 | 一般用它来指向自定义的连接结构体 |
多线程同时调用是安全的 | 我们可以启动多个线程同时阻塞在 GetQueuedCompletionStatus 上 |
失败并不等于没有完成 | 有些完成通知是失败的,比如客户端断开,这也是通知 |