« May 2016 | Main | July 2016 »

June 30, 2016

skynet 的一个简单范例

之前已经介绍过,skynet 只是一个轻量框架,不是一个开箱即用的引擎。能不能用好它,取决于使用者是否清楚知道自己要干什么,如果是用 skynet 做网络游戏服务器,那么就必须先知道网络游戏服务器应该如何设计。

在 skynet 发布版中带的 example 中,有类似 gate watchdog agent 之类的服务,它们并不是唯一的用 skynet 构建游戏服务器的模式。我想另外写一个范例,示范依旧基于 skynet 但用不同的模式构建游戏服务器的方法。

我花了两天时间写了这么一个 sample ,放在 github 上

在这个范例中,我主要想展示这样一些东西:

GateServer 并不是唯一的管理连接的模式。在 skynet 中,也可以自定义其它的方式来管理大量外部连接。这个例子中使用了前段时间我实现的另一个模块 ,这个模块并没有放在 skynet 发布版中。

在这个范例中,实现了一个 hub 服务,类似 gate 的作用。但是它仅仅监听端口,并把新建连接交给合适的服务处理。按范例中的流程,每个新连接都直接转交 auth 服务;只有 auth 服务认可了连接,再转给 manager 服务。

这里 auth 和 manager 都是单一服务。如果实际使用的时候有性能问题,auth 服务可以扩展为多个,做负载均衡。如果有必要,还可以加一个排队服务的环节。

manager 拿到连接的身份后,会根据身份分派 agent 服务代理这个连接上的请求。注意:这里的代码并没有简单的为已认证身份的连接启动一个新的 agent 。这也是很多对 skynet 缺乏了解的同学普遍的误解——skynet 一定会为每个链接创建一个独立的 lua vm 。

manager 管理若干 agent 的原则是,如果系统中没有为特定用户服务的 agent 存在,则启动一个新的。但即使这个用户连接断开,也不一定及时退出 agent 服务。agent 是否退出,是由 agent 自己决定的。manager 只负责将用户关联到活着的 agent 服务上。这个关联关系面向用户而不是面向连接的,多个连接可以同时通过 auth 认证,一起关联到同一个 agent 服务上(比如多客户端同时以不同连接接入)。

manager 服务目前实现的还很简陋,但是稍加改造,就可以支持把多个用户关联到同一个 agent 。比如,做棋牌服务器时,你可能让同一个牌桌的用户在一起会更好。

agent 服务可以用来处理业务逻辑。目前的范例中仅能处理 login 和 ping 请求。我们区分了 signin 和 login 。signin 表示用户已经通过了认证进入系统,但未必可以进行业务请求;而 login 表示被 agent 接受。这个范例里,如果一个用户 login 成功,在他的连接断开前,这个用户无法再次 login ;当然你可以稍微改造,变成后login 的用户顶掉前一个;或是让他们可以同存。


这个范例还提供了一个不同于 snax 另一个简单封装。展示如何不用 skynet 早期提供的具名服务方式,而使用 skynet.uniqueservice 来取代它们。

在这个范例封装中,只需要声明服务依赖的其它服务的名称就可以以正确的次序启动它们了。

封装层把 skynet.dispatch skynet.info_func 等在编写 skynet 服务时的繁琐步骤简化了,它的工作原理理解起来可能比 snax 要简单一些,用起来也很容易。


这次的客户端使用了一个开源的 lsocket 库,而不是 skynet 发布版中那个简陋的 clientsocket 模块。这能更好的展示怎么编写 skynet 的客户端。

同时,客户端中使用 sproto 协议的代码也更清晰一点,稍微做了一些封装,让代码比 skynet 自带的 example 更易读。

服务器部分和客户端交互的部分也有对应的封装模块。


关于客户端部分,我比较推崇只使用请求/回应模式,而不支持服务器推送数据。如果需要推送,可以用 long polling 解决。

在客户端,和服务器不同,它要同时面对用户 UI 的交互、图像渲染、以及网络请求回应。所以我觉得不适合把服务器的那套 rpc 机制直接搬到客户端。所以在范例中,我也并没有使用 coroutine 来做 rpc 调用。

