前言
距离第一次发布iocpframework已经三年多了,这期间这款基于windows的网络库经过多次的修改,已经和第一版有非常大的区别了,但是整体思想架构并没有改变,这得益于当初对下载调度器的认识--模块应该是可替换的,这也是后话了。
在阅读本文的时候,希望你能有以下的基础:
- 熟悉C++11 ,auto、decltype、lambda、type_traits、move等
- 熟悉Windows的网络编程、IOCP模型(IOCP仅仅是个异步队列 blocking queue,当然不止一个队列)
如果你符合以下描述,那么恭喜你,本文就是为你而写的:
- 追求自由,热爱自由
- 对网络proactor IO模型细枝末节摸棱两可
- 对IOCP机制感到非常模糊及细节难以把握
- 对C++充满热爱与激情
如果您具有以下特征之一,那么本文可能会给你带来不适,请谅解:
- 披着C++外衣的Cer
- 反模板联盟
- 反感自造轮子
iocpframework的github地址:https://github.com/chenyu2202863/iocpframework/
换句话说,我所走的弯路,你一样得走(哈哈,被坑了),我所路过的捷径,也希望会成为你的捷径。祝你好运。需要C++11编译期支持,至少VS2012 CTP补丁才能编译
一个迷你的服务器
设置好监听端口,启动。等待accept事件,针对session投递读请求,等待读事件,返回写数据。麻雀虽小五脏俱全,再来看看其中涉及到的设计思考及实现细节。
网络库应该提供的功能:
- 3个半事件,详情请参考陈硕的《网络编程本质论》
- 可扩展的内存分配方式
- 对网络错误处理
- 异步、同步IO
网络库避免限制的决策:
- 对session集中式管理
- 对接收缓冲区与发送缓冲区的强制
- 需要做到协议无关
正如《UNIX编程艺术》所说:提供机制,而不是策略
与第一版的变化:
- 性能大幅度提高
- 接口更加灵活
- 增加socket pool
- 增加zero copy机制
- 增加timewheel
- 改用WinVista以后API,不支持WinXP
库结构
设计实现剖析
一、service部分
dispatcher
类似于asio的io_service,同时提供线程池,负责数据的调度。
这里,采用的是GetQueuedCompletionStatusEx,支持同时多个回调事件的处理,而且发生的错误信息是由OVERLAPPED_ENTRY里的Internal字段提供的,这个是文档里没有告诉我们的。这样来处理,可以不用那么麻烦的判断GetQueuedCompletionStatusEx及GetLastError的返回值的组合。
async_result
这是提供给socket、file、timer与dispacther交互的粘合曾,负责生命期的处理
定制了std::unique_ptr的释放方法,完成我们的RAII方式来管理资源。
async_callback_base_t是一个基类,抽象出调用接口及释放资源的接口
dispatcher通过call调用,把指针转换为async_callback_base_ptr,然后通过基类指针p调用invoke,invoke只关心错误信息与事件回调时的数据大小(就是read、write所实际产生的大小),再来看看实际干活的async_callback
持有外部传入的Handler和一个Allcoator,Handler就是用户所关心的callback,Allocator负责对象的分配释放.每当向系统投递一个异步请求的时候,就会make_async_callback,把handler、allocator传入win_async_callback_t里持有。这里就是简单的动多态与静多态的一个结合案例。就这样,通过async_callback_base就把dispatcher与file、socket、timer分离。所以说,计算机界的问题都可以引入一个间接层来解决。
再来看看zero-copy机制的设计实现
首先,基于C++11提供的move语义和fsocket支持非连续性内存的写入(WSASend支持多个WSABUF)。其次,需要在编译期推导传入需要写入数据的类型及个数,这会影响到WSABUF数组的个数。
把callback handler及parameters打包到param_t对象中,param_t根据参数,编译期推导出不同数据类型的个数
buffers接口就在完成填充std::array<WSABUF, details::args_count_t<tuple_t>::value>
在这里,为了能支持用户对zero_copy的扩展,可以去特化 ,默认支持了原生类型、用户POD类型及vector、string,比如string的特化版本如下
arg_size是这个类型对需要占用几个WSABUF,因为string需要一个长度和内容,所以这里arg_size = 2。
二、network部分
两个问题
- server分几层设计实现,每一层干什么事情?
- 对外暴露的server接口提供哪几个?
我的看法是,server分两层。第一层负责socket的连接断开、socket的生命期管理(当然不需要进入一个容器,采用锁机制,这是很笨拙的方式)、数据收发方式。第二层负责buffer缓冲区处理,及协议相关处理,同时需要负责心跳的活。来看开始给出的迷你服务器原型
一个服务器,首先应该考虑的是错误信息的处理。在这里,error_handler_t就是错误信息的处理回调函数,在这里还需要考虑接收到数据包后,返回给上层,所以也需要一个数据包处理回调。
register_callback提供注册socket连接与断开的接口,有些场景是不关心这两个事件的,所以这两个接口需要单独设置,我们的server现在就差start(port)了。
在这一层中,提供了心跳管理,接收缓冲区(发送缓冲区不需要管理,因为我们的库提供了zero copy机制及用户保证buffer生命期两种方式)。
对于timewheel,大家可以去参考陈硕这篇文章,非常棒!《用timewheel踢掉空闲》。
multi_buffer_decode_t是一个解码器,针对recv_buffer_里的数据进行decode,然后返回到用户设置的callback里。
再看看提供session管理及连接、断开处理的server层。在这一层需要有两个抽象,一个session、一个server。session负责socket的持有、收发动作、错误处理。server负责session的连接及创建。具体请查看源码,这里介绍下socket的连接处理及socket pool
利用了WSAEventSelect来检测是否需要投递socket,通过socket pool来获取可用的socket,因为socket的创建是很占系统资源的。关于object pool的文章,前一篇讲过。
使用socket pool需要注意的是,必须使用DiseconnectEx,而这个系统的回调不是立马返回,而是需要在能正确断开连接后才返回,微软给出的时间默认是4分钟,也就是TIME_WAIT的时间。所以需要复用的socket需要在session析构的时候才放回到pool里
在这里,使用了一个技巧,当session需要关闭的时候,取消掉已经投递的请求
session的生命期管理采用的是陈硕提供的方式,shared_ptr、enable_shared_from_this。自己管理自己,这样就避免了一个容器,避免了锁竞争。如下
更多的细节请到源码里找吧。
与ASIO对比
asio | iocpframework | |
跨平台 | 可 | 目前仅支持windows |
性能 | 好 | 更好,需要数据支撑 |
IO模型 | Proactor | Proactor |
支持zero copy | 否 | 支持 |
支持socket pool | 否 | 支持 |
内存分配方式 | 链式hook | 接口参数 |
其他方面 | 完胜 | 不是同一个目标定位,而且也是完败 |
未来的演化方向:
如果C++标准库加入网络库,估计这个库也就完蛋了.接下来会在performanceexample est进行更多的支持.接口也许会变
打个小广告,如果有兴趣,请加入
C++11的群: 165666547
iocpframework的群:54801033