稳定的完成端口开发细节讨论

来源:互联网 发布:linux web扫描工具 编辑:程序博客网 时间:2024/05/17 02:49

 

完成端口做为windows上最高效的网络编程模型,做为众多服务器网络层的首选。网上有很多参考资料和示例源码,大多存在问题,本文将以开发一个稳定易用的完成端口组件为目标,详细讨论开发过程中所遇到的细节问题,并给出相应的解决方案。阅读本文需要你有这方面的开发经验,对于IOCP的工作流程以及上层的应用有清晰的了解。

 

1。组件需要提供什么样的服务

2。线程相关的问题

3。内存相关的问题

4。怎样合理的发送数据

5。怎样合理的接收数据

6。连接关闭的处理

7。完成键的选择

 

 

1。组件需要提供什么样的服务

     一个易用的组件,提供的接口并不复杂,只需要向上层提供连接通知,断开通知,数据收到通知和数据发送4个接口既可。对于每一个连接而言,组件需要向上层给出一个唯一的标识,在这里我们选用SOCKET作为标识。每一个事件(连接,数据收发,断开)通知,都将与该标识关联。

 

2。线程相关的问题

    工作线程(GetXXX)按MS的推荐做法,CPU*2吧,检测投递请求是否用完的线程是必须的,另外我们还需要一类线程,就是接收数据通知线程,为了提高网络接收数据的效率,网络层在接收到数据后并不立即通知给上层应用(防止上层应用占用过多时间),而是放入一个缓存中,这个线程的任务就是从缓存中读取接收到的包,通知给上层应用。线程个数可以根据用户需要指定。

    是否需要线程池? 首先我们知道线程池的作用是降低创建管理线程的开销,适用于频繁的销毁创建线程,在这里,我们的线程个数基本上是固定的,生存周期是在组件启动到停止的整个周期,个人认为,线程池是没有必要的。

 

3。内存相关的问题

     IOCP随时都有大量的请求,所以内存池是必须的,关于内存池的极限高效实现比较复杂,本文暂讨论简易一点的内存池,就是用标准库容器(如map)去管理分配的内存,并设定阀值,在空闲内存超过一定数量的时候,就delete掉它。由于我们需要的内存块大小有好几种,这个内存池需要可以指定块的大小。

 

4。怎样合理的发送数据

     在网络上有不少关于数据只发送了半包情况的讨论,有的说不存在,理由是要么将整个请求完成,要么就将请求操作置为失败;有的说存在。由于TCP缓冲区的大小是有限的,而投递发送请求的大小不确定,IOCP本来就提供了完成字节数的参数通知,个人认为,这种发送出半包的情况理论上是存在的。这就引出一个问题,我们要对一个客户端发送一个数据的时候,万一它的上一次发送请求还不知道有没有成功怎么办?有两种解决方案,第一种在发送的时候检测上一次发送是否成功完成,没有完成则等待,直到检测到上一次发送请求成功以后再发送,第二种是对每一个连接建立一个发送队列,在发送的时候,如果这个连接上没有未决的发送请求则直接发送,如果有则放入发送队列。工作线程收到发送完成通知时,检测该连接发送队列是否有数据,有就将它取出来继续发送。在这里需要对这个连接的发送管理进行同步。这种发送的思想就是在同一时间同一连接确保只有一个发送请求处于pending状态。

 

5。怎样合理的接收数据

    网上有许多人在讨论IOCP由于多个工作线程导致接收数据乱序的问题,很明显,如果有多个pending的接收请求存在,肯定会存在这一问题,这需要对数据进行组包,这是由于多线程的原因引起的数据包乱序。组包就需要对接收数据区域同步,效率和复杂性???在这里推荐另一种实现方式,同发送的思想一样,就是在同一时间同一连接确保只有一个接收请求处于pending状态,我们在一个连接上收到一个数据以后,再投递接收请求,这样就不存在数据包乱序的问题了。当然,TCP发送的流式数据,我们需要处理粘包问题,这个比较简单,我们在接收的时候确保至少接收到一个完整的package(用户自定义)才交给接收缓存,否则就继续接收吧。数据包里面至少要一个特定的标识以及一个标识包长度的变量。

 

    也许有人会觉得只投递一个请求是不是完全丢掉了IOCP的优势了,其实不是的,只是对单个连接是这样的,而对于众多连接,它的请求还是很多的,对于总体性能是没什么影响的。既然选择了完成端口,就是说明你需要对比较多的客户端提供服务啊。

 

6。连接关闭的处理

    这是一个麻烦的事情,因为一个连接总会有那么点儿事儿吧(发送,接收。。。),要确保发送和接收处理过程中,与这个连接相关的资源(PER_HANDLE)不能被释放吧,而工作线程又是多个,这里可以采用引用计数机制进行同步处理。当然有那么一点点麻烦。在介绍完完成键的选择以后,我们再讨论另一种方式。

 

7。完成键的选择

     看了一大部分资料,很多都选择以PER_HANDLE为KEY,这样做从表面上看很合理,实际上在资源释放(连接关闭)处理的时候会带来麻烦,如果关闭的时候就已经释放了这个资源,那么GetXX返回的KEY就成了野指针,不崩溃都不行啊。虽然可以通过一些开发技巧来同步处理这个问题,但是问题就变得复杂了一些了。在这里,我们推荐以SOCKET做为完成键,每检测到一次完成通知的时候,就去连接容器里面查找它的PER_HANDLE数据是否存在,这样就不会出现野指针了。

     回到上一个连接关闭问题的处理,在这里我们选择了SOCKET做为KEY,野指针的问题我们解决了,接下来我们需要解决释放这个PER_HANDLE的问题,前面说过可以用引用计数解决这个问题。在这里推荐另外一种方式,TR1库里面的shared_ptr,一个线程安全且可以自定义删除器的智能指针,其实它内部也是一个引用计数,只是有现成的岂不是更爽?用shared_ptr管理这个PER_HANDLE,当没有任何东西引用这个HANDLE 的时候,它会自动释放,这样在关闭处理的时候,只需要将它从连接容器里面清除掉,这时它的引用计数就减1了,如果没有别的地方在引用它,这个时候它就该执行释放操作了。如果有别的地方在引用,那么在引用结束以后会自动执行删除操作,这样写出来,程序不仅简化,而且条理清晰,更重要的是,稳定啊!

 

   今天能想到的就写到这里了,改天想到了别的,就继续写,以上只是个人总结思考出的一些解决方案,有不完善的地方还请大家多多指教。期待一起讨论啊!!!!本人已经实现了一个这样的IOCP组件,基本上的难点就是上面所讲的,如果大家有兴趣,就把源码发出来和大家共同进步。