protobuf - ProtoBuffs 编解码
作者:马梦阳
一、概述
Protocol Buffers(简称 Protobuf)是一种由 Google 开发的高效、跨平台、语言无关的结构化数据序列化协议,广泛应用于网络通信、数据存储、配置管理等场景,具有体积小、解析快、扩展性强等优势。
LuatOS 提供了统一的 Protobuf 核心库(protobuf),对底层 Protobuf 编解码进行轻量级封装。开发者只需调用简洁的 API 接口,即可实现高性能的结构化数据序列化与反序列化,显著降低协议对接复杂度,提升通信效率与代码可维护性。
该库支持加载由 protoc 编译生成的二进制描述文件(.pb),提供完整的数据编码(encode)、数据解码(decode)等功能,支持 Proto2 和 Proto3 协议版本,能够根据输入的 protobuf 定义自动识别并处理相应版本的消息格式,适用于设备与云端、模块间高效数据交换等典型应用场景。
二、核心示例
1、核心示例是指:使用本库文件提供的核心 API,开发的基础业务逻辑的演示代码;
2、核心示例的作用是:帮助开发者快速理解如何使用本库,所以核心示例的逻辑都比较简单;
3、更加完整和详细的 demo,请参考 LuatOS 仓库 中各个产品目录下的 demo/protobuf;
protobuf(main.lua)
-- LuaTools需要PROJECT和VERSION这两个信息
PROJECT = "pb_demo"
VERSION = "001.000.000"
log.info("main", PROJECT, VERSION)
function main_task()
    -- 加载 pb 文件, 这个是从 pbtxt 转换得到的
    -- 下载资源到模块时不需要下载pbtxt
    -- 转换命令: protoc.exe -operson.pb person.pbtxt
    -- protoc.exe 下载地址: https://github.com/protocolbuffers/protobuf/releases
    local pb_file = "/luadb/person.pb"
    local tbdata = {
        name = "wendal",
        id = 123,
        email = "abc@qq.com"
    }
    if io.exists(pb_file) then
        local success, bytesRead = protobuf.load(io.readFile(pb_file))
        if not success then
            log.info("protobuf", "加载 protobuf 定义失败,已读取 " .. bytesRead .. " 字节")
            return
        else
            log.info("protobuf", "加载 protobuf 定义成功,共解析 " .. bytesRead .. " 字节")
        end
    else
        log.info("protobuf", "pb 文件不存在")
        return
    end
    -- 编码数据;
    local pbdata = protobuf.encode("Person", tbdata)
    if pbdata then
        -- 编码成功,编码后的数据通常包含不可见字符;
        -- 打印长度和十六进制内容(便于调试);
        log.info("protobuf", "编码成功,数据长度:" .. #pbdata)
        log.info("protobuf", "十六进制内容:" .. pbdata:toHex())
    else
        log.info("protobuf", "编码失败:数据格式或类型不匹配")
    end
    -- 对比 protobuf 编码和 json 编码的大小;
    local jdata = json.encode(tbdata)
    if jdata then
        log.info("json", "编码成功,数据长度:" .. #jdata)
        log.info("json", "数据内容:" .. jdata)
    else
        log.info("json", "编码失败:数据格式或类型不匹配")
    end
    -- 可见 protobuffs 比 json 节省很多空间;
    -- 数据解码;
    local tbdata = protobuf.decode("Person", pbdata)
    if tbdata then
        -- 解码后的数据为 Lua table 格式,需要转化为 json 进行显示;
        log.info("protobuf", "解码成功,数据内容:", json.encode(tbdata))
    else
        log.info("protobuf", "解码失败")
    end
    -- 清除所有已加载的定义数据;
    protobuf.clear()
    log.info("protobuf", "所有 protobuf 定义已清除")
end
-- 启动主任务;
sys.taskInit(main_task)
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
需要用到的文件:
三、常量详解
核心库常量,顾名思义是由合宙 LuatOS 内核固件中定义的、不可重新赋值或修改的固定值,在脚本代码中不需要声明,可直接调用;
每个常量对应的常量取值仅做日志打印时查询使用,不要将这个常量取值用做具体的业务逻辑判断,因为LuatOS内核固件可能会变更每个常量对应的常量取值;
如果用做具体的业务逻辑判断,一旦常量取值发生改变,业务逻辑就会出错;
protobuf 库没有常量;
四、函数详解
4.1 protobuf.load(pbdata)
功能
加载 Protocol Buffers 二进制定义数据到系统中,使其可以用于后续的编码和解码操作;
注意事项
1. 同一个文件只需要加载一次,除非调用过 protobuf.clear() 清除已加载的定义;
2. 加载的数据必须是通过 protoc.exe 程序转换得到的二进制数据;
参数
pbdata
参数含义:Protocol Buffers 二进制定义数据;
数据类型:string;
取值范围:取决于模组可用内存空间;
是否必选:必须传入此参数;
注意事项:必须是通过 protoc.exe 程序从 .proto 文件转换得到的数据;
参数示例:pbdata = io.readFile("/luadb/person.pb")
返回值
local success, bytesRead = protobuf.load(data)
有两个返回值 success、bytesRead;
success
含义说明:加载操作是否成功;
数值类型:boolean;
取值范围:true/false;
注意事项:成功时返回 true,失败时返回 false;
返回示例:true
bytesRead
含义说明:读取的二进制数据长度,主要用于调试;
数值类型:number;
取值范围:0 到 #pbdata(即输入字符串长度);
注意事项:若 success 为 false,该值仍会返回已尝试解析的字节数,可用于定位错误位置;
         正常情况下,bytesRead 应等于输入数据的总长度;
返回示例:128
示例
-- 加载 pb 文件, 这个是从 pbtxt 转换得到的;
-- 下载资源到模块时不需要下载 pbtxt;
-- 转换命令: protoc.exe -operson.pb person.pbtxt;
-- protoc.exe 下载地址: https://github.com/protocolbuffers/protobuf/releases;
local pb_file = "/luadb/person.pb"
if io.exists(pb_file) then
    local success, bytesRead = protobuf.load(io.readFile(pb_file))
    if not success then
        log.info("protobuf", "加载 protobuf 定义失败,已读取 " .. bytesRead .. " 字节")
    else
        log.info("protobuf", "加载 protobuf 定义成功,共解析 " .. bytesRead .. " 字节")
    end
else
    log.info("protobuf","pb 文件不存在")
end
4.2 protobuf.clear()
功能
清除已加载的 Protocol Buffers 二进制定义数据;
注意事项
1. 清除所有定义数据后,可以通过调用 protobuf.load() 重新加载新的定义数据;
2. 清除后,任何依赖已删除类型的序列化/反序列化操作将失败;
3. 该函数总是成功执行,没有返回错误的情况;
参数
无;
返回值
该接口无返回值,只需调用该接口执行相关操作,无需处理返回结果;
如果一定要把接口调用的结果赋值给一个变量,则这个变量就是一个 nil 值;
示例
-- 清除所有已加载的定义数据
protobuf.clear()
log.info("protobuf", "所有 protobuf 定义已清除")
4.3 protobuf.encode(tpname, data)
功能
将符合 Protocol Buffer 消息定义的 Lua 表(table)数据,按照指定的消息类型进行序列化,生成二进制编码后的字符串;
注意事项
1. 编码前必须先使用 protobuf.load() 加载对应的 Protocol Buffer 定义数据;
2. 待编码的 table 内容必须符合 pb 文件里的定义;
3. 编码结果为二进制字符串,可能包含不可打印字符,调试时建议使用十六进制(如 :toHex())或 Base64 查看;
4. 如果编码失败,函数会返回 nil;
参数
tpname
参数含义:数据类型名称;
数据类型:string;
取值范围:取决于模组可用内存空间;
是否必选:必须填入此参数;
注意事项:类型名称区分大小写,必须与 pb 文件的定义的完全一致;
         必须是已加载的有效类型名称,否则会报错;
参数示例:tpname = "Person"
data
参数含义:待编码的数据;
数据类型:table;
取值范围:取决于模组可用内存空间;
是否必选:必须传入此参数;
注意事项:表中的字段必须与 pb 文件中定义的字段严格匹配;
参数示例:data = {
                 name = "wendal",
                 age = 30,
                 emails = {"a@example.com", "b@example.com"}
         }
返回值
local pbdata = protobuf.encode(tpname, data)
有一个返回值 pbdata;
pbdata
含义说明:编码后的二进制数据字符串;
数值类型:string;
取值范围:二进制数据字符串或者 nil;
注意事项:成功时返回编码后的二进制数据字符串,失败时返回 nil;
         编码后的数据通常包含不可见字符,可能需要使用 toHex() 等方法转换后查看;
返回示例:二进制数据字符串(不可见)
示例
-- 准备待编码的数据;
local tbdata = {
    name = "wendal",
    id = 123,
    email = "abc@qq.com"
}
-- 编码数据;
local pbdata = protobuf.encode("Person", tbdata)
if pbdata then
    -- 编码成功,编码后的数据通常包含不可见字符;
    -- 打印长度和十六进制内容(便于调试);
    log.info("protobuf", "编码成功,数据长度:" .. #pbdata)
    log.info("protobuf", "十六进制内容:" .. pbdata:toHex())
else
    log.info("protobuf", "编码失败:数据格式或类型不匹配")
end
4.4 protobuf.decode(tpname, data)
功能
将 Protocol Buffer 二进制编码的数据(字符串)按照指定的消息类型进行反序列化,还原为 Lua 表(table)结构;
注意事项
1. 在调用此接口前,必须先通过 protobuf.load() 加载对应的 Protocol Buffers 定义数据;
2. 输入数据 data 必须是有效的 protobuf 二进制编码字符串,且与 tpname 对应的消息格式兼容;
3. 解码成功后返回 Lua table,若解码失败则返回 nil;
4. Lua table 内容无法直接打印出来,建议搭配 json.encode() 查看;
参数
tpname
参数含义:数据类型名称;
数据类型:string;
取值范围:取决于模组可用内存空间;
是否必选:必须填入此参数;
注意事项:类型名称区分大小写,必须与 pb 文件的定义的完全一致;
         必须是已加载的有效类型名称,否则会报错;
参数示例:tpname = "Person"
data
参数含义:待解码的二进制数据字符串;
数据类型:string;
取值范围:取决于模组可用内存空间;
是否必选:可选传入此参数(不填时将解码为空表);
注意事项:必须是符合对应类型定义的有效二进制数据;
参数示例:通过 protobuf.encode() 生成的二进制数据;
返回值
local tbdata = protobuf.decode(tpname, data)
有一个返回值 tbdata;
tbdata
含义说明:解码后的数据;
数值类型:table;
取值范围:Lua 表或者 nil;
注意事项:成功时返回解码后的 Lua 表数据,失败时返回 nil;
         返回值为 Lua table 格式,无法打印出实际的数据内容,建议搭配 json.encode() 查看;
返回示例:-- 直接打印时的显示;
         table: 028ED4F0
         -- 转化为 json 后的打印;
         {"name":"wendal","id":123,"email":"abc@qq.com"}
示例
-- 假设已通过 protobuf.load() 加载了 Person 类型定义;
-- 例如通过 protobuf.encode() 生成的返回值变量为 pbdata;
-- 数据解码;
local tbdata = protobuf.decode("Person", pbdata)
if tbdata then
    -- 解码后的数据为 Lua table 格式,需要转化为 json 进行显示;
    log.info("protobuf", "解码成功,数据内容:", json.encode(tbdata))
else
    log.info("protobuf", "解码失败")
end
五、产品支持说明
支持 LuatOS 开发的所有产品都支持 protobuf 核心库。