« April 2013 | Main | June 2013 »

May 26, 2013

skynet 的网关模块的一点修改

skynet 有一个叫做 gate 的模块,用来解决外部连接数据读取的问题。它最初是用我随手为 ringbuffer 示例 而写的一段代码改造成的。

最初我认为,用 epoll 去处理读事件足够了。至于写数据,完全可以用阻塞写的方式进行。因为 skynet 可以将事务分到多线程中,所以特定几个 socket 发生阻塞,也并不会把系统阻塞住。也可以简单的理解为,单线程读,多线程写。

随着我们的游戏的开发,这样做的弊端逐渐显露出来。大量玩家聚集的场景里,广播数据会突然同时塞满多条连接。skynet 的工作线程数据固定,这样就有可能因为同时写数据而阻塞住所有的工作线程。

这个问题发现有一段时间了。当时蜗牛同学顺手改了一下,把外部连接设为非阻塞状态,并调大了缓冲区。一旦发生缓冲区慢的情况,就关闭连接。

这个改法并不彻底,而且在关闭连接后未能发送消息通知 agent ,导致业务逻辑不正确。上周仔细考虑了一下,决定把当初偷懒而没有完成的代码重新实现一下。

一开始,我在发送数据的模块中增加了写缓冲区。一旦系统缓冲区满,就放进应用层的缓冲区。等下次发起写请求时,再将以前没有发生完的数据发完。对于网络游戏来说,这样做基本够了。但隐患是,有可能最后一小片数据永远没有发送出去。(在 MMORPG 中,这种可能性几乎为零,即使发生,对用户也没有太坏的影响)

为了堵住这点纰漏,我设置了超时继续的 timer 。一旦应用层缓冲区有数据,timer 会在一定时间后处理它。

但这个做法引起了晓靖同学的不满。他觉得不统一用 epoll 来解决大量写事件而用 timer 这种事情是不可接受的。OK ,我也认同这点,但我这不是不想去动已经跑的好好的代码么?

周末还是忍不住动手了。

一开始,我想另外写一个模块整合所有的写事件。不过,读写模块分开有个问题:当用户想关闭 socket 时,它无法知道应用层写缓冲区里是否还有没发完的数据。对于 MMORPG 应用来说,有数据没发完就断开连接也不是啥大问题,不过对于 skynet 这个层次的东西来说,这样做粗暴了点。

我也想过另一个方案:重新做一个模块,利用 epoll 监听读写事件,并把事件发送给需要的服务,而自己并不真正读写数据。这样可以完美的把系统的 socket 读写 api 转换为 skynet 友好的接口。这样做的坏处是,需要更大的内存缓冲区(因为每个连接独立了);对现有代码改动较大;多了一个间接层,可能对性能有些影响。

我认为这可能是最佳方案,如果这样做,还可以把原来 skynet 中别的 IO 操作的部分也整合在一起。但最后我还是放弃了。

昨天,我决定把所有的 socket 写操作都统一交给 gate 模块处理,并保留原有分开处理的方式做兼容(等新代码跑一段时间,再去掉旧代码)。以前的发送模块继续保留,为一个连接设立一个服务负责发送数据到客户端。但它们不再直接写 socket ,而是将数据打包转发给 gate 。

这样修改后数据的处理流会多一次复制的环节,但考虑到日后还需要对数据加密和压缩,这个环节倒不算冗余。

代码写好后,已经提到到 github 。kqueue 的部分暂时还没有环境测试,希望有测试环境的同学可以帮我看看 :)

May 16, 2013

招聘 Windows/Linux SA 一名

5 月 30 日:从发布消息至今,我们已经通过 email 前后收到 9 位同学的自荐材料。目前我们公司的 HR 同学正在进行后续的工作。到今天,这个职位的对外招聘阶段结束,感谢大家的支持。


最近我们代理的游戏 狂刃 开始二测了,忙坏了我们的 SA Aply 同学。

Aply 是我的大学校友,低我一届,在学校门口的网吧认识,到现在已经有十多年。前年底我们的公司 简悦 成立时,他就找到我想一起干点事情。我们正好缺一个 SA ,他便从程序员转职了。从开始的各种不熟悉,他迅速的承担了所有 SA 的工作,得到全公司同事的认可。从今年开始,狂刃一步步发展,已经不再是一两台服务器的管理工作,一个人做这些事情已经显得太繁重了。

为了让 Aply 同学能有正常的假期和休息时间,我们想再招聘一名系统管理员,协助 Aply 同学的工作。职责主要是负责狂刃游戏的服务器运维。

狂刃的服务器暂时工作在 Windows 下(我们正在协助移植到 Linux 平台,但需要一段时间),所以需要有 Windows 平台的系统管理经验。

我们公司未来自研发项目全部在 Linux 下,所以也需要用 Linux 平台的经验(不必须特别丰富,但需要有学习的热情)

对这个职位的要求是:

  1. 有系统管理员工作经验三年以上。
  2. 熟悉 Windows 平台,对 Linux 有一定了解。
  3. 懂得如何批量部署,监控多台服务器(工具不限)。
  4. 懂得如何编写脚本(不限语言和工具)进行系统管理工作。
  5. 了解基本的网络协议。
  6. 可以 24 小时响应突发事件,并能承受突发事件带来的额外工作强度。

有兴趣的同学可以 email 和我联系了解更多细节。

介绍几个和 Lua 有关的东西

最近有份工作是需要把 Lua 中的数据结构以某种特定的格式输出为文本的,所以就用到了 Lust 这是个代码生成工作的利器。

