【菜狗教程】Protobuf.01 - 基本用法和特性介绍

1,739 阅读5分钟

菜狗教程 —— Protobuf 篇 01

官方文档:protobuf.dev/

Protobuf 的概念和用途

Protobuf 的含义是一件比较混乱的事,在日常开发的沟通里当我们提到 Protobuf 时,很可能还要根据前后语境判断具体指代什么东西,写出来的时候丢失了语境,所以我们要统一一个定义。

按照官方文档的定义:

Protocol Buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data.

首先,Protobuf 是一个机制(mechanism),而不是一种数据(data)。这意味着 Protobuf 在使用的过程中包含多个步骤,其中包括了序列化成数据和其他的步骤。

其次,Protobuf 具有语言中立、平台中立和可扩展的特性,所以可以用于几乎所有的场景,无论是一个系统内不同编程语言之间的交互,还是网络请求这种同时跨语言跨平台的场景都没问题。

最后我想补充一点定义中缺少的特性,Protobuf 机制序列化之后的数据是二进制的,算是有一层天然的加密,并且一般情况下会比 JSON 序列化成字符串数据量更小,小就表示更省流量和储存空间,网络传输耗时也更短,是很大的优势。

凡事皆有代价,Protobuf 诸多优势背后的代价是较为复杂的使用方式,先看一个流程图:

image.png

  1. 定义 *.proto 文件
  2. 使用 protoc 编译成不同语言的代码
  3. (使用具体某种语言)创建数据并序列化成二进制文件(或数据流)
  4. (使用具体某种语言)读取二进制文件,解析出其中的内容

从 Protobuf 的优点和使用方式来看,最适合 Protobuf 的使用场景就是网络请求和本地储存比较复杂的结构化数据,这两种在客户端开发中都很常见。另外,在 Android App 开发中,由于 JNI 的复杂度比较高,用 Protobuf 在 Kotlin 和 C++ 之间做数据交互也是能降低开发难度的。

Protobuf 的安装和编译

根据使用流程,Protobuf 需要先定义 .proto 文件,再编译成我们需要的语言的代码,编译使用的工具是 Protobuf 提供的 protoc

安装的说明在 github.com/protocolbuf…

如果不是特别需要,推荐使用预编译好的二进制包,解压之后就能用,方便控制 protoc 的版本,如果涉及多个项目的开发,甚至可能需要同时使用多个版本的 protoc

protoc 的用法规则是:

Usage: protoc [OPTION] PROTO_FILES

Options:
### 输出当前 protoc 版本 ###
  --version                   Show version info and exit.
  
### 指定输入文件路径 ###
-IPATH, --proto_path=PATH     Specify the directory in which to search for
                              imports.  May be specified multiple times;
                              directories will be searched in order.  If not
                              given, the current working directory is used.
### 使用插件 ###
  --plugin=EXECUTABLE
  
### 指定输出文件路径 ###
  --cpp_out=OUT_DIR           Generate C++ header and source.
  --csharp_out=OUT_DIR        Generate C# source file.
  --java_out=OUT_DIR          Generate Java source file.
  --js_out=OUT_DIR            Generate JavaScript source.
  --objc_out=OUT_DIR          Generate Objective C header and source.
  --php_out=OUT_DIR           Generate PHP source file.
  --python_out=OUT_DIR        Generate Python source file.
  --ruby_out=OUT_DIR          Generate Ruby source file.

此外还有一些可选参数,可以通过 protoc -h 查看。

Protobuf 的定义和使用

介绍完编译方式,是时候来实践一下了。

假设一个场景,我们需要把一个单词的数据从 Kotlin 传给 C++。按照使用流程,我们先定义数据结构,新建一个 word.proto 文件。

syntax="proto3";

package cg;

option java_package = "com.caigou.example.proto";
option java_outer_classname = "WordProto";

message Word {
  int32 id = 1;
  string spell = 2;
  string mean = 3;
}

然后使用 protoc 来编译这段代码,分别生成对应的 C++ 类和 Java 类。

#/path/to/cpp/proto
$ protoc -I ./ --cpp_out=./ word.proto

#/path/to/java/proto
$ protoc -I ./ --java_out=./ word.proto

正确的执行在终端不会输出任何内容。

image.png

C++ 的输出是一个标准的 class,内容相当丰富,用上面短短几行 proto 编译出来的两个文件加起来有接近 800 行代码,此处就不贴出来了。

Java 的输出我们增加了一点 option 控制了包名和文件名,默认的包名跟 proto 里面的 package 相同,一般都不符合 Java 的编程规范。

image.png

然后就可以在 Kotlin 代码中创建一个 Word 对象并序列化成文件,交给 C++ 读取。

Tips: 此处仅展示功能,省去了比较麻烦的 JNI,两边是两个独立的可执行程序。程序中需要配置一些 protobuf 的依赖才能正常运行,此处不做赘述。

import com.caigou.example.proto.WordProto
import java.io.File
import java.io.FileOutputStream
import java.io.IOException

fun main(args: Array<String>) {
    val word = WordProto.Word.newBuilder()
        .setId(10000)
        .setSpell("abandon")
        .setMean("抛弃,放弃")
        .build()

    write2File(word.toByteArray(), File("./pb_word"))
}

private fun write2File(byteArray: ByteArray, outputFile: File) {
    try {
        val fos = FileOutputStream(outputFile, false)
        fos.write(byteArray)
        fos.close()
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

代码很简单,就是把最经典的第一个单词写入 pb_word 文件。得到文件之后复制到 C++ 的工程中,再写一段读取数据的代码:

#include <iostream>
#include <string>
#include <fstream>
#include "proto/word.pb.h"

int main() {
    std::cout << "Start Read File" << std::endl;
    std::string data;
    std::ifstream fin("./pb_word", std::ios::binary | std::ios::in);
    fin >> data;
    fin.close();
    auto word = cg::Word();
    word.ParseFromArray(data.data(), data.size());

    std::cout << word.Utf8DebugString() << std::endl;

    return 0;
}

Let's go!

image.png

到这里你已经完全学会使用 Protobuf 了,快去试试吧!

彩蛋环节

ip 被限流了,么得 ChatGPT 玩了,自问自答两个问题吧。

Q1. 明明是 Kotlin 的 sample,为什么不直接编译成 kt 文件呢?

A1:现在确实支持 --kotlin_out 了,只是因为工作中使用的环境是用 Java 写了 JNI,然后 Protobuf 生成的代码也在一起,用 Java 比较统一。另外 Kotlin 相关文档也不是很完善,不敢贸然提出更换。构造一个 Message 对象用 Kotlin 肯定会容易很多,持续关注。

Q2. 复制了你这代码运行不起来啊?

A2:确实,文中也提到了工程中使用 Protobuf 需要额外的依赖,依赖就存在版本问题,还有跟 protoc 的兼容问题,都写出来似乎有点主次不分了。实际工作中 Protobuf 基本上是两个不同端的同事一起使用,我没想到有什么特别简单直接的单个工程能作为示例的,有想法可以教教我(真诚)。