很久之前,就想接触异步通信,直到今天才真正的下定决心来好好学习学习。
异步是针对同步来说的,本来服务器应该与客户端保持同步的收发数据操作。但是这种模式的操作过程会导致服务端阻塞在同步事件上,而不能有效利用资源去干其他的事情。
Windows 消息异步通信
先从最简单的,微软关于异步操作提供的API函数来看。WSAAsyncSelect(SOCKET, HWND, UINT, UINT);该函数为通信过程提供了异步的处理方法,微软利用其消息机制的方式将网络通信事件以消息方式发送给对应的HWND窗体。
SOCKET client = Accept(ServerSock, (sockaddr*)&clientAddr, sizeof(clientAddr)); // 接收客户端连接请求
WSAAsyncSelect(client, m_hWnd, WM_SOCK_MSG, FD_READ|FD_CLOSE); // 指定接收客户端采用异步操作
此时,在m_hWnd所在的窗体响应WM_SOCK_MSG消息。在该消息响应中来执行相应的网络通信事件。
Select异步模式
第一个异步通信方式中,必须得依赖于一个windows窗体,由于消息机制是依赖与windows窗体。在开发过程中,经常需要开发一个网络通信的模块,并没有一个真实的窗体可以供开发者利用。当然开发者可以预留一个接受窗口句柄的接口。
开门见山,Select异步模式,主要是借用Select函数对已建立链接套接字进行监视。如果被Select套接字:(针对客户端相应操作)
被connect,则该套接字为可读状态;
被recv,则该套接字为可写状态;
被send,则该套接字为可读状态。
该模式下,牢记上述3点,便可以灵活应用了。
再来分析一下该函数: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);
nfds-----------> 该参数无用,完全是为了兼容其他版本添加
readfds------>可读套接字集合
writefds------>可写套接字集合
exceptfds---->检查有异常发生的套接字集合
timeout------->Select函数最大超时时间,该函数是阻塞函数。
通过Select函数可以将所有处于读状态的套接字填充到readfds集合中,同时将所有写状态的套接字填充到writefds集合中。
IOCP模型
IOCP又叫I/O完成端口,是性能最好的一种I/O模型。是应用程序使用线程池处理异步I/O请求的一种机制。它所解决的问题是,当有很多的客户端来服务器端请求数据时,往往服务器端需要开辟较多的线程来保证每条客户端链路的正常通信,所造成的线程及线程切换所造成的资源消耗。
该模型操作拥有一套流程,从网上抄录下来:
1:创建一个完成端口。
2:创建一个线程A。
3:A线程循环调用GetQueuedCompletionStatus()函数来得到IO操作结果,这个函数是个阻塞函数。
4:主线程循环里调用accept等待客户端连接上来。
5:主线程里accept返回新连接建立以后,把这个新的套接字句柄用CreateIoCompletionPort关联到完成端口,然后发出一个异步的WSASend或者WSARecv调用,因为是异步函数,WSASend/WSARecv会马上返回,实际的发送或者接收数据的操作由WINDOWS系统去做。
6:主线程继续下一次循环,阻塞在accept这里等待客户端连接。
7:WINDOWS系统完成WSASend或者WSArecv的操作,把结果发到完成端口。
8:A线程里的GetQueuedCompletionStatus()马上返回,并从完成端口取得刚完成的WSASend/WSARecv的结果。
9:在A线程里对这些数据进行处理(如果处理过程很耗时,需要新开线程处理),然后接着发出WSASend/WSARecv,并继续下一次循环阻塞在GetQueuedCompletionStatus()这里。
归根到底概括完成端口模型一句话:
我们不停地发出异步的WSASend/WSARecv IO操作,具体的IO处理过程由WINDOWS系统完成,WINDOWS系统完成实际的IO处理后,把结果送到完成端口上(如果有多个IO都完成了,那么就在完成端口那里排成一个队列)。我们在另外一个线程里从完成端口不断地取出IO操作结果,然后根据需要再发出WSASend/WSARecv IO操作。
IOCP的机制是由系统来维护的,该模型下事先已经开辟了多个线程,存储在线程池中,所有用户的请求映射到一个完全端口上,提前开辟的线程就会逐一的从完全端口上来获取客户端信息。这样就不需要对每一个用户开辟线程进行通信操作了,这样提高了线程切换资源,同时也提高了线程的利用率。
这个工作线程的创建也是有讲究的,一般为服务器的CPU数*2 + 2个线程。
SYSTEM_INFO si;
GetSystemInfo(&si);
for(int i = 1; si.dwNumberOfProcessors*2 + 2; i++)
{
CreateThread(FComplePort); // 创建线程,并将完全端口告知该线程函数
}
以上内容,参考:http://www.cnblogs.com/hanyuanbo/archive/2012/04/01/2427856.html