当前位置: 首页 > 工具软件 > Mobile Lua > 使用案例 >

在Lua中“优雅”地使用Protobuf

贲绪
2023-12-01

Protobuf有着出色的性能、优秀的版本兼容性并且支持当下大部分的主流语言,在各种网络通信场景中被广泛使用。Lua作为一种效率极高的脚本语言,它可以方便得被嵌入到C程序中,并且支持热更新代码,在游戏行业不管是客户端还是服务器都很受欢迎。所以我想在Lua中使用Protobuf这个需求应该“合情合理”。但是,我在Protobuf中并未发现有Lua的官方版本实现。在一顿百度后发现有几个第三方的实现,不过感觉都不是特别满意。

protoc-gen-luahttps://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-protobufhttps://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、不停机维护都可以在这个的基础上轻易实现。

如果有任何为题可以直接给我发私信,写这篇文章的目的一是为了稍微“推广”一下,二是想了解不同人的需求,使之更完善,三是结识更多同行业人士。

 类似资料: