这几周我接触了Windows网络通讯中的IOCP模型,自己在网上找了相关的知识进行学习,自己又下了好多服务器端的代码,但都运行不了,也是自己菜,能力还需加强。幸好我师父资助了我一个能运行的服务端IOCP代码,自己参照网上的相关知识后又与这个能运行的代码做了参照,算是勉强理解了构造IOCP的一般方法,对IOCP的使用也有了很大的心得。接下来我就把自己对IOCP相关知识的理解记录下来,方便自己以后的复习,当然这篇文章如果对正在阅读的你有帮助也算是很好的。
讲IOCP之前先把它之前的五种通讯模型讲下,由于异步选择模型、事件选择模型和重叠I/O模型没有接触过,所以就不再评论。
我之前写过Windows下socket编程,在这篇文章中写的TCP服务端代码使用的就是阻塞模型。在阻塞模型中,send和recv时要看对应的缓冲区中是否有数据,有过有数据就进行发送,如果没有数据send或recv行为就进入阻塞等待状态,直到缓冲区内有数据进入为止。该模型效率比较低下。大致创建步骤如下:
客户端
选择(select)模型是Winsock中最常见的 I/O模型。核心便是利用 select 函数,实现对 I/O的管理!利用 select 函数来判断某Socket上是否有数据可读,或者能否向一个套接字写入数据,防止程序在Socket处于阻塞模式中时,在一次 I/O 调用(如send或recv、accept等)过程中,被迫进入“锁定”状态;同时防止在套接字处于非阻塞模式中时,产生WSAEWOULDBLOCK错误。
select函数原型:
int select(
__in int nfds,
__in_out fd_set* readfds,//检查可读性
__in_out fd_set* writefds,//检查可写性
__in_out fd_set* exceptfds,//用于例外数据
__in const struct timeval* timeout
);
没接触,不评论。
没接触,不评论。
没接触,不评论。
好嘞,终于来到了我们的重头戏——IOCP模型,IOCP模型又叫完成端口。IOCP(输入输出完成端口)服务器端模型是Windows上网络模型的一个重点。IOCP简单的说明就是创建专用的I/O线程,该线程负责与所有客户端进行I/O,类似于one connection one thread,但是IOCP并不会无限制的创建线程,而是在并行的线程之间有一个上限。
IOCP的完成端口不是指TCP/IP端口号,而是一个消息队列,当某项I/O完成之后,其对应的工作者就会收到一个通知,然后进行其他的操作。IOCP是进行的异步I/O,其依赖于一个工作者线程池。使用工作者线程池限制线程的数量以避免创建太多thread而导致在切换线程时浪费大量的时间。IOCP会充分利用Windows内核来进行I/O的调度,是用于C/S通信模式中性能最好的网络通信模型,没有之一;
IOCP使用的是异步通讯的机制,避免了因为信息阻塞而耽误后续进程的执行。所以高性能的服务器模型一定是异步的
首先对IOCP模型中用到的结构体和函数模型解释说明下:
typedef struct PER_HANDLE_DATA{
SOCKET socket;
SOCKADDR_IN addr;
}*pPER_HANDLE_DATA,PER_HANDLE_DATA;
typedef struct PER_IO_DATA{
OVERLAPPED overlapped; //类似id,每个io都必须有一个
SOCKET socket; //io请求的套接字
WSABUF wsabuf; //用于从缓冲区获取数据的结构
char buffer[LENGTH]; //保存获得的数据
OPT_TYPE opt_type; //这次io请求的类型,如ACCEPT,RECV,SEND等
}*pPER_IO_DATA,PER_IO_DATA;
typedef struct _WSABUF{
ULONG len; /*the length of buffer*/
__field_bcount(len) CHAR FAR *buf; /* the pointer to the buffer */
};
HANDLE WINAPI CreateIoCompletionPort(
__in HANDLE FileHandle, // 这里当然是连入的这个套接字句柄了
__in_opt HANDLE ExistingCompletionPort, // 这个就是前面创建的那个完成端口
__in ULONG_PTR CompletionKey, // 这个参数就是类似于线程参数一样,在
// 绑定的时候把自己定义的结构体指针传递
// 这样到了Worker线程中,也可以使用这个
// 结构体的数据了,相当于参数的传递
__in DWORD NumberOfConcurrentThreads // 这里同样置0
);
BOOL WINAPI GetQueuedCompletionStatus(
__in HANDLE CompletionPort, // 建立的完成端口
__out LPDWORD lpNumberOfBytes, //返回的字节数
__out PULONG_PTR lpCompletionKey, // 与完成端口的时候绑定的那个socket对应的自定义结构体PER_HANDLE_DATA
__out LPOVERLAPPED *lpOverlapped, // 重叠结构
__in DWORD dwMilliseconds // 等待完成端口的超时时间,一般设置INFINITE
);
//传递参数,调用就行
int WSARecv(
SOCKET socket, // 投递的套接字
LPWSABUF lpBuffers, // 接收缓冲区WSABUF结构构成的数组
DWORD dwBufferCount, // 数组中WSABUF结构的数量,设置为1
LPDWORD lpNumberOfBytesRecvd, // 返回函数调用所接收到的字节数
LPDWORD lpFlags, // 设置为0
LPWSAOVERLAPPED lpOverlapped, // Socket对应的重叠结构
NULL // 设置完成例程模式,这里设置为NULL
);
BOOL WINAPI PostQueuedCompletionStatus(
__in HANDLE CompletionPort, //当初创建的完成端口
__in DWORD dwNumberOfBytesTransferred, //可做为通知线程退出的一个标示码,其对应于GetQueuedCompletionStatus中的参数lpNumberOfBytes,所以可做文章
__in ULONG_PTR dwCompletionKey, //PER_HANDLE_DATA结构体
__in_opt LPOVERLAPPED lpOverlapped
);
大致的步骤如下:
1、创建一个完成端口
HANDLE m_hIOCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0 );
每错,就是那么简单。
2、根据CPU个数创建工作者线程,把完成端口传进去线程里
// 创建对应数量的工作者线程,一般是CPU核心数量*2
SYSTEM_INFO si;
GetSystemInfo(&si);
int m_nThreads = si.dwNumberOfProcessors * 2;
HANDLE* m_phWorkerThreads = new HANDLE[m_nThreads];
for (int i = 0; i < m_nThreads; i++) {
//将IOCP对象作为线程参数传递
m_phWorkerThreads[i] = ::CreateThread(0, 0, (LPTHREAD_START_ROUTINE)_WorkerThread, m_hIOCompletionPort, 0, 0);
}
3、创建侦听SOCKET,把SOCKET和完成端口关联起来
CreateIoCompletionPort((HANDLE)m_listensocket, m_hIOCompletionPort, 0, 0);
4、创建PerIOData,向连接进来的SOCKET投递WSARecv操作
WSARecv(PerHandleData->m_clientSock, &PerIoData->m_wsaBuf, 1, &dwRecv, &Flags, &PerIoData->m_Overlapped, NULL);
5、线程里所做的事情:
a、GetQueuedCompletionStatus,在退出的时候就可以使用 PostQueudCompletionStatus使线程退出;
BOOL bReturn = GetQueuedCompletionStatus(CompletionPort, &dwBytesTransferred, (LPDWORD)&PerHandleData, (LPOVERLAPPED*)&PerIoData, INFINITE);
b、取得数据并处理;
完整的代码见链接:
点此下载