callback 模式可能更适合客户端的工作。但 callback 并不是 rpc_call(request, cb) 这种。而是把 request (只可以从客户端发起,服务器永远只响应客户端的请求,而没有反向请求)的回应处理方法注册在一张表中。

比如,有一个叫做 ping 的请求,客户端先定义好:

function ping(req, resp, session)

然后在需要 ping 服务器(对于客户端来说通常是由用户 UI 操作引起的)时,local session = request("ping", req) 就可以了。当收到服务器的 ping 回应时,上面的 ping 函数被回调,可以接收到当初 request 时发起的 req 数据,以及服务器传回的 resp ,和 session 。

如果 ping 操作是无状态的,那么 session 多半可以忽略掉。在回调函数中,我们可以拿到提起请求时的内容 req ,也就不必再依赖其它状态了。

有部分流程,可能依赖多次和服务器交互。这种带上下文的交互,或许我们应该用 coroutine 封装一下这类 RPC 调用?但目前最常见的多次交互只出现在登录认证流程中,似乎不必为它单独做太复杂的东西。

June 23, 2016

正确的序列化 Lua 中带元表的对象

在 Lua 5.2 之后的版本,约定了在元表中可以给出一个 __pairs 方法,而 lua 的基础库 pairs 会使用这个元方法来迭代一个对象。

Lua 5.3 之后的版本,取消了 lua 5.2 中的 __ipairs 约定,而统一使用 lua_geti 来访问整数为索引的数组。

可惜的是,许多 lua 序列化库对此支持的并不好。今天我在改进 bson 的序列化库时,重新考虑了这个问题,看看这个序列化过程怎么做,才能更好的支持 lua 5.3 以后的约定。

在 skynet 中重新实现的 bson 库是这样做的:

json 和 bson 在规范中都区分了数组和字典。

那么,序列化时,应首先判断一个 table 是否有 __len 元方法,如果有,则表示它是一个数组。因为我们不支持把一个需要传入 bson 序列化的 table 同时当成数组和字典使用,如果实现了 __len 元方法,那么显然是希望把它看作一个数组。

然后,判断这个 table 是否有 __pairs 元方法。如果有,这表示它是一个字典,需要用这个元方法迭代它。

如果两个元方法都没有,那么这个 table 是个原生表,需要用额外的方法探测它到底是一个数组还是一个字典。这里采用的方法是,使用 lua_rawlen 获取一下数组部分的长度。然后调用 lua_next 传入最后一个数字索引,探测是否有其它的 key 。

这种方法在 lua 的文档中并没有严格约定,它依赖 lua 的实现。目前官方 lua 的实现中, lua_next 总是先迭代完数组部分,再迭代 hash 部分的。这样实现最为便捷,所以看起来也不会修改。而严格的检测方法则应该是用 lua_next 迭代所有的 key ,逐个判断它们都是否是数字,且是否连续。我不想采用这种开销 O(n) 的严格算法,前面提到的 O(1) 的取巧方法实际工作的很好。

这种,我们对 table 分了三种类别:数组、带元表的字典、原生字典。

数组这个类型不必区分是原生数组还是带元表的,而只需要取出长度(使用 __len 或调用 lua_rawlen ),然后用 lua_geti 逐个调用。如果发现数组中有空洞(value 为 nil ),序列化过程会抛出 error 。(这点对之前的实现是一个改进,老的版本并不能检测出数组中的空洞)

带元表的字典采用 __pairs 方法进行迭代,而原生字典可以直接用 lua_next 迭代。

June 07, 2016

skynet 入门指南

随着 lua 5.3.3 的正式发布,我不想再拖了,希望下周就完全冻结 skynet 1.0 的 rc 版本,正式发布。

鉴于 skynet 一直都没有专门的上手指南,过去写的 skynet 设计综述 太老,之后 skynet 又有了很大的变化,所以我想是时候补一篇面向刚接触 skynet 的同学的文章了。

我在 wiki 上补充了一篇 GettingStarted ,欢迎大家多提意见。特别是没有接触过 skynet 又有兴趣的同学,可以看看这篇文章是否讲清楚了 skynet 到底是什么 :)