Sucha's Blog ~ Archive for September, 2017

17年9月5日 周二 00:59

有关 m_net 跨平台的小型网络库

最近一直在玩 m_net,当然总是处于修修改改当中。

很早就有参考 dyad 这个优雅到不行的网络库,C89 的语法,接口设计清晰明了; 仅针对 TCP 抽象为 Stream;对于 socket 的状态变化,设置为事件监听接收处理; 简单粗暴的 SDK 接收数据再发通知,buffer 地址以及数据 size 直接送到用户手 上,接收部分则随着数据增大,而不断 realloc 出来;读写接口还加上了方便的 基于 C string 的处理。

基于这一套网络库构造的 demo,简明得不行,比如首页的 echo server。

且后者还提供了 timer。这个 timer 的实现,其实基于 select 对于阻塞 socket 的监听。

m_net 历史,最早是从 13 年做游戏时弄的一个单独处理 socket 的接口,当时写 得很挫先不说。后来慢慢发展,看到了 dyad 后,大量参考了 dyad 的设计。比如 抽象了 win 下的接口,使用 select 处理三个平台的 socket 事件,不同的是, 一开始就包含了 TCP、UDP 的支持,一开始就是非阻塞的接口,对于 socket 则命 名为 channel,简称 chann。

但后来 m_net 在不同平台采用了 kqueue、epoll 的实现,接口以及处理上就很不 同了,代码量也增加了很多。状态关注点少了不少,从事件数量上就可以看出来。

最近 m_net 的更改,是将 C 这一层的接口整理了一下,增加了两个 utils。一个 是用于 resolve host 的函数处理 host name 到 ip 的转化(DNS);另外是单独 对于 ip:port 这样的字串,加入一个 parse 的接口,分割拿到 ip 跟 port。

把 C 的这一层命名为 core 之后,在 C 接口的基础上,抽象了 C++ 的 wrapper 对象,有三种 ChannAddr,Chann 和 ChannDispacher。

分别用来处理地址部分,chann 的数据、状态处理,以及所有 chann 的事件监听、 分发。抽象的结果,是在 C++ 这一层,比如针对 client、server 的处理,可以 类似 C 一样设置一个事件函数,或者继承 Chann 父类,封装专门处理 client、 server 的子类,所有的事件、数据都在子类的内部流转,容易理解,易看易懂。

C++ 支持了闭包后,单纯的 listen 接口不需要外建函数,或者派生子类,只需构 造一个闭包处理就好,方便了很多。

但是跟 dyad 还是有很大差距的,巨大的差距,在于对于数据的处理,m_net 不会 在 recv 事件(数据到达)的时候帮用户接收数据,所以用户任何时候去 recv 数 据,都需要自己准备一个 buffer,跟平常的 socket recv、send 没有什么不同。

这个代码量就出来了,且 buffer 处理的优雅与否,比如针对不同的 protocol 抽 象,是巨大的考验,我目前没有好的想法。

暂时是不想跟 dyad 一样帮用户处理了,我觉得这样也不好,比如不断读取然后 append 到已有的 buffer 上,大小不够了向系统要,不断 realloc 也很颓。

m_net 没有对 recv 先接收处理,但是对于 send 部分,如果 socket 只是反馈 would block,而不是其他的不可恢复的 error,则 SDK 会构造 buffer 存入用户 数据,待 socket 可写后再慢慢 drain 数据。

目前没有设置阈值,上层可以一直写,一直到内存耗尽,不知道底层其实 socket 虽未断开但已经无法写入很久很久了。

可以考虑设置某个阈值,比如 16M 的时候,给上层一个 EAGAIN 之类的,目前先 不管。

另外重要 feature,比如 timer 部分目前是没有的,由于所有 socket 都是非阻 塞,如果仅仅在事件循环的时候获取当前时间,并走一次 timer chain,极端情况 下,CPU 时间片占用是很可观的,这也是我一开始避免加入 timer 的原因。

如果要加入 timer,那在这种情况下,我觉得,需要定一个最小的时间分割,一次 循环,不多不少,就走一个 timer circle,比如 10ms,每 10ms 获取一次事件, 或者多次循环获取一次,然后矫正,这个问题不大。

这种方案下,CPU 时间片占用方面才可以接受,上层事件的精确度也能够保证。

只是目前 timer 部分接口,如果 mnet_poll() 上面增加调用,感觉也不不是很好, 毕竟很多时候,是不需要这个 timer 的。目前一个 -1 的值设过去,有事件就突 击处理一下,没事件就死等待好了。

所以 timer 部分的接口,是集成在 mnet_poll() 之上,还是给用户一个选择,单 独的设置并处理,还需要斟酌一下。