Protobuf有着出色的性能、优秀的版本兼容性并且支持当下大部分的主流语言,在各种网络通信场景中被广泛使用。Lua作为一种效率极高的脚本语言,它可以方便得被嵌入到C程序中,并且支持热更新代码,在游戏行业不管是客户端还是服务器都很受欢迎。所以我想在Lua中使用Protobuf这个需求应该“合情合理”。但是,我在Protobuf中并未发现有Lua的官方版本实现。在一顿百度后发现有几个第三方的实现,不过感觉都不是特别满意。
protoc-gen-lua:https://github.com/sean-lin/protoc-gen-lua
这个库的用法和C++版本的比较接近,通过工具生成每个message的代码,在Lua里面new一个message出来进行赋值,最后通过msg:SerializeToString()将其序列化成二进制。这种使用方式在Lua里面很不方便,没有了脚本语言的优势。
pbc:https://github.com/cloudwu/pbc
云风写的一个C库,自己解析了proto文件并按照protobuf的数据格式来打包解包,接口设计得挺好的,但是看评论有一些bug(好像是反序列化出来的table修改后再序列化会有问题)并且也没再维护了的样子。后面又搞了个他自己的协议sproto,但是这个sproto是不兼容protobuf的,所以使用场景的局限性太大。
lua-protobuf:https://github.com/starwing/lua-protobuf
这个库是相对比较满意的一个实现,代码轻量、接口和pbc类似,自己解析proto协议文件,传一个Lua的table可以直接encode成二进制数据,decode二进制数据可以直接生成一个table。因为整个库都是完全脱离Protobuf的代码自己手写的,没有完整得去读过它的代码,所以也不作太多的评价,因为我个人比较信任官方库和自己。
因为在序列化submessage的时候需要填一个“变长”编码的长度信息,自己实现的库这个会是非常的棘手。要么先算一次submessage的长度(在Lua中效率低),要么是需要将数据“分片”最后再将它们拷贝到一块连续的内存空间,要么是用定长的方式来兼容变长的编码方式。但是感觉这3种方式都不太满意,我猜这个库应该是使用的第二种方式
我不太想对它作太深入的了解,因为我觉得就用Protobuf的标准库,通过它的反射接口来实现和Lua的对接实现起来应该很简单,并且这样实现出来的Bug应该会是比较可控,也可以快速得支持Protobuf的新增特性(如支持Map等),所以我参照着前人的接口使用Protobuf原生的C++库实现了按照自己的想法实现了一个Lua的库,实际代码应该不到1000行。
我的protolua:https://github.com/jinjiazhang/protolua
syntax = "proto3";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
map<int32, string> subjects = 5;
}
require "protolua"
proto.parse("person.proto")
local player = {
name = "jinjiazh",
id = 10001,
email = "jinjiazh@qq.com",
phones = {
{number = "1818864xxxx", type = 1},
{number = "1868200xxxx", type = 2},
},
subjects = {
[101] = "Chinese",
[102] = "English",
[103] = "Maths",
}
}
local data = proto.encode("Person", player)
local clone = proto.decode("Person", data)
local data = proto.pack("Person", player.name, player.id, player.email, player.phones, player.subjects)
local name, id, email, phones, subjects = proto.unpack("Person", data)
用法也比较类似,同时支持proto2和proto3,导出了几个Lua的接口,parse、encode、decode、pack、unpack。或者直接在网络收发包那里直接调用C函数来pack、unpack可以少一次内存拷贝。我写的代码主要也就是通过协议名字找到对应的协议描述Descriptor*,然后遍历它的fields,通过字段名从Lua的table中取值然后在用Protobuf的反射接口给Message赋值,最后再调用message->SerializeToString()来序列化,反序列化的代码刚好相反。
实际应用中是长这个样子的:
message cs_account {
string openid = 1;
int64 roleid = 2;
string name = 3;
}
message cs_login_req {
string openid = 1;
string token = 2;
}
message cs_login_rsp {
errno result = 1;
oneof oneof_account {
cs_account account = 2;
}
}
function net.cs_login_req( ss, openid, token )
log_debug("cs_login_req", ss.number, openid, token)
local result, account = dbagent.login(openid, token)
ss.cs_login_rsp(result, account)
end
新加一条协议服务器只需要找个地方实现一个net.csxxx_req() 函数,并且通过ss.cs_xxx_rsp()来给客户端回包,参数顺序都是按协议的字段num顺序一一对应的。消息注册、派发这些代码都不需要,只需要专注于业务逻辑的实现,可以大大提高开发效率。按照同样的封装思路也可以将角色数据根据proto的描述信息将它写入到mysql、mongodb、redis中,这样整个服务器的数据和逻辑就都在Lua里了,热跟新修复Bug、不停机维护都可以在这个的基础上轻易实现。
如果有任何为题可以直接给我发私信,写这篇文章的目的一是为了稍微“推广”一下,二是想了解不同人的需求,使之更完善,三是结识更多同行业人士。