skynet 的核心并没有规定怎样处理 TCP 的数据流,但在开发网络游戏时,我们往往需要按传统,把 TCP 连接上的数据流分割为一个个数据包。
将数据流转换为数据包,比较常见的做法是给数据包加一个长度信息,组装在数据流中。我个人比较推荐使用两个字节表示包长度。在 skynet 中提供了一个 GateServer 的模板来帮助用户实现这样一个网关。
这个网关模板采用推送的模式,一旦用户初始化完毕,就会自动分割连接上的数据流,按协议(两字节长度加内容)分割为数据包,回调处理函数。在回调函数中,你可以为新连接启动一个独立服务来处理这个连接上的请求,也可以在单一服务中处理。这些不在本文中讨论。
今天,我想介绍另一个模式来做这个分包的业务,下面给出的 example 相比套用 GateServer 模板可能更适合用户改造合适自己游戏的模块。
skynet 中启动和维持一个 C 服务都是非常廉价的。当然 lua 服务本质上也是一个 C 服务,但它之所以不那么廉价,是因为服务中启动了一个 lua VM 。如果 C 服务的数据结构简单,那么无论从内存角度,还是从 skynet 的消息调度器角度来看,都没有太大的负担。
我们可以为每个 TCP 连接都启动一个 C 服务来管理这个连接。对于连接上的输入数据流,这个服务负责解读两个字节的包头,并分割出数据包;对于输出数据流,这个服务负责给内部数据加上 size 打包。
当然,如果你愿意的话,还可以在这里做通讯的加密和解密工作。
和 GateServer 的推送模式不同,我们可以使用简单的请求回应模式。应用层(通常是 agent)向这个 C 实现的连接代理服务提起一个请求,索要一个数据包;如果当前数据包尚未接收完整,或没有新的数据,这个代理服务简单的挂起这个请求即可。每次从连接上分离出一个数据包后,就从未完成的请求队列中弹出一个请求,回应它。
有了这么一个连接代理服务,使用的 API 要简洁的多。只需要不断的请求新的数据包,一旦外部连接断开,请求抛出异常。而在你不方便处理外部数据时,让代理服务缓存住未处理的数据。这个代理服务可以自己实现超时管理。
如果一个连接上的数据需要先后由不同内部服务处理,请求模式要比推送模式方便。你可以先由认证服务统一对新连接做身份认证;待认证通过后,再分配 agent ,由 agent 接管。
如果担心请求回应模式对高频的输入流有延迟,完全可以使用流水线的方式去调用代理服务(即不等上个请求回应,就立刻提起下个数据请求)。
我的实现放在 https://github.com/cloudwu/skynet_package ,这是个和 skynet 主仓库分离的独立仓库。
有兴趣的同学可以在这个基础上修改使用。
这个库分三个部分 :
skynet_package.c
编译出来的 package.so
是纯 C 实现的连接代理。一个 package 服务只管理一个 TCP 连接。只能在启动的时候初始化它管理的 fd ,并总是由管理服务启动它。
service/socket_proxyd.lua
是一个管理器服务,由它管理所有的 package 服务。由于 package 是一个 C 服务,所以并不通过 launcher 服务管理,在 debug console 里自然也无法列出。而这个管理器也就同时承担了查看所有 package 服务状态的职责。可以通过 debug console 给管理器发送 info 指令查看。
lualib/socket_proxy.lua
是一个封装好的库,避免直接向管理器或 package 代理发送消息。它有四个 API :subscribe 可以将一个 fd 注册入管理器;read 可以从已注册的 fd 上读一个包;write 可以发送一个包;close 可以强制断开 fd 连接。
注意:和 gate 不同,这个库并不负责 listen 端口。你需要自己 listen,并在 accept 连接时,把 fd 注册进管理器。具体的使用例子可以看 test 下的 main.lua 。
test 目录下还有一个 client.lua ,是一个简单的客户端测试程序,可以把控制台上输入的字符串,加上两字节的包头,再发送出去;并可以按协议分包读回服务器发送过来的包。
这个测试程序实现了一个简单的 echo 服务,能回发任何接收到的包。一旦发现接收到 quit 则主动断开连接。
btw, 这个示例假定你使用的是 linux ,且已经下载编译了 skynet 放在 $HOME/skynet 目录下。如果你的环境不同,可以直接修改 Makefile 文件。