IOCP技术详解

这几周我接触了Windows网络通讯中的IOCP模型,自己在网上找了相关的知识进行学习,自己又下了好多服务器端的代码,但都运行不了,也是自己菜,能力还需加强。幸好我师父资助了我一个能运行的服务端IOCP代码,自己参照网上的相关知识后又与这个能运行的代码做了参照,算是勉强理解了构造IOCP的一般方法,对IOCP的使用也有了很大的心得。接下来我就把自己对IOCP相关知识的理解记录下来,方便自己以后的复习,当然这篇文章如果对正在阅读的你有帮助也算是很好的。

Windows下的六种通讯模型

讲IOCP之前先把它之前的五种通讯模型讲下,由于异步选择模型、事件选择模型和重叠I/O模型没有接触过,所以就不再评论。

阻塞模型

我之前写过Windows下socket编程,在这篇文章中写的TCP服务端代码使用的就是阻塞模型。在阻塞模型中,send和recv时要看对应的缓冲区中是否有数据,有过有数据就进行发送,如果没有数据send或recv行为就进入阻塞等待状态,直到缓冲区内有数据进入为止。该模型效率比较低下。大致创建步骤如下:
客户端

  • connect
  • send
  • recv
  • closesocket
    服务端:
  • bind
  • listen
  • accept
  • recv
  • send
    示意图如下:
    IOCP技术详解_第1张图片
    该模式只是简单的实现了服务端和客户端的通讯问题,对多线程、服务器并发效率等问题并没有进行考虑,算是最原始的网络通讯模型。

选择模型

选择(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
);

异步选择模型

没接触,不评论。

事件选择模型

没接触,不评论。

重叠I/O模型

没接触,不评论。

IOCP模型

好嘞,终于来到了我们的重头戏——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模型的优点:
  1. 异步通讯

IOCP使用的是异步通讯的机制,避免了因为信息阻塞而耽误后续进程的执行。所以高性能的服务器模型一定是异步的

  1. 效率高
    IOCP因为采用了异步通讯的机制,和阻塞模型相比避免了阻塞的问题,大大提高了效率。和“阻塞通信+多线程”相比,减少了进程之间切换造成的时间浪费,所以相较于其他模型,IOCP的效率很高。

IOCP实现详解

首先对IOCP模型中用到的结构体和函数模型解释说明下:

  1. 单句柄数据:该结构体用于管理具体哪个socket在进行io请求操作。
typedef struct PER_HANDLE_DATA{
	SOCKET 				socket;
	SOCKADDR_IN 		addr;
}*pPER_HANDLE_DATA,PER_HANDLE_DATA;
  1. 单I/O数据:该结构体用于每一次客户端socket向IOCP提交请求时提交给系统,在其结构内可定义任意参数(overlapped必须在第一个),当请求完成后,IOCP**“原封不动”**的返回到工作线程,但给相应参数进行了赋值,如用于接收数据的buffer数组。
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;

  1. WSABUF结构体:
typedef struct _WSABUF{
	ULONG len;                            /*the length of buffer*/
	__field_bcount(len) CHAR FAR *buf;    /*  the pointer to the buffer  */
};

  1. CreateIoCompletionPort函数
HANDLE WINAPI CreateIoCompletionPort(
	__in      HANDLE  FileHandle,             // 这里当然是连入的这个套接字句柄了
	 __in_opt  HANDLE  ExistingCompletionPort, // 这个就是前面创建的那个完成端口
	 __in      ULONG_PTR CompletionKey,        // 这个参数就是类似于线程参数一样,在
											   // 绑定的时候把自己定义的结构体指针传递
											   // 这样到了Worker线程中,也可以使用这个
											   // 结构体的数据了,相当于参数的传递
	 __in      DWORD NumberOfConcurrentThreads // 这里同样置0
);
  1. 监听窗口状态函数(GetQueuedCompletionStatus)
BOOL WINAPI GetQueuedCompletionStatus(  
    __in   HANDLE          CompletionPort,    // 建立的完成端口  
    __out  LPDWORD         lpNumberOfBytes,   //返回的字节数  
    __out  PULONG_PTR      lpCompletionKey,   // 与完成端口的时候绑定的那个socket对应的自定义结构体PER_HANDLE_DATA
    __out  LPOVERLAPPED    *lpOverlapped,     // 重叠结构  
    __in   DWORD           dwMilliseconds     // 等待完成端口的超时时间,一般设置INFINITE  
    ); 

  1. 投递WSARecv函数
//传递参数,调用就行
int WSARecv(  
    SOCKET socket,                       // 投递的套接字  
     LPWSABUF lpBuffers,                 // 接收缓冲区WSABUF结构构成的数组  
     DWORD dwBufferCount,                // 数组中WSABUF结构的数量,设置为1  
     LPDWORD lpNumberOfBytesRecvd,       // 返回函数调用所接收到的字节数  
     LPDWORD lpFlags,                    // 设置为0  
     LPWSAOVERLAPPED lpOverlapped,       // Socket对应的重叠结构  
     NULL                                // 设置完成例程模式,这里设置为NULL 
); 


  1. 通知工作线程退出函数
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、取得数据并处理;

完整的代码见链接:
点此下载

你可能感兴趣的:(C++,服务器,windows,网络)