网络编程:IOCP及关键网络API的使用

一、CreateIoCompletionPort

createIoCompletionPort 函数 (ioapiset.h) 该接口可以仅创建 IO 完成端口,也可以将现有 IO 完成端口与支持重叠 IO 的任何句柄(TCP 套接字,文件,命名管道 等)相关联。

函数作用:

  1. 创建一个新的 IOCP(I/O 完成端口)对象
  2. 把一个文件(或 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. dwLocalAddressLengthdwRemoteAddressLength

  • 这是为获取本地/远程地址预留的缓冲区大小
  • 通常每个设为 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 接收到的)
lpOverlappedOVERLAPPED 异步结构体,支持异步断开
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,我们希望用异步方式接收数据。此时不去等待它返回数据,而是:

  1. 异步返回,WSARecv 函数立即返回(值为 0 或 SOCKET_ERROR + WSA_IO_PENDING)
  2. 数据真正收到后,Windows 会将完成事件加入 IOCP 的完成队列
  3. 正在 GetQueuedCompletionStatus 中阻塞等待的线程会收到这个完成通知
  4. 可以从 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 模型下如何处理发送?

  1. 调用 WSASend 并传入 OVERLAPPED,提交异步发送请求
  2. 如果数据没有立即发送,操作系统会排队
  3. 数据真正发送完毕后,会把“发送完成”事件放进 IOCP 的完成队列
  4. 工作线程调用 GetQueuedCompletionStatus 等待这些事件
  5. 系统唤醒线程,线程就知道“发送完成了”,可以继续下一步操作

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 参数详解

参数名用法解释
CompletionPortIOCP的句柄我们传入之前用 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
失败并不等于没有完成有些完成通知是失败的,比如客户端断开,这也是通知
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值