逐步实现TCP服务端Step04-9:IOCP

IOCP是Windows平台特有的机制,在Windows平台上,它与epoll在Linux上的地位相当。二者常放在一起讨论,不过,它俩完全不是一类东西。

selectpollepoll系列,属于I/O多路复用机制。通过这种机制,就可在单一流程(单线程)中一次性找出,当前I/O已就绪的文件描述符。这大大降低了,程序因不知道哪些I/O已就绪,而无休止地轮询(调用recv/send等)所有文件描述符所产生的大量无用功。当然,多路复用是用在单线程处理多路I/O的场景中,若一路I/O独占一个线程的话,就没必要轮询了,线程直接阻塞在I/O上就行了。

IOCP是配合Overlapped I/O来使用的,这种所谓的重叠I/O是Windows平台上的异步I/O机制。说到同步、异步的概念,在不同场合下有不同解释。单说异步I/O的话,可简单理解为,进程把I/O任务完全交给内核去办,待内核办完以后,将结果告知给进程。进程的态度就是,不管当前的I/O是否准备就绪,只管告诉内核,自 己现在要做I/O操作,让内核看着办。

异步I/O要比同步模式多做一件事情,就是通告完成结果。常用的通告方式就是对进程空间的某个函数进行“回调”,这个函数是进程在发起I/O请求时指定的。Overlapped I/O当然也支持这种方式,除此之外,它还支持一种名为事件对象的通知机制。

使用回调函数的问题在于,发起I/O操作的线程必须是I/O结果的处理者。这个函数是在当前线程被设定的,不可能在其它线程中被回调。现实当中,对I/O结果(业务)的处理往往需要耗费很多时间,我们希望通过多线程的方式将业务拆分处理。但这种回调的方式,就做不到I/O与处理I/O结果的分离。

对于使用事件对象的方式,由于在单线程中WSAWaitForMultipleEvents最多只能等待64个事件,更多的情况,就需要启用多线程。这是事件对象通知机制的局限性。

IOCP是对重叠I/O结果通知机制的补充,基于它就可以方便地实现,用多个线程去处理I/O结果的目标

它的做法就是设定了一个完成端口内核对象,该对象用于关联各文件描述符(在Windows中叫文件句柄)。同时,重叠I/O的完成结果也由该对象管理。结果处理线程只要阻塞在GetQueuedCompletionStatus调用处等待返回,这样就可达到用线程去处理结果的目的。同时,IOCP机制还可以设定并发处理的线程数。

注意,IOCP关注的点是在异步I/O的完成通知这块(注意,它并不负责区分完成的结果是读还是写,需要调用者自行标识),而不是多路复用机制的I/O就绪监测。或者说,它提供的是异步I/O的完成结果监测。

使用IOCP,主要涉及如下几个API:

根据参数值的不同,该API有两个用处,一是创建完成端口对象,此时,只有最后一个参数有实际作用,其它均置为空。第二是用于建立文件描述符与完成端口对象的关联,此时的各参数含义如下:

  • FileHandle:准备要与完成端口对象建立关联的那个文件描述符(文件句柄)。
  • ExistingCompletionPort:完成端口对象句柄。
  • CompletionKey:调用者传入的一个自定值。在获取完成结果的时候,这个值会被取到,它通常被用作唯一标识被指定的那个文件句柄的值。我们这里把该文件句柄对应于socket_info_list_的索引值传了进去,这样就能在处理结果时方便找出文件句柄对应的SocketInfo结构。
  • NumberOfConcurrentThreads:并发线程数。这个字段只在第二个参数为NULL的时候有效。在创建完成端口对象的时候需要指定它,指定一个正值n,表示在完成端口对象上最多可并发n个线程。若指定0,则以当前CPU的个数作为并发上限值。

该API用于查询在完成端口上注册的重叠I/O,哪些已经出结果了。该函数应该在各工作线程中被调用。

  • CompletionPort:显然,这是完成端口句柄。
  • lpNumberOfBytes:用于记录实际传输数据大小的变量的指针。
  • lpCompletionKey:上面说的那个CompletionKey,这是指向该变量的指针。
  • lpOverlapped:再调用WSASend、WSARecv发起I/O的时候,需要传递一个OVERLAPPED结构。这个参数就是指向它地址的指针。
  • dwMilliseconds:设置等待时间,置为INFINITE,程序将阻塞,直到有I/O完成结果通知给完成端口对象。

上面两个API是IOCP机制特有的,下面这几个是用于发起重叠I/O的API:

由于只在netio中使用了recv操作,所以只说WSARecv 。

  • s:socket文件描述符(句柄)。
  • lpBuffers:接收缓冲区,注意是WSABUF结构。
  • dwBufferCount:缓冲区个数,传入的WSABUF结构个数。
  • lpNumberOfBytesRecvd:接收到的字节数。
  • lpFlags:这个字段用于指定WSARecv的行为,这里不用关注它。
  • lpOverlapped:指向OVERLAPPED结构的指针,每一个重叠I/O操作都必须给出一个OVERLAPPED结构。
  • lpCompletionRoutine:回调函数,使用完成端口机制,这里置为零。

要使用重叠I/O的话,需要用WSASocket创建socket句柄。这个与普通版的socket类似,除了最后一个参数,设置为WSA_FLAG_OVERLAPPED即可。

由于发起重叠I/O使用的是WSARecv/WSASend函数,其需要用WSABUF结构去指定接收和发送缓冲。在TCPSocket类中加入一个WSABUF类型成员以供使用。
在接收数据的时候,完全没有用到TCPSocket中的那套东西,仅仅是使用了其中维护的接收缓冲区来存放结果数据(将recv_buf_赋给了WSABUF的buf字段)。因此,又在TCPSocket::RecvBytes方法中加入了一些逻辑来对recv_end_做相应的修改。这样,程序逻辑中还是继续调用RecvBytes,只不过这个对应于IOCP的RecvBytes并不做实际的数据接收工作。

netio以及s目前都是单进程单线程结构,这是我们一直使用的基本结构。而IOCP机制的优越性其实体现在对多线程的支持上。基于IOCP可以很方便地构建一个线程池。本次只是在单线程环境下实现了基于IOCP的WSARecv操作,以后在讨论多线程模型的时候,再深入研究IOCP的使用。

之前的实现未考虑对Windows平台的兼容,如果要让所有代码都支持Windows平台的话,工作量过大。此次,只对netio用到的部分做了临时的跨平台处理,对于共享内存部分,由于我们只关注netio,不需要与s进行通信。所以,在创建内存的地方,直接使用了原始的new在堆上完成。

改后的代码使用VisualStudio编译,改动较多不一一列出,点击图片进入相应工程:


<==  index  ==>