可能是用的人不多,所以还略显不完整。用的时候发现一些个小问题,原来以为是 bug,读了源代码后发现是个 feature 。但是觉得这个 feature 不太合理,就上 github 上留言。作者倒挺爽快,马上表示赞同并去掉了。


第 2 个是 luaffi 。这个东西原本是 luajit 的一部分,可好多人确是冲着 ffi 库去用 luajit 的。

luajit 目前尚有不少的局限性,比如内存只能用 32 位寻址,不支持 lua 5.2 的 api 等。另外,从稳定性上来说,也不如原版的 lua 更让人放心。

据我所之,我们合作的狂刃 的服务器端就为了 ffi 使用了 luajit ,却担心稳定性问题,把 jit 功能关闭了。

还有 luaclang 这类项目,未必是稀罕 luajit 的性能,更多的是贪图用 ffi 写 binding 的便捷才启用 luajit 的。

把 ffi 库从 luajit 项目中拆分出来做成独立库,绝对是对 lua 社区的功劳一件。luaffi 在 windows 下,如果用 mingw 编译会遇到一些小麻烦。自己改一下 Makefile 并定义 -DWIN32WINNT=0x500 才能顺利 build 出来。


第 3 个是 terra 。它不是一个 lua 的库。严格来说,它是混合了 lua 语法的一门新语言。

比较独特的是 terra 的工作方式。

当你用虚拟机去运行代码时,它是解释运行那些 lua 代码的。但你又可以在运行时编译(通过嵌入的 llvm 模块)代码为本地指令。并且可以保存为 .o 或执行文件。

编译后的代码,无论是 terra 还是 lua 部分(我猜测的,接触 terra 时间比较短,还没有深入研究)都被翻译了。

terra 本身是一个静态类型的编译型语言,和 C 一样,是手工管理内存的。它有很高的运行效率,又可以方便的和 Lua 交互。terra 函数直接就是 first-class 的 lua value .

从语法上看, terra 受 objective-C 很多影响。我暂且认为,objective-C 发明了一套新语法,并兼容了 C ;而 terra 尽量少发明语法,而借用成熟的 Lua 语言,同时也可以方便和 C 代码混用。

terra 比较奇妙的特性是可以(利用 llvm )方便的扩展语言,定制 DSL 。

总之,terra 是个有趣的东西,值得多花一些时间了解。btw, terra 整合了 luajit ,但目前看来,没有使用luajit 的扩展 api ,只是用了 ffi 部分。如果有必要,可以考虑用独立的 luaffi 库替换掉 luajit 。

May 06, 2013

招聘 Lua 开发人员一名

5 月 16 日 注:由于已经收到足够多的简历,所以招聘提前终止,谢谢大家的热情。


我们公司 简悦 招聘网络游戏服务端开发人员一名(截至到 2013 年 6 月 1 日)。

基本要求:有至少原创 1000 行以上 Lua 语言编程经验,一万行 C/C++ 语言编程经验。有网络服务开发经验:可以独立解决问题(包括但不限于设计合理的通讯协议,评估其效率及安全性)。

有游戏行业从业经验两年以上可以加分。

有兴趣且满足基本要求的同学,可以 email 和我联系获得更详细的信息。

May 05, 2013

XOR 链表

前两天阿楠同学想用链表实现一个消息队列,虽说是链表,但是没有从中间删除节点或添加节点的需求。只需要先压入的消息先处理。为了完成这个需求,最后实现了一个双向链表。

当然这个东西是用 Lua 写的,做一个双向链表也不算复杂。采用单向链表也可以做到,只需要多记录一个尾节点。不过今天想说的是,如果这个玩意在 C 里实现的话,其实只需要用一个指针的空间就可以搞定一个双向队列,可以从任意一头进出数据,可以以两个方向遍历,而实现需要的代码量却和单向链表类似。因为算法非常有趣,在 C 语言之外很少见到,知道并使用过的人也就不太多了。这就是所谓的 XOR Linked List

我们不需要在链表节点上保存前序和后继两个指针,而改而保存这两个指针的 XOR 值。把两个指针强制转换为 uintptr_t ,做位运算 ^ 即可。

这个数据结构可以工作起来,依赖位运算的一个特性: A^(A^B) == B ,我们知道前序地址或后继地址中的任意一个,都可以用这个值推算出另一个来。这样的链表,从前向后与从后向前遍历的算法是一样的,区别只在于初始参数。

每个链表对象,我们保存链表的首节点和尾节点的指针。由于首节点没有前序节点,它的链接信息只需要记录后续节点( XOR NULL 不会改变它)即可。当链表内只有一个节点时,链接信息记录的就是它自己的地址了。

对于队列,我们只需要在新节点加入时,替换掉首节点,链接信息设为原来的头节点,并把原头节点的链接数据域 xor 上新加入的节点地址即可。

而出队列的时候,只需要用尾节点的链接域替换掉它并用 xor 更新新的尾节点链接域的数据即可。


XOR 链表的实现很巧妙有趣,但并不意味着实现很复杂。它比传统的双向链表实现起来要简单的多(甚至更容易实现为无锁的数据结构,传统的单向链表很容易实现为无锁数据结构,而双向链表就麻烦的多 )。当然,局限性也很明显。

它很难在 C/C++ 之外的其它语言中实现,而且链接信息很隐讳,会导致调试起来更困难一些。

我们几乎只能在遍历链表的过程中去插入和删除单个节点(当然很多情况下,我们也只需要在这个场合做这些事情),插入一个节点需要同时知道两个节点,我们才能把新节点插入其间,删除节点也必须有它前或后的节点信息才行。

但这个数据结构在某些场景毕竟是有用的,比起用 xor 交换两个变量来说,XOR 交换算法 才真的像道智力题。