大佬文章:https://blog.csdn.net/piggyxp/article/details/6922277
首先我们应该了解,Windows的俩种通信模型:
1.串行模型:
一个线程等待一个请求,当请求到达时,线程被唤醒对请求进行处理;处理完后再接着等待下一个请求。
缺点:不能同时处理多个请求。
2.并发模型: 一个线程等待一个请求,当请求到达时,线程会创建一个新的线程来处理请求。然后线程进入下一次循环等待下一个请求。新的线程处理完请求后,会自行终止。
缺点:如果请求数量较多,需要创建相对数量的线程,一方面线程频繁地创建、终止会带来开销;另一方面,线程数量过多,但实际上机器的CPU数量有限,且这些线程大多都是处于可调度的状态,那么内核需花费很多在线程的切换上下文的过程,导致没有多少CPU时间来执行真正需要的任务。
为了解决此类问题,可以使用I/O完成端口内核对象。
这里有一篇文章写的太好了,我也是针对这篇文章进行学习总结,这一部分,看这位大神的文章可以更好地理解:
https://blog.csdn.net/piggyxp/article/details/6922277
从前面的讨论中,我们知道高性能服务器程序使用异步通信机制是必须的。 异步通信就是在咱们与外部的I/O设备进行打交道的时候,我们都知道外部设备的I/O和CPU比起来简直是龟速,比如硬盘读写、网络通信等等,我们没有必要在咱们自己的线程里面等待着I/O操作完成再执行后续的代码,而是将这个请求交给设备的驱动程序自己去处理,我们的线程可以继续做其他更重要的事情
我可以从图中看到一个很明显的并行操作的过程,而“同步”的通信方式是在进行网络操作的时候,主线程就挂起了,主线程要等待网络操作完成之后,才能继续执行后续的代码,就是说要么执行主线程,要么执行网络操作,是没法这样并行的; “异步”方式无疑比 “阻塞模式+多线程”的方式效率要高的多,这也是前者为什么叫“异步”,后者为什么叫“同步”的原因了,因为不需要等待网络操作完成再执行别的操作。
并发模型与线程池:
在典型的并发模型中,服务器为每一个客户端创建一个线程,如果很多客户同时请求,则这些线程都是运行的,那么CPU就要一个个切换,CPU花费了更多的时间在线程切换,线程确没得到很多CPU时间。
到底应该创建多少个线程比较合适呢,微软件帮助文档上讲应该是2*CPU个。但理想条件下最好线程不要切换,而又能象线程池一样,重复利用。I/O完成端口就是使用了线程池。
完成端口:
完成端口的做法是这样的:事先开多个线程,你有几个CPU我就开几个,首先是避免了线程的上下文切换,因为线程想要执行的时候,总有CPU资源可用,然后让这几个线程等着,等到有用户请求来到的时候,就把这些请求都加入到一个公共消息队列中去,然后这几个开好的线程就排队逐一去从消息队列中取出消息并加以处理,这种方式就很优雅的实现了异步通信和负载均衡的问题,因为它提供了一种机制来使用几个线程“公平的”处理来自于多个客户端的输入/输出,并且线程如果没事干的时候也会被系统挂起,不会占用CPU周期。
这个关键的作为交换的消息队列,就是完成端口。
首先,它之所以叫“完成”端口,就是说系统会在网络I/O操作“完成”之后才会通知我们,也就是说,我们在接到系统的通知的时候,其实网络操作已经完成了,就是比如说在系统通知我们的时候,并非是有数据从网络上到来,而是来自于网络上的数据已经接收完毕了;或者是客户端的连入请求已经被系统接入完毕了等等,我们只需要处理后面的事情就好了。
其次,我们需要知道,所谓的完成端口,其实和HANDLE一样,也是一个内核对象。
重叠结构(OVERLAPPED)
要实现异步通信,必须要用到一个很风骚的I/O数据结构,叫重叠结构“Overlapped”,Windows里所有的异步通信都是基于它的,完成端口也不例外。
重叠结构是异步通信机制实现的一个核心数据结构,因为你看到后面的代码你会发现,几乎所有的网络操作例如发送/接收之类的,都会用WSASend()和WSARecv()代替,参数里面都会附带一个重叠结构,这是为什么呢?因为重叠结构我们就可以理解成为是一个网络操作的ID号,也就是说我们要利用重叠I/O提供的异步机制的话,每一个网络操作都要有一个唯一的ID号,因为进了系统内核,里面黑灯瞎火的,也不了解上面出了什么状况,一看到有重叠I/O的调用进来了,就会使用其异步机制,并且操作系统就只能靠这个重叠结构带有的ID号来区分是哪一个网络操作了,然后内核里面处理完毕之后,根据这个ID号,把对应的数据传上去。
第一步:
在我们使用完成端口之前,要调用CreateIoCompletionPort函数先创建完成端口对象。
HANDLE CreateIoCompletionPort(
HANDLE FileHandle,
HANDLE ExistingCompletionPort,
DWORD CompletionKey,
DWORD NumberOfConcurrentThreads
);
第二步:
根据处理器个数,创建cpu2个工作线程:
CreateThread(NULL, 0, ServerWorkerThread, CompletionPort,0, &ThreadID))
之所以是CPU2,是因为这能更好地利用CPU的处理能力。
第三步:
服务器调用WSASocket,bind, listen, WSAAccept,之后,调用CreateIoCompletionPort()把一个套接字句柄和一个完成端口绑定到一起。完成端口又同一个或多个设备相关联着,所以以套接字为基础,投递发送和请求,对I/O处理。接着,可以依赖完成端口,接收有关I/O操作完成情况的通知。
每当有客户端连入的时候,我们就还是得调用CreateIoCompletionPort()函数,这里却不是新建立完成端口了,而是把新连入的Socket(也就是前面所谓的设备句柄),与目前的完成端口绑定在一起。
至此,我们其实就已经完成了完成端口的相关部署工作了
当一个设备的异步I/O请求完成之后,系统会检查该设备是否关联了一个完成端口,如果是,系统就向该完成端口的I/O完成队列中加入完成的I/O请求列。
然后我们需要从这个完成队列中,取出调用后的结果(需要通过一个Overlapped结构来接收调用的结果)。怎么知道这个队列中已经有处理后的结果呢,调用GetQueuedCompletionStatus函数。
在工作线程内调用GetQueuedCompletionStatus函数扫描完成端口的队列里是否有网络通信的请求存在(例如读取数据,发送数据等)
GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytesTransferred,
LPDWORD lpCompletionKey,
LPOVERLAPPED *lpOverlapped,
DWORD dwMilliseconds
);
GetQueuedCompletionStatus使调用线程挂起,直到指定的端口的I/O完成队列中出现了一项或直到超时。(I/0完成队列中出现了记录)调用GetQueuedCompletionStatus时,调用线程的ID(cpu*2个线程,每个ServerWorkerThread的线程ID)就被放入该等待线程队列中。
等待线程队列很简单,只是保存了这些线程的ID。完成端口会按照后进先出的原则将一个线程队列的ID放入到释放线程列表中。
这样,I/O完成端口内核对象就知道哪些线程正在等待处理完成的I/O请求。当端口的I/O完成队列出现一项时,完成端口就唤醒(睡眠状态中变为可调度状态)等待线程队列中的一个线程。线程将得到完成I/O项中的信息:传输的字节数,完成键(单句柄数据结构)和Overlapped结构地址,线程是通过GetQueuedCompletionStatus返回这些信息,等待CPU的调度。
调用的顺序
调用GetQueuedCompletionStatus的线程是后进先出的方式唤醒的,比如有4个线程等待,如果有一个I/O,最后一个调用GetQueuedCompletionStatus的线程被唤醒来处理。处理完之后,再调用GetQueuedCompletionStatus进入等待线程队列中。
线程并发量:
并发量限制了与该完成端口相关联的可运行线程的数目, 它类似阀门的作用。 当与该完成端口相关联的可运行线程的总数目达到了该并发量,系统就会阻塞任何与该完成端口相关联的后续线程的执行, 直到与该完成端口相关联的可运行线程数目下降到小于该并发量为止。所以解释了线程池中的运行线程可能会比设置的并发线程多的原因。
它的作用:
最有效的假想是发生在有完成包在队列中等待,而没有等待被满足,因为此时完成端口达到了其并发量的极限。此时,一个正在运行中的线程调用 GetQueuedCompletionStatus时,它就会立刻从队列中取走该完成包。这样就不存在着环境的切换,因为该处于运行中的线程就会连续不断地从队列中取走完成包,而其他的线程就不能运行了。
注意:如果池中的所有线程都在忙,客户请求就可能拒绝,所以要适当调整这个参数,获得最佳性能。
线程并发:D线程挂起,加入暂停线程,醒来后又加入释放线程队列。
线程的安全退出:
PostQueudCompletionStatus函数,我们可以用它发送一个自定义的包含了OVERLAPPED成员变量的结构地址,里面包含一个状态变量,当状态变量为退出标志时,线程就执行清除动作然后退出。
完成端口使用需要注意的地方:
1.在执行wsasend和wsarecv操作前,请先将overlapped结构体使用memset进行清零。
借用大佬俩张图:
采用accept方式的流程示意图如下:
采用AcceptEx方式的流程示意图如下:
写在最后,自己对网络编程还是没有了解的很透彻,本文主要是根据《Windows核心编程》第十章和大佬们的文章总结的,希望未来能有更多自己的感悟和认识。