Protobuf 与 Json 数据格式转换

Protobuf 和 Json 存在显著的差异

数据格式 编码 自描述 字段顺序性 基本数据类型
Protobuf 二进制 丰富
         
    编码后的数据不包括字段名称,   double
    字段以 proto 文件 tag 值标识   float
        int32
        int64
        uint32
        uint64
        sint32
        sint64
        fixed32
        fixed64
        sfixed32
        sfixed64
        bool
        string
        bytes
Json 文本 较少
         
        string
        number
        bool
        null
         
        number 整型取值范围[-(2**53-1), 2**53-1]
        因此不能精确转换为 int64 或 uint64 类型

参考

Number - JavaScript | MDN

Language Guide (proto3)  |  Protocol Buffers  |  Google Developers

Protobuf 与 Json 的静态转换

使用 protoc 生成 proto 文件的编解码代码,再手工进行 Protobuf 与 Json 格式转换是很容易的,但是这需要在 proto 文件更新时重新生成代码,并重新编译、部署程序。

Protobuf 与 Json 的动态转换

如果程序能够解析 proto 文件,并根据 proto 定义完成 Protobuf 与 Json 的转换会很有用处。

example.proto

以下为接下来要使用的 example.proto 文件内容

syntax = "proto3";

package example;

message Hello {
  string name = 1;
  int32 times = 2;
}

Node.js

在查找 Node.js 下 Protobuf 使用相关资料的过程中,发现 Node.js 可以直接载入 proto 文件,然后直接获取 Protobuf 编解码对象进行 Protobuf 的编解码,如下所示:

var ProtoBuf = require("protobufjs");
var dgram = require('dgram');
var server = dgram.createSocket('udp4');

var builder = ProtoBuf.loadProtoFile("./cover.helloworld.proto"),
    Cover = builder.build("cover"),
    HelloCoverReq = Cover.helloworld.helloCoverReq;
    HelloCoverRsp = Cover.helloworld.helloCoverRsp;

引用自 在NodeJS中玩转Protocol Buffer - 腾讯Web前端 IMWeb 团队社区 | blog | 团队博客

这就是动态语言的优势,而且 Javascript 原生支持 Json,用来演示再合适不过。

以下为完整的 Protobuf 与 Json 动态互转示例代码:

"use strict";

const ProtoBuf = require("protobufjs");
const util = require('util');
const assert = require('assert');

ProtoBuf.load("./example.proto").then((root) => {
    const Hello = root.lookup('example.Hello');
    const source = {
        name: "world",
        times: 100
    };
    const hello = Hello.create(source);
    const buffer = Hello.encode(hello).finish();
    const destination = Hello.decode(buffer);
    assert(JSON.stringify(source) == JSON.stringify(destination));

    console.log({
        source: JSON.stringify(source),
        destination: JSON.stringify(destination)
    });
});

运行结果

$ node /home/tangxinfa/Examples/learn_node/protobuf_json.js
{ source: '{"name":"world","times":100}',
  destination: '{"name":"world","times":100}' }

C++

C++ 是静态类型的语言,肯定不如 Node.js 那么方便,主要用到 Protobuf 的反射(Reflection)机制,具体实现见:

HaustWang/pb2json 实现了静态和动态 Protobuf 与 Json 互转,这里以它写一个完整的示例

  • 拉取 HaustWang/pb2json 代码

    git clone https://github.com/HaustWang/pb2json.git
    
  • protobuf_json.cpp

    #include "pb2json/byReflection/pb2json.h"
    #include <google/protobuf/compiler/importer.h>
    #include <google/protobuf/dynamic_message.h>
    #include <iostream>
    #include <string>
    #include <cassert>
    using std::string;
    
    class ErrorCollector: public google::protobuf::compiler::MultiFileErrorCollector {
    public:
      ErrorCollector()
        :warnings_(0), errors_(0)
      {}
      virtual ~ErrorCollector() {}
      virtual void AddError(const string& filename, int line, int column,
                            const string& message) {
        ++errors_;
        std::cerr << filename << ":" << line << ":" << column << ": " << message << std::endl;
      }
    
      virtual void AddWarning(const string& filename, int line, int column,
                              const string& message) {
        ++warnings_;
        std::cerr << filename << ":" << line << ":" << column << ": " << message << std::endl;
      }
    
      uint errors() const { return errors_; }
      uint warnings() const { return warnings_; }
    
    private:
      uint errors_;
      uint warnings_;
    };
    
    
    int main(int argc, char *argv[]) {
      // parse proto file with protobuf compiler module,
      // also can generate self-described protobuf message with protoc,
      // then we can get file descriptor without protobuf compiler,
      // see https://developers.google.com/protocol-buffers/docs/techniques#self-description
      google::protobuf::compiler::DiskSourceTree sourceTree;
      sourceTree.MapPath("", ".");
      ErrorCollector collector;
      google::protobuf::compiler::Importer importer(&sourceTree, &collector);
      const char* proto_file = "example.proto";
      const google::protobuf::FileDescriptor* fileDescriptor = importer.Import(proto_file);
      if (fileDescriptor == NULL) {
        std::cerr << "proto file " << proto_file << " import failed" << std::endl;
        return EXIT_FAILURE;
      }
    
      const google::protobuf::Descriptor* descriptor = fileDescriptor->FindMessageTypeByName("Hello");
      assert(descriptor);
    
      google::protobuf::DynamicMessageFactory factory;
      const google::protobuf::Message* prototype = factory.GetPrototype(descriptor);
      assert(prototype);
      google::protobuf::Message* message = prototype->New();
      assert(message);
    
      string source = "{\"name\":\"world\",\"times\":100}";
      bool ok = Pb2Json::Json2Message(nlohmann::json::parse(source), *message);
      assert(ok);
    
      nlohmann::json destinationJson;
      Pb2Json::Message2Json(*message, destinationJson);
      string destination = destinationJson.dump();
    
      std::cout << "source: " << source << "\tdestination: " << destination << std::endl;
      assert(source == destination);
    
      return EXIT_SUCCESS;
    }
    
    • 编译

      g++ -O2 protobuf_json.cpp ./pb2json/byReflection/pb2json.cpp -lm -o protobuf_json -lprotobuf
      
    • 运行

      $ ./protobuf_json
      source: {"name":"world","times":100}    destination: {"name":"world","times":100}
      

相关参考