查看文章 |
近期初学winsock2……,编写了一个小小的使用IOCP的服务器测试程序。总结了一下程序的重点部分: 为何使用IOCP:1、传统的socket服务程序流程是这样: 创建侦听socket -> 绑定socket和IP、端口号 -> listen() -> 循环accept客户端的连接、收发数据recv/send 此模型很古董,俺觉得也没什么实用价值……因为accept()函数,revc()/send()都是阻塞式的,如果有客户端响应慢等情况,服务器线程会卡在这些函数上出不来,状似瘫痪。 2、多线程模型: 与前面的模型差不多一样,只是最后一步,accept连接以后,创建一个新的线程去与客户端交互。 此模型也不咋实用(我认为)……且不说主线程还是会卡死在accept函数上,单看如果有并发很多个客户端连接的时候,服务器创建了N多线程,时间都花在了切换线程context上,效率欠佳…… (正因为Microsoft发现过多的工作线程并不能提高工作效率,于是弄出了IO完成端口,以适应大规模IO交互服务) 3、异步IO模型: Windows提供了一些异步IO的设施供程序员使用,使用这些异步机制,可以编写出无阻塞,响应度比较好的程序。下表中列出了Windows下支持异步传输数据的设备(摘自《Windows核心编程》):
可以看出,利用Windows的异步IO机制可以实现socket的异步传输,因此下文中将把Socket当作一种“设备”,收发数据当作IO操作。 假设一个线程想向socket发出一个异步IO请求,这个请求被传给网络驱动程序,后者负责完成实际IO操作。当驱动程序在等待设备响应的时候,应用程序线程并没有被挂起,线程会继续运行其它有用的任务。 向底层投递异步IO请求比较简单,针对于Socket来说,使用WSASocket创建套接字时指定WSA_FLAG_OVERLAPPED标志,则该socket带有了异步传输属性,而后,使用WSASend()、WSARecv()代替原来的send()、recv()函数就实现了异步IO的投递。 使用这些函数投递异步IO请求时,必须传入一个OVERLAPPED结构的地址,该结构告诉底层驱动程序从哪开始传(偏移量)、以及传完了后用于通知应用程序的事件对象句柄,并且,当调用WSASend等函数投递异步传输完成后,不管操作系统以什么方式通知用户IO结束,用户都可以取回此次异步传输调用时传递给WSASend等函数的Overlapped结构,因此,它十分适合于被扩充,以携带用户定义的、异步IO完成后要用的数据,比如该客户的标识啦……等等。
由于每次异步传输都要使用一个Overlapped,因此,它代表了这次异步IO。通常,我们对其进行扩充以携带自定义的数据的方法有:
这样,由于ol_的首地址就是OverlappedEx的首地址,因此很容易在完成异步IO后,把取得的Overlapped指针强制转换回OverlappedEx。
此方法直接从OVERLAPPED派生出来自己的结构,传递指针时不用强制转换,比较方便。 到某一时刻,设备驱动程序完成了IO操作,这时,他必须通知应用程序数据已发送、或者出错。这些通知方法大体包括以下四种: 1、触发此次IO操作使用的设备对象。它允许一个线程发出IO请求,比如一个线程在socket上WSASend(),另一个线程在该socket上WaitForSingleObject以收到完成通知。缺点是当提交多个异步传输请求时,此方法失效,因为线程不知道通知它的是哪次传输成功了。 2、触发事件内核对象。 调用WSASend或是WSARecv时传入的OVERLAPPED结构中,可以创建一个事件内核对象,并将其赋值给hEvent成员,当此次异步传输完成时,hEvent将被触发。这时,如果有线程正在WaitForSingleObject(lpOverlapped->hEvent, ...),则该线程将会被唤醒。 此模型避免了上面的缺陷,但是如果不同时期投递多个IO请求的话,就有多个Overlapped结构,每个结构代表了一次异步操作,因此工作线程需要WaitForMultipleObject,而且需要将hEvent形成一个数组作为该函数的参数,由于异步请求数量在程序运行时是会变的,因此程序员编写工作线程时要维护这样一个全局的Overlapped的数组,还要做一些互斥操作。 3、使用可提醒I/O。WSASend或者是WSARecv最后一个参数是LPWSAOVERLAPPED_COMPLETION_ROUTINE类型,这是一个函数指针,当调用WSASend或是WSARead时,如果最后这个参数不为NULL,则IO完成时,该指针所指向的函数将被调用。《Windows核心编程》作者指出,可提醒I/O非常糟糕,应该避免使用,原因是 a)需要使用回调函数,这将使得代码的实现变得更加复杂,我们不得不将相关的信息放在全局变量中以便该函数参考。b)发出IO请求的线程必须同时对完成通知进行处理,如果一个线程发出多个IO请求,即使其它线程处于空闲状态,也无法帮助此线程处理消息。 4、使用IO完成端口。此乃最佳方案,允许向一个设备发出多个IO请求,允许一个线程发出IO请求,操作系统完成传输后,另一个线程对结果进行处理,下面详细讨论。 IOCP使用方法:IOCP全名叫做…… Input / Output Completion Port(……汗一个……),感觉他意思就是说,这是一个专门提醒别人“IO操作已经完事儿”的东东。因此,它内部有个设备列表,用户把某个socket传进IOCP时,它就把这个socket加入到设备列表中监视着。当用户调用了WSASend、WSARecv,设备驱动程序也完成了传输的时候,IOCP会监测到该设备上IO操作完事儿了,于是乎就提醒正在等待该消息的线程~基本就是这个过程……下面函数可以实现1)创建一个IOCP以及2)将某个设备加入IOCP的监测列表中:
下面这个函数实现工作线程在IOCP上等待IO操作完成的通知:
因此,工作线程一般都是这样:
示例程序:本人使用IOCP和WinSock2,编写了一个服务器验证程序,该服务器可以简单的接收多个客户端的消息,并传回“×××消息已收到”。程序流程如下:
其实,使用AcceptEx函数是可以投递异步Accept请求的,这样,当新的客户端连接进来时,操作系统会完成accept工作,并通知工作线程。但是这样做的话,你无法知道什么时刻会并行连接连接进来多少客户端,只能使用某种策略,当IOCP通知工作线程某个连接已完成时,看情况继续投递合适数量的AcceptEx请求给操作系统。并且,MSDN杂志某文章http://msdn.microsoft.com/en-us/magazine/cc302334.aspx指出,"it is best to have a separate thread that posts AcceptEx and is not involved in other I/O processing"。 故而,本程序新创建了个线程专门用于accept客户端连接,其实在此线程中还可以完成其它的后台工作,比如记录日志等。而主线程使用WSAEventSelect()函数注册了Accept事件,当操作系统发现有客户端连接时,将激发此事件,accept线程侦测到该事件,直接使用accept函数接受连接并创建了新的客户socket,这里由于已经知道有一个客户端连接进来了,因此调用accept函数也不会阻塞线程。 还可以使用Windows提供的默认线程池来管理工作线程,动态的调整工作线程的数量,由于Microsoft在Vista操作系统中重新规划了其线程池,因此XP下和Vista下编写方法、调用的API是不一样的,这里不再讨论了。详情可以参看《Windows核心编程》中线程池这一章的“在异步I/O请求完成时调用一个函数”主题。不同的是《Windows核心编程》第四版中描述了2000/XP下的线程池;《Windows核心编程》第五版中描述的是Vista及更高版本的Windows线程池。 程序运行截图如下:
程序在Visual Studio 2008 SP1 专业版下编的,为了图省事,基本上写在一个cpp文件里了……牛人看了不要雷俺,写的很烂……代码仅供参考……如果有不妥的地方,请留言告诉俺,俺好改正……。 |

