网络模型之IOCP基础(一)
前言
前面介绍了重叠IO模型,该模型的缺点是发出IO请求的线程必须同时对完成通知进行处理,若一个线程发出多个请求,那么即使其它线程完全处于空闲状态,该线程也必须对每个请求的完成通知做出响应,从而影响了性能。
为了解决这个问题,我们可以在主线程中调用 accept
函数,再单独创建 1 个线程来负责客户端IO。实际上,IOCP模型便是创建了专用的线程来处理客户端IO,不过为了充分发挥 CPU 性能,它创建了不止一个线程。
所以IOCP模型其实就是对于重叠IO模型的更加完善,在阅读本篇之前,需要懂得如何使用重叠IO模型。
在学习各种网络模型之时,我们主要应该去关注两点,在select模型的时候大家就看过这个图:
实际上这两点也就是理解各个模型的关键,我们要关注如何等待客户端连接,还有如何将数据从网卡缓冲区拷贝到程序缓冲区。这两部分若有一部分阻塞,那么性能就不是太好。
IOCP(I/O Completion Port),即IO完成端口模型,该模型会创建一个CP(完成端口)内核对象,然后将CP对象与套接字绑定起来,之后套接字若有IO消息便可通过相关函数获得到。
创建CP对象
使用如下函数创建一个CP对象:
HANDLE CreateIoCompletionPort (
HANDLE FileHandle, // file handle to associate with
// the I/O completion port
HANDLE ExistingCompletionPort, // handle to the I/O completion port
DWORD CompletionKey, // per-file completion key for I/O
// completion packets
DWORD NumberOfConcurrentThreads // number of threads allowed to
// execute concurrently
);
这个函数有两个功能,既可以创建CP对象,也可以连接文件句柄与CP对象。在创建CP对象时,只需传入 -1,0,0,0
即可。
由于这里只是创建CP对象,所以前3个参数都不会用到,-1
表示无效的文件句柄,官方对于此提供的有一个专门的宏 INVALID_HANDLE_VALUE
。所以可以这样创建一个CP对象:
HANDLE hCompPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
最后一个参数是这里唯一用上的,表示分配给CP对象用于处理IO的线程数,0 并不表示零个,这是一个默认值,此时会默认使用机器CPU的核心数。但是注意这里并没有真正的创建线程,那还是得我们自己创建。
连接文件句柄与CP对象
同样使用 CreateIoCompletionPort
函数来将文件句柄与CP对象关联起来:
HANDLE CreateIoCompletionPort (
HANDLE FileHandle, // 欲连接到CPU对象的套接字句柄
HANDLE ExistingCompletionPort, // CP对象句柄
DWORD CompletionKey, // 完成键,用于传递已完成IO相关信息
DWORD NumberOfConcurrentThreads // 若第二个参数非NULL,则忽略
);
现在只需考虑前3个参数,第一个参数就是我们的套接字句柄,第二个参数是上一步创建的CP对象,第三个参数用于传递套接字的相关信息,即前面各种模型中用过的 PER_HANDLE_DATA
,其中包含了套接字的句柄和地址。
所以可以这样连接一个套接字与CP对象:
LPPER_HANDLE_DATA lpPerHandleInfo = ...;
SOCKET sock;
// ...
CreateIoCompletionPort((HANDLE)sock, hCompPort, (ULONG_PTR)lpPerHandleInfo, 0);
获取IO消息
当将CP对象与套接字连接了后,系统便管理起该套接字的IO,当该套接字有消息时,系统便会通知我们。
通过如下函数来获取通知:
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort, // CP对象句柄
LPDWORD lpNumberOfBytesTransferred, // 指向传输的数据大小的变量地址
LPDWORD lpCompletionKey, // 接收完成键
LPOVERLAPPED *lpOverlapped, // 指向重叠结构
DWORD dwMilliseconds // 超时时间
);
下面来分别介绍每个参数的意义。
第一个参数就是我们创建的CP对象,这个CP对象在整个程序中就只会创建一个。第二个参数是实际收到的字节数,已经看过重叠IO模型的朋友自不陌生。
第三个参数就是得到的就是 CreateIoCompletionPort
中第三个传入的参数。
而第四个参数则是通过 WSASend
和 WSARecv
函数传入的 WSAOVERLAPPED
结构体的变量地址值,实际上就是我们在重叠IO模型中所说的“小纸条”。
最后一个参数是超时时间,应该传入 INFINITE
,表示一直等待。
获取IO消息属于前面所说的第二个关注点,这部分肯定是包含在线程中的,稍后便会在线程中使用这个来获取IO通知。
发送IO通知
使用下面这个函数,可以手动向完成端口发出IO通知:
BOOL PostQueuedCompletionStatus(
HANDLE CompletionPort, // CP对象句柄
DWORD dwNumberOfBytesTransferred, // 传输的字节数
DWORD dwCompletionKey, // 完成键
LPOVERLAPPED lpOverlapped // 重叠结构
);
一般来说,无需手动发送IO通知,但是在需要退出IO处理线程的时候可以使用这个函数来发出退出的标识。比如可以用 0xFFFFFFFF
来表示实际传输的字节数,这样在线程中收到字节数为 0xFFFFFFFF
大小的数据时就可以安全地退出线程了。
实例一
以上就是IOCP的全部基础内容,因为我们是从select一路学过来的,所以这里的新东西的确不多,实现起来也不太难,包括后面还会介绍的配合扩展函数实现起来其实也不太难。我感觉麻烦的是线程的同步,这里一不小心就会出错,而且不易查改,它可能会在大多数时间完美运行而在有时产生错误,追踪起来实在麻烦。而且大多书上都未给出IOCP完整的例子,网上CV现象严重,也很少能找到完整的C++例子,无处参考只能自己慢慢调试了。
第一个版本的实例是配合 accept
的IOCP,我们由易到难,先通过此例子明白IOCP的用法,之后再使用Windows扩展函数来配合IOCP来实现第二个版本。最后若有时间就再来写一个完整的高级一点的聊天程序,包括界面,服务器,客户端,聊天,到时候看到什么有用的功能就试着做一下,来写个稍微完整的项目吧。
先来包含可能需要用到的头文件:
#include <memory> // 用到智能指针
#include <vector> // 用来保存连接进来的用户
#include <thread> // 开线程用
#include <mutex> // 用于线程同步
#include <cassert> // 断言
#include <iostream>
#include <algorithm> // 可能会用到一些算法
#include <WinSock2.h> // winsock库
#pragma comment(lib, "ws2_32.lib")
接着声明将会用到的结构体:
// 单句柄数据
typedef struct {
SOCKET hSock;
SOCKADDR_IN sockAddr;
}PER_HANDLE_DATA, *LPPER_HANDLE_DATA;
// 单IO数据
typedef struct {
WSAOVERLAPPED overlapped; // 重叠结构
WSABUF wsaBuf; // 数据缓冲区
char buf[BUF_SIZE]; // 数据地址
int opType; // 操作类型
}PER_IO_DATA, *LPPER_IO_DATA;
主要看单IO数据,即我们的“小纸条”,第一个是个重叠结构,需要注意的是它必须放到第一个,为何在后面用到之时再说。接着是一些存数据的字段,最后是一个字段用于标记操作类型,因为系统并不知是发送的操作还是接收的操作,所以我们需要记录一下,之后便可通过此字段识别出具体操作类型。那么都有那些操作类型呢?
我们定义如下:
enum {
OP_READ,
OP_WRITE
};
分别表示读取数据和写入的数据,即接收与发送。
接着定义IOCP服务器类:
class IOCPServer
{
public:
IOCPServer(int port);
~IOCPServer();
void Accept();
private:
void InitSock(); // 初始化套接字
void CreateIocp(); // 创建CP对象并开启线程
void AcceptHandler(); // 处理接受部分
void RequestHandler(); // 处理IO消息部分的线程
void RecvMsg(SOCKET); // 接收消息
void SendMsg(SOCKET, LPPER_IO_DATA, std::string&); // 发送消息
void CloseSock(SOCKET); // 关闭套接字
private:
std::shared_ptr<PER_HANDLE_DATA> m_servSock; // 服务端Socket
std::shared_ptr<PER_HANDLE_DATA> m_clntSock; // 客户端Socket
std::shared_ptr<PER_IO_DATA> m_ioInfo; // 单IO数据
std::vector<std::shared_ptr<PER_HANDLE_DATA>> m_vAcceptedSock; //用于保存连接进来的用户信息
HANDLE m_hCompPort; // CP对象句柄
int m_nPort; // 端口
std::recursive_mutex m_remtx; // 用于线程同步的递归锁
};
实现起来内容也不少,便放到下篇(实际上我还没开始写呢,有点忙……)。