网络库的设计与实现

前言

距离第一次发布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补丁才能编译

 

一个迷你的服务器

image

设置好监听端口,启动。等待accept事件,针对session投递读请求,等待读事件,返回写数据。麻雀虽小五脏俱全,再来看看其中涉及到的设计思考及实现细节。

 

网络库应该提供的功能:

  1. 3个半事件,详情请参考陈硕的《网络编程本质论
  2. 可扩展的内存分配方式
  3. 对网络错误处理
  4. 异步、同步IO

网络库避免限制的决策:

  1. 对session集中式管理
  2. 对接收缓冲区与发送缓冲区的强制
  3. 需要做到协议无关

正如《UNIX编程艺术》所说:提供机制,而不是策略

与第一版的变化:

  1. 性能大幅度提高
  2. 接口更加灵活
  3. 增加socket pool
  4. 增加zero copy机制
  5. 增加timewheel
  6. 不再支持Win XP

 

库结构

image

 

设计实现剖析

一、service部分

dispatcher

类似于asio的io_service,同时提供线程池,负责数据的调度。


这里,采用的是GetQueuedCompletionStatusEx,支持同时多个回调事件的处理,而且发生的错误信息是由OVERLAPPED_ENTRY里的Internal字段提供的,这个是文档里没有告诉我们的。这样来处理,可以不用那么麻烦的判断GetQueuedCompletionStatusEx及GetLastError的返回值的组合。

async_result

这是提供给socket、file、timer与dispacther交互的粘合曾,负责生命期的处理

image

定制了std::unique_ptr的释放方法,完成我们的RAII方式来管理资源。

async_callback_base_t是一个基类,抽象出调用接口及释放资源的接口

image

dispatcher通过call调用,把指针转换为async_callback_base_ptr,然后通过基类指针p调用invoke,invoke只关心错误信息与事件回调时的数据大小(就是read、write所实际产生的大小),再来看看实际干活的async_callback

image

image

持有外部传入的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数组的个数。

image

把callback handler及parameters打包到param_t对象中,param_t根据参数,编译期推导出不同数据类型的个数

image

buffers接口就在完成填充std::array<WSABUF, details::args_count_t<tuple_t>::value>

在这里,为了能支持用户对zero_copy的扩展,可以去特化image ,默认支持了原生类型、用户POD类型及vector、string,比如string的特化版本如下

image

arg_size是这个类型对需要占用几个WSABUF,因为string需要一个长度和内容,所以这里arg_size = 2。


二、network部分

两个问题

  1. server分几层设计实现,每一层干什么事情?
  2. 对外暴露的server接口提供哪几个?

我的看法是,server分两层。第一层负责socket的连接断开、socket的生命期管理(当然不需要进入一个容器,采用锁机制,这是很笨拙的方式)、数据收发方式。第二层负责buffer缓冲区处理,及协议相关处理,同时需要负责心跳的活。来看开始给出的迷你服务器原型

一个服务器,首先应该考虑的是错误信息的处理。在这里,error_handler_t就是错误信息的处理回调函数,在这里还需要考虑接收到数据包后,返回给上层,所以也需要一个数据包处理回调。

register_callback提供注册socket连接与断开的接口,有些场景是不关心这两个事件的,所以这两个接口需要单独设置,我们的server现在就差start(port)了。

在这一层中,提供了心跳管理,接收缓冲区(发送缓冲区不需要管理,因为我们的库提供了zero copy机制及用户保证buffer生命期两种方式)。

image

对于timewheel,大家可以去参考陈硕这篇文章,非常棒!《用timewheel踢掉空闲》。

multi_buffer_decode_t是一个解码器,针对recv_buffer_里的数据进行decode,然后返回到用户设置的callback里。

再看看提供session管理及连接、断开处理的server层。在这一层需要有两个抽象,一个session、一个server。session负责socket的持有、收发动作、错误处理。server负责session的连接及创建。具体请查看源码,这里介绍下socket的连接处理及socket pool

image

利用了WSAEventSelect来检测是否需要投递socket,通过socket pool来获取可用的socket,因为socket的创建是很占系统资源的。关于object pool的文章,前一篇讲过。

image

使用socket pool需要注意的是,必须使用DiseconnectEx,而这个系统的回调不是立马返回,而是需要在能正确断开连接后才返回,微软给出的时间默认是4分钟,也就是TIME_WAIT的时间。所以需要复用的socket需要在session析构的时候才放回到pool里

image

在这里,使用了一个技巧,当session需要关闭的时候,取消掉已经投递的请求

image

session的生命期管理采用的是陈硕提供的方式,shared_ptr、enable_shared_from_this。自己管理自己,这样就避免了一个容器,避免了锁竞争。如下

image

更多的细节请到源码里找吧。


与ASIO对比

  asio iocpframework
跨平台 目前仅支持windows
性能 更好,需要数据支撑
IO模型 Proactor Proactor
支持zero copy 支持
支持socket pool 支持
内存分配方式 链式hook 接口参数
其他方面 完胜 不是同一个目标定位,而且也是完败

 

未来的演化方向:

如果C++标准库加入网络库,估计这个库也就完蛋了.接下来会在performance\example\test进行更多的支持.接口也许会变

C++11的群: 165666547

iocpframework的群:54801033


阅读更多

没有更多推荐了,返回首页