最近同事碰到的一个需求:需要频繁把一组数据在 skynet 中跨网络传递,而这组数据实际变化并不频繁,所以做了大量重复的序列化和传输工作。
更具体一点说,他在 skynet 中设计了一个网关节点,这个网关服务可以负责把一条消息广播给一组客户端,每个客户端由内部的一个 uuid 串识别,而每条消息都附带有客户端 uuid 列表。而实际上这些 uuid 列表组有大量的重复。每条广播消息都重复打包了列表组,且列表组有大量重复信息。
一开始我想的方法是专门针对这个需求设计一组协议,给发送过的数据组编上 id ,然后在发送方和接收方都根据 id 压缩通讯数据。即,第一次发送时,发送全量信息,之后再根据数据变化发送差异;如果完全没有变化,则只需要发送 id 。
之后我想,能不能设计一种较为通用的差异同步方法,可以在跨节点传递数据组的时候,避免将相同的数据重复传输,而采用差异同步的方法同步对象。
晚饭后,我试着实现了一个这样一个简单的模块。
https://github.com/cloudwu/syncobj
我们可以先在通讯的两端建立对等的通道,构造数据的一端用 syncobj.source() 创建一个发送通道;而同步数据的另一端用 syncobj.clone() 创建一个接收通道。
当我们在发送端需要构造一组数据的时候,利用 obj = source:new() 创建出一个依附在这个通道上的对象 obj;为了简化实现,这个对象是一个单层的 lua 表。
我们可以在发送端像一个普通表一样访问这个对象,当我们需要同步到另一端时,可以用 diff = source:diff(obj) 生成一个和上个版本之间的差异数据;然后可以把 patch 作为普通的对象序列化发送。
而接收端用 cobj = clone:patch(diff) 就可以还原这个对象。由于 diff 中只是每次对象变化的差异数据,所以数据量会比直接全量传输 obj 要小。
如果通讯过程出了问题,我们还可以用 diff = source:reset(obj) 生成一个全量数据,而接收端则可以无视差异包和全量包的差别,都可以用 clone:patch(diff) 还原数据。
在内部协议中,实际上是给每组数据附加了一个单调递增的 id 号的,这个 id 号包含在 diff 数据中,所以 clone:patch 可以正确识别出如何还原数据。source:reset 生成全量包的过程,其实是赋予了对象一个全新的 id 。
当然,这个同步方案长期工作的话,就会存在一些对象已经在 source 端废弃,lua 的 gc 虽然会回收那些不再使用的对象内存,但 clone 端并不知晓;另外 source:reset 也会在 clone 端制造垃圾(老的版本已经废弃)。
所以,还提供了 removeset = source:collect() 返回一个不再使用的 id 列表;只要定期调用,把返回值传递给 clone:collect(removeset) 就可以让接收端清理不用对象。