那些年我们追过的网络库

计算机史前文明时代,y2k problem 后,世界难题 “c10k problem”

八仙过海, 各显神通

NPTL 还没有研究出来. 还不能创建成千上万个线程 windows 还在蓝屏中挣扎, 无暇顾及网络

UNIX提供了 select() 系统调用,只支持 1024 个fd, windows 只64个

这种背景下, IBM 带领着MS先搞了 IOCP

开源人有开源的做法, 在 NIH 综合症的影响下, BSD 的人敢为天下所不齿, 发明了 Kqueue, Linux 捣鼓出了 epoll

分裂, 让人头疼

都声称自己的新接口对 select 有质的提升, 是破解 c10k 问题的不二法宝,你用也得用, 不用也得用

为了让自己编写的网络程序能跨平台, 程序员开始了对3大各自为阵的法宝的膜拜学习

除了需要应对多套互不兼容的 API , 异步本身也需要更高级的抽象, 把程序员从编写异步代码的地狱模式里拯救出来

程序员们急需一个上天入地无所不能的法宝的法宝, 把这3家法宝给统御起来

率先站出来的是 ACE

ACE

ACE 支持 IOCP/kqueu/epoll/select/ 各种接口, 号曰没有不能跨的平台, 支持多种模型,在《Pattern-Oriented Software Architecture》有详细叙述

ACE 过于复杂,甚至比它试图封装的对象更复杂,

过度设计,以对象代替接口, 以虚函数代替回调,以继承代替组合,以虚类代替模板

对象间关系错综复杂,牵一发而动全身。除了作者,已经无人能参与 ACE 的开发了

libevent

异步事件框架,从 OS 那里获得事件, 然后派发。派发机制就是“回调函数”

iocp也好, epoll也罢,都只是用来获取事件的接口

libevent 去掉了ACE华而不实的包装,保留了异步事件,极大的简化了模型

libevent把简单问题简单化,让异步网络编程反朴归真,本是一个好库

然而因为设计缺陷,例如使用全局变量,定时器无法处理时间跳变,诸如此类的设计缺陷导致了 libev 的出现

libev

libev 已经够原始了

向下改进还不如让人直接使用系统的 api

向上改进,一是会导致和libevent的重叠,二是很快就碰到了 C 语言强加的禁锢

C 缺乏必要的抽象能力,编写异步程序,就如同安迪拿着小锤子琢开肖生克监狱的墙壁一样。能,但是要耗费巨大的精力和时间

编写异步程序, 最需要的2个抽象能力, 其一为协程,其二是函数对象,包括匿名函数对象, 也就是lambda

C统统没有。函数对象是实现闭包比不可少的,如果没有函数对象, 就只能通过携带 void* 指针的形式迂回完整,繁琐不说,还特别容易出错。程序里也到处充满了类型强转。到处是临时定义的类型,就为了传给 void* 使用

尽管C 有那么多缺点,然而 libev 还未来得及被C的缺点拖累,因为不支持 IOCP

于是 libuv 就出来给 libev 擦屁股了。 支持了 iocp 后的 libuv 就真的只有 C 本身的缺点了吗?

libuv

可以说是 C 语言的异步库所能达到的最高高度了。完完全全的触碰到了C语言的自身瓶颈,libuv 只是 nodejs 的底层库,上层软件转移到 javascript 语言而逃避了 C 的禁锢

libuv 自身缺点呢

一个网络库好不好,就看他有没有正确的处理 TCP 关闭, read write 实现的对不对

libuv 的 uv_write 没有返回值,允许空回调。也就是忽略write错误。 网络出错的情况下, libuv 的用户只能稀里糊涂的知道出错了, 至于错在哪?数据到底有没有发出去了? 一概不知道。 把数据交给 uv_write 后,就是一笔糊涂账了

ASIO 腾空出世

那还是 SARS 病毒肆虐的年代,悉尼的学子少年开始拜读 ACE 的大作。那时候,没有 libuv 没有 libev 更没有 libevent . 有的只是 ACE

少年没有跟风陷入 ACE 崇拜,他以敏锐的目光察觉到了 ACE 的弊病。 ACE 哪里做的不好?又哪里是值得借鉴的?

提出 ACE 6 个模型之 Proactor 模型乃最优模型

IT 界赢者通吃律,一个优秀的网络库,只需要支持 Proactor 模型即可。 支持其他次优模型都是徒劳的。ACE 试图全盘通吃,犯了大忌

