2013-09-25

    这里的 hive 和 skynet 都是云风大神的开源项目。skynet 是一个基于 actor 模型的开源并发框架。hive 是 skynet 简化并去掉了一些 “历史包袱” 之后重新设计的框架。go 是 google 开源的一门编程语言。

    为什么把这些东西放到一块呢?因为我看了一下它们的代码,发现很多地方有惊人的相似之处,这些正是大牛们长时间积累沉淀下来的东西,非常有价值,所以这篇文章将它们拿到一起分析一下。

    actor 模型和[[][skynet]]的文章,最新的 skynet 代码我没有跟进。按云风的说法,skynet 已在项目中使用,不太好做大的修改,所以重新写了 hive 项目。

    1. 更精简的核心部分 hive 相比 skynet 更加精简,比如说 skynet 做了 rpc 方面的东西的,而 hive 中去掉了。原因是像这种东西放在核心层做,使得核心复杂化,并且不一定能满足应用层的各种场景。不如移到上层去实现,保持精简的核心层。

    2. hive 直接绑定 lua 既然像 skynet 实现消息传递的核心代码后,后面一定会绑定一个动态语言做上层,与其单独做一个 lua 的服务模块,不如直接就绑定 lua。我觉得这个还是带来了不少好处的。可以看一下代码上的区别,这是以前 skynet 中对 actor 的定义:

    struct skynet_context {
    void instance;
    struct skynet_module
    mod;
    uint32_t handle;
    int ref;
    char result[32];
    void cb_ud;
    skynet_cb cb;
    int session_id;
    uint32_t forward;
    struct message_queue
    queue;
    bool init;
    bool endless;

    1. CHECKCALLING\_DECL

    };

    其中的核心就是 skynet_cb 和 message_queue,一个是回调函数, 个是消息队列。而在 hive 中变了,可以说 skynet 还是偏一点 C 的,而 hive 是 lua 的。它的 actor 是这么定义的:

    struct cell {
    int lock;
    int ref;
    lua_State *L;
    struct message_queue mq;
    bool quit;
    bool close;
    };

    它的 actor 就是一个 lua_State,一个 message_queue。

    一个 skynet_context 就是一个服务,在 skynet 中每个服务要写处理特定消息的回调函数。

    struct skynet_message {
    uint32_t source;
    int session;
    void * data;
    size_t sz;
    };

    这是消息的定义,然后是消息处理的回调函数:

    typedef int (skynet_cb)(struct skynet_context context, void ud, int type, int session, uint32_t source , const void msg, size_t sz);

    而有了 lua 则可以直接在消息中传 lua 函数了!这是一个质的飞越,灵活性大大提高。另一个 actor 中有一个 lua 运行环境,你要给它发什么消息,直接可以发一个 lua 函数过去让它执行好了。这个提升相当于从双方约定允许发送和处理什么消息,到 rpc 屏蔽底层细节的飞越。

    1. 网络消息的处理 skynet 中比较区分外部消息和内部消息,外部消息比如来自于网络的。它专门用了一个 gate 服务,所有网络消息通过 gate 进来,然后分发给各个 actor。gate 那边管理好所有连接,并且可以做 ringbuffer 的一些内存管理优化。但后面云风发现与外部进行交互是不可避免的,于是出现了各个服务可能不走 gate 直接处理网络服务,系统中也存在多个 epoll 的端口了,这样对效率有影响。

    于是,在 hive 中直接封装了 epoll 非阻塞操作,提供给上层同步的接口。

    还有许多其它的细节我也没认真地看。

    hive 和 go 的代码我都看过,发现有些想法惊人地相似。为了充分地利于多核的并发优势,它们都选择了协程,go 中是 goroutine,hive 框架中是借助 lua 的 coroutine,非常轻量。协程之间不会有加锁之类方式的处理数据依赖,不会通过共享内存来通信,而是通过通信来共享内存。go 中是 channel,hive 中每个 actor 都附带一个消息队列。如果遇到协和执行不下去了,则会暂时地将它 yield,直接条件满足时继续。go 中是通过分段栈实现保存一个 goroutine 的低开销,而 hive 更省,直接利用 lua 虚拟机。

    在底层实现上,它们都是开了几条物理线程,不停地取一个协程执行,如果要 yield 就将协和放到队列中等待时机重新拿出来执行。调度方面 go 要做得完善一些,毕竟 hive 代码量小。

    不过在保存上下文上,hive 更牛一些。据云风说保存一个 coroutine 只要 200 到 300B,每个 lua_State 不到 10K,而 go 的每个 goroutine 则至少需要 4k 以上,即使使用分段栈技术,所以还是没有 lua 轻量。只要是按栈去实现的保存上下文都不可能更轻量的,没办法。而且分段栈带来的很大一个负作用就是与 C 的兼容性,其实 cgo 并不那么好用的。lua 使用虚拟机的,与 C 的兼容性堪称完美。不过也不是完全没有代价,C 的数据与 lua 栈数据的传递也是一笔额外的开销。

    在网络处理方面,从 skynet 改版后的 hive 与 go 的做法是相同的,底层是 epoll/kqueue 机制的异步 io,上层提供给用户的阻塞的 io 接口。我觉得这才是人性化的方案,异步加回调那种绝对是反人类的。

    底层实现上也是相同,调用上层的网络 api 后导致阻塞,则会把当前的协程 yield 掉。有一个后台线程不停地做 poll,如果收到数据则会唤醒相应端口的协程。

    不同的地方是通道方面。Go 提供了 first class 的 channel,这个通用性更强一些。而 hive 则受了更多 erlang 的影响,每个 actor 绑定一条消息队列。

    虽然我是 Go 语言粉丝,毕竟不是低端脑残粉。看了 hive 的代码后,有时候我甚至觉得 hive 做得更好一些。完美兼容 C 是个很大的优势,比如说内存管理可以自己选择让 lua 垃圾回收或者自己手动管理。甚至,使用完之后,直接释放一个 lua_State 都不用进行垃圾回收。虽然 Go 也可以自己申请一块大内存后手动管理,但总不是像直接使用 C 那么爽。随便进行比较一下,代码量短小易控方面,hive 胜。coroutine 的开销上,hive 胜。DSL 设计上,hive 胜。内存管理的灵活性方面,hive 胜。

    最近想找点有意思的东西做,就想到了网游。找个适合研究的网游并不容易。无非就是直接开源的网游,开源的服务器框架,或者就是源代码泄露了的私服。

    先下了个 diamonon,这是个开源的网游。看了下 2d 界面,丑暴了。3d 的没编译出来。随便看了一下它的源代码,感觉比较挫,还停留在很早期的时代。IO 复用还用的是 select 做的,而不是 poll。服务器方便也没做任何区分,整个编译出来一个服务器文件,包括了 loginserver,gameserver,database。准确点说 database 也是没有的,数据直接持久化到本机的文件系统。

    然后是 planeshift,这个也是个开源网游,3d 的。编译到一半放弃了。想了想有名的 T 端 M 端之类的,C++ 什么的最无爱了,况且太大的代码量就没有去研究的欲望了。

    接着就是考虑源代码泄露的一些游戏,传奇什么的代码应该不难搞到,但游戏没玩过,研究起来就没什么意思了。其实找个自己玩过的比较熟的还是蛮不错的。最终回到了 darkeden。这个游戏我最熟习,玩了很久很久。10 年的时候也读过一点点服务端的代码,虽然版本比较旧。

    要是能找到一份服务端代码以及配套的客户端,还是可以玩玩的。初步计划,loginserver 可以用我熟习的 Go 语言写,gameserver 用 hive。数据库能避开就先尽量避开。前期先求一个能跑起来的东西,主要是把 packet 搞定。写了这么多,有点偏题了。目前只是计划,具体的实施得留到下一篇文章了…


    有疑问加站长微信联系(非本文作者)

    hive,skynet以及go语言 - Go语言中文网 - Golang中文社区 - 图1
    https://studygolang.com/articles/2601