网络库不宜做成框架,而是要像系统的API那样,作为一个乐高积木。ACE 做成了一个框架,同样不妥

你说了那么多 ACE 不好,有本事你弄个好的啊? 批评者向来都是这么理直气壮

正如 ACE 的作者实践了 “纸上得来终觉浅 绝知此事要躬行” 一样,这位勇敢的少年也拿出了 ASIO, “实践出真知”

asio 才三岁的时候,作者就将 asio 引荐给了 c++ 委员会的老人们。上一次他们接纳了 STL

ASIO 放入 Boost 锻炼, 经过 Boost 十余的锻炼,在 2017 年进入了 c++ 标准

Why Proactor

为什么 Proactor 会是最佳模型?

  • 跨平台

    许多操作系统都有异步API,即便是没有异步API的Linux, 通过 epoll 也能模拟 Proactor 模式

  • 支持回调函数组合

    将一系列异步操作进行组合,封装成对外的一个异步调用。这个只有Proactor能做到,Reactor 做不到。意味着如果asio使用Reactor模式,就对不起他“库” 之名

  • 相比 Reactor 可以实现 Zero-copy

  • 和线程解耦

    长时间执行的过程总是由os异步完成,应用程序无需为此开启线程

Proactor 缺点就是内存占用比 Reactor 大。需要先分配内存而后处理IO

Reactor 先等待 IO 而后分配内存

相对的Proactor却获得了Zero-copy好处。因为内存已经分配好了,因此操作系统可以将接受到的网络数据直接从网络接口拷贝到应用程序内存,而无需经过内核中转

Proactor 模式需要一个 loop ,这个 loop asio 将其封装为 io_service。不仅是 asio的核心,更是一切基于asio设计的程序的核心

宇宙级异步核心

宇宙级异步模型里,一个异步操作由三部分构成

  1. 发起

所有发起操作都使用 async_ 前缀,使用 async_动词 的形式作为函数名

  1. 执行

异步过程在发起的时候被executor执行(系统可以是支持 AIO 的内核,不支持 AIO 的系统则是 aiso 用户层模拟)

  1. 完成并回调

发起 async_* 操作的时候,总是携带一个回调的闭包 asio使用闭包作为异步事件完成的处理回调,没而不是C式的回调函数 asio的宇宙异步模型里,回调总是在执行 io_service::run 的线程里执行。asio绝不会在内部线程里调用回调

在回调里发起新的异步操作,一轮套一轮。整个程序就围绕着 io_service::run 运转起来了

io_service 不仅仅能用于异步 IO ,还可以用来投递任意闭包,实现作为线程池的功能

这一通用型异步模型彻底击败微软 PPL 提案,致使微软转而研究协程。然而微软在协程上同样面临 asio 的绞杀

闭包和协程

宇宙级 asio 使用闭包作为回调,而 C 库只能使用函数+void*

ACE 虽然使用的 C++语言,却不知道闭包为何物,使用的是 虚函数作为回调。需要大量的从 ACE 的对象继承

以闭包为回调,asio支持了叫“无栈协程”的强悍武器。 asio无栈协程,仅通过库的形式,不论是在性能上,还是易用性上,还是简洁性上,甚至是B格上,都超过了微软易于修改语言而得的 await提案

微软,ACE ,并不是不知道闭包,而是在c++里实现闭包的宇宙级executor —— 也就是 io_service,需要对模板技术的精通

asio “把困难留给自己,把方便带给大家”,以地球人无法理解的方式硬是在 c++98 上实现了宇宙级异步核心

如果 c++11 ,c++17 早点出现,实现 asio 的宇宙模型会更加的简单 —— 其实这也是 c++ 的理念,增加语言特性,只是为了让语言用起来更简单

buffers

有了闭包的支持,内存管理也变得轻轻松松起来

ASIO 本身并不管理内存,所有的IO操作,只提交对用户管理的内存的引用,称 Buffers

asio::buffers 引用了用户提交的内存,保持整个 IO 期间,这块内存的有效性是用户的责任

然而这并不难! 因为回调是一个闭包。通过闭包持有内存,只要 asio 还未回调,闭包就在,闭包在,内存在

asio 在调用完回调后才删除相应的闭包。因此资源管理的责任可以丢给闭包,而闭包可以通过智能指针精确的控制内存。 不是 GC , 胜于 GC 千百倍!益于c++的 RAII机制,再无内存泄漏之忧!

ref

那些年我们追过的网络库