protobuf 的使用

介绍序列化和反序列化工具 protobuf

Posted by Jerry Chen on July 4, 2021

建议继续看下一篇 grpc 的使用介绍,在编译安装 grpc 时会自动编译安装依赖项(包括 protobuf);可以直接安装 grpc,这里就不用单独安装 protobuf;

介绍 protobuf

Protocol Buffers,是 Google 公司开发的一种数据描述语言,类似于 XML 能够将结构化数据序列化,可用于数据存储、通信协议等方面。

官网 下载

安装 protobuf

  1. 安装依赖 zlib

    1
    
    sudo apt install zlib1g-dev
    
  2. 下载安装

    安装完成后,会生成 protoc 执行文件,这是 protobuf 的编译器;

    头文件目录:/usr/local/include/google/protobuf

    库文件目录:/usr/local/lib

    可执行文件目录:/usr/local/bin

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    git clone https://github.com/protocolbuffers/protobuf.git
    cd protobuf/cmake
       
    mkdir build
    cd build
       
    # 编译为静态库(默认,和下面的方式选一种)
    cmake .. -Dprotobuf_BUILD_TESTS=OFF
       
    # 编译为动态库
    cmake .. -Dprotobuf_BUILD_TESTS=OFF -DBUILD_SHARED_LIBS=ON
       
    make
    sudo make install
    
  3. 将 /usr/local/bin 放到环境变量,编辑 vim ~/.zshrc 文件最后行

    1
    
    export PATH=$PATH:/usr/local/bin
    

    接着刷新 source ~/.zshrc

  4. 如果执行报错,就刷新下 ldconfig

    报错:

    解决:

    1
    
    sudo ldconfig
    

语法介绍

基础描述
使用版本

现在一般都是使用 proto3 语法,将如下语句写在 proto 文件首行,编译时会按照 proto3 语法进行解析;

1
syntax = "proto3";
声明包名

包名相当于 cpp 上 namespace。编译 proto 为 cpp 源,生成的类、方法会放到 namespace pb_demo 中,其他语言类似;

1
package pb_demo;
proto 选项

放在全局(在声明包名 package xxx 的后一行):

1
option go_package = "./xxx";	// 指定 go 模块相对 go_out=dir 的生成路径和包名,路径会自动创建,go 模块名为 xxx,仅 golang 使用

放在 message 描述、enum 描述的第一行:

1
option allow_alias = true;		// 允许将同一个 field number 赋值给多个枚举变量
导入 proto

放在声明包名 package xxx 的后面,且放在全局 option 前面:

1
import "other.proto";
消息描述
1
2
3
message <messageName>{
	[field rules] <field type> <field name> = <field number>;
}
  • [可选项] field rules 可取值:

    • optional 表示此字段可选;
    • required 表示此字段必须存在;
    • repeated 表示此字段可以被重复任何次数(包含 0),相当于数组;
  • [必选项] field type 可取值:

    • 基础类型:double、float、int32、int64、uint32、uint64、sint32、sint64、fixed32、fixed64、sfixed32、sfixed64、bool、string、bytes;

    • 枚举类型:enum(可放在全局或某个 message 中)

      1
      2
      3
      4
      5
      
      enum PhoneType{
      	MOBILE = 0;
      	HOME = 1;
      	WORK = 2;
      }
      

      示例使用如下:

      第一字段是必选电话号码;

      第二字段是可选电话号码类型,如未选默认值为 HOME;

      1
      2
      3
      4
      
      message PhoneNumber{
      	required string number = 1;
      	optional PhoneType = 2 [default = HOME];
      }
      
    • 自定义类型:你定义的 messageName;

  • [必选项] field number 字段号可取值:

    • 范围 0< n <2^30;
    • 字段号 1-15 编码后字段长度为 1 字节,而字段号 >=16 的编码后字段长度为 2 字节;

使用 protobuf

定义第一个 .proto 协议文件

  1. 新建目录和文件

    1
    2
    
    mkdir -p ~/vscode/protobuf/proto
    touch ~/vscode/protobuf/proto/addressbook.proto
    

    目录看起来像这样:

    proto 目录存放 .proto 协议文件;

    proto_out_cpp 存放生成的 cpp 源文件;

    proto_out_go 存放生成的 go 源文件;

  2. 编辑 addressbook.proto 文件

    syntax = "proto3" 表示使用 proto3 语法;

    package addressbook 表示生成的方法放在 namespace addressbook 中;

    option go_package = "./addressbook" 仅在生成 go 源码有效,可指定一个生成路径(相对 go_out=dir 的路径)和 go 模块名;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    
    // [START declaration]
    syntax = "proto3";
    package addressbook;
       
    import "google/protobuf/timestamp.proto";
    // [END declaration]
       
    // [START java_declaration]
    option java_multiple_files = true;
    option java_package = "com.example.addressbook.protos";
    option java_outer_classname = "AddressBookProtos";
    // [END java_declaration]
       
    // [START csharp_declaration]
    option csharp_namespace = "Google.Protobuf.Examples.AddressBook";
    // [END csharp_declaration]
       
    // [START go_declaration]
    option go_package = "./addressbook";
    // [END go_declaration]
       
    // [START messages]
    message Person {
      string name = 1;
      int32 id = 2;  // Unique ID number for this person.
      string email = 3;
       
      enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
      }
       
      message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
      }
       
      repeated PhoneNumber phones = 4;
       
      google.protobuf.Timestamp last_updated = 5;
    }
       
    // Our address book file is just one of these.
    message AddressBook {
      repeated Person people = 1;
    }
    // [END messages]
    

编译协议文件为 c++ 源

  1. 使用 protoc 进行编译

    1
    
    protoc --cpp_out=../proto_out_cpp addressbook.proto
    
  2. 编译后生成的文件如下:

    其中命名空间和协议类如下图:

编译协议文件为 go 源

  1. 安装系列 go 插件

    下载安装有关 protoc、grpc 的插件,默认放到 ~/go/bin 目录;

    1
    2
    3
    4
    
    go get -u google.golang.org/protobuf/cmd/protoc-gen-go
    go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
    go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc
    go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
    
  2. 编辑 vim ~/.zshrc 文件最后行

    环境变量包括:protoc、go、go 插件目录;

    1
    
    export PATH=$PATH:/usr/local/bin:/usr/local/go/bin:~/go/bin
    

    接着刷新 source ~/.zshrc

  3. 使用 protoc 进行编译

    1
    
    protoc --go_out=../proto_out_go addressbook.proto
    

    编译后生成的文件如下:

  4. 在 proto_out_go/frist 目录中执行如下指令,生成模块

    1
    2
    
    go mod init example.com/addressbook
    go mod tidy
    

    接着生成的模块信息文件如下:

c++ 使用生成的源文件

  1. 新建主模块 list_people.cpp 文件

    1
    
    touch proto_out_cpp/list_people.cpp
    

  2. 编辑 proto_out_cpp/list_people.cpp 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    
    #include <fstream>
    #include <google/protobuf/util/time_util.h>
    #include <iostream>
    #include <string>
       
    #include "addressbook.pb.h"
       
    using namespace std;
       
    using google::protobuf::util::TimeUtil;
       
    // Iterates though all people in the AddressBook and prints info about them.
    void ListPeople(const addressbook::AddressBook& address_book) {
      for (int i = 0; i < address_book.people_size(); i++) {
        const addressbook::Person& person = address_book.people(i);
       
        cout << "Person ID: " << person.id() << endl;
        cout << "  Name: " << person.name() << endl;
        if (person.email() != "") {
          cout << "  E-mail address: " << person.email() << endl;
        }
       
        for (int j = 0; j < person.phones_size(); j++) {
          const addressbook::Person::PhoneNumber& phone_number = person.phones(j);
       
          switch (phone_number.type()) {
            case addressbook::Person::MOBILE:
              cout << "  Mobile phone #: ";
              break;
            case addressbook::Person::HOME:
              cout << "  Home phone #: ";
              break;
            case addressbook::Person::WORK:
              cout << "  Work phone #: ";
              break;
            default:
              cout << "  Unknown phone #: ";
              break;
          }
          cout << phone_number.number() << endl;
        }
        if (person.has_last_updated()) {
          cout << "  Updated: " << TimeUtil::ToString(person.last_updated()) << endl;
        }
      }
    }
       
    // Main function:  Reads the entire address book from a file and prints all
    //   the information inside.
    int main(int argc, char* argv[]) {
      // Verify that the version of the library that we linked against is
      // compatible with the version of the headers we compiled against.
      GOOGLE_PROTOBUF_VERIFY_VERSION;
       
      if (argc != 2) {
        cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
        return -1;
      }
       
      addressbook::AddressBook address_book;
       
      {
        // Read the existing address book.
        fstream input(argv[1], ios::in | ios::binary);
        if (!address_book.ParseFromIstream(&input)) {
          cerr << "Failed to parse address book." << endl;
          return -1;
        }
      }
       
      ListPeople(address_book);
       
      // Optional:  Delete all global objects allocated by libprotobuf.
      google::protobuf::ShutdownProtobufLibrary();
       
      return 0;
    }
    
  3. 安装 pkg-config

    pkg-config 用于检索系统中安装库文件的信息,一般用于库的编译和连接;

    1
    
    sudo apt-get install pkg-config
    
  4. 编译成 elf

    1
    
    g++ list_people.cpp addressbook.pb.cc -o list_people `pkg-config --cflags --libs protobuf`
    
  5. 执行程序

    1
    
    ./list_people
    

使用 CMakeLists.txt 方式编译

自定义命令 add_custom_command 中有 DEPENDS ${msg},实现了当 ${msg} 发生变化时重新进行 protoc 编译;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
cmake_minimum_required (VERSION 3.14)

project (list_people)

# 查找 Protobuf 3 库
find_package(Protobuf 3 REQUIRED)
if(Protobuf_FOUND)
    message(STATUS "protobuf library found")
else()
    message(FATAL_ERROR "protobuf library is needed but cant be found")
endif()

# 设置 proto 目录
set(MSG_PROTO_DIR ${CMAKE_SOURCE_DIR}/../proto)
# 设置需要编译的 proto 文件
file(GLOB_RECURSE MSG_PROTOS ${MSG_PROTO_DIR}/*.proto)

# 循环执行 protoc
set(MESSAGE_SRC "")
set(MESSAGE_HDRS "")
foreach(msg ${MSG_PROTOS})
    get_filename_component(FIL_WE ${msg} NAME_WE)

    list(APPEND MESSAGE_SRC "${CMAKE_SOURCE_DIR}/${FIL_WE}.pb.cc")
    list(APPEND MESSAGE_HDRS "${CMAKE_SOURCE_DIR}/${FIL_WE}.pb.h")

    add_custom_command(
        OUTPUT "${CMAKE_SOURCE_DIR}/${FIL_WE}.pb.cc"
               "${CMAKE_SOURCE_DIR}/${FIL_WE}.pb.h"
        COMMAND protoc
        ARGS --proto_path=${MSG_PROTO_DIR} --cpp_out=${CMAKE_SOURCE_DIR} ${msg}
        DEPENDS ${msg}
        COMMENT "Running C++ protocol buffer compiler on ${msg}"
        VERBATIM
    )
endforeach()

# 设置文件属性为 GENERATED
set_source_files_properties(${MESSAGE_SRC} ${MESSAGE_HDRS} PROPERTIES GENERATED TRUE)

include_directories(${PROTOBUF_INCLUDE_DIRS})
include_directories(${CMAKE_SOURCE_DIR})

add_executable(list_people list_people.cpp ${MESSAGE_SRC} ${MESSAGE_HDRS})
 
target_link_libraries(list_people ${PROTOBUF_LIBRARIES})

go 使用生成的源文件

编译的配置太痛苦了,后续请使用 GoLand ide

  1. 新建主模块 list_people.go 文件

    1
    2
    
    mkdir -p proto_out_go/main
    touch proto_out_go/main/list_people.go
    

  2. 编辑 proto_out_go/main/list_people.go 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    
    package main
       
    import (
    	"fmt"
    	"io"
    	"io/ioutil"
    	"log"
    	"os"
       
    	"github.com/golang/protobuf/proto"
    	pb "example.com/addressbook"
    )
       
    func writePerson(w io.Writer, p *pb.Person) {
    	fmt.Fprintln(w, "Person ID:", p.Id)
    	fmt.Fprintln(w, "  Name:", p.Name)
    	if p.Email != "" {
    		fmt.Fprintln(w, "  E-mail address:", p.Email)
    	}
       
    	for _, pn := range p.Phones {
    		switch pn.Type {
    		case pb.Person_MOBILE:
    			fmt.Fprint(w, "  Mobile phone #: ")
    		case pb.Person_HOME:
    			fmt.Fprint(w, "  Home phone #: ")
    		case pb.Person_WORK:
    			fmt.Fprint(w, "  Work phone #: ")
    		}
    		fmt.Fprintln(w, pn.Number)
    	}
    }
       
    func listPeople(w io.Writer, book *pb.AddressBook) {
    	for _, p := range book.People {
    		writePerson(w, p)
    	}
    }
       
    // Main reads the entire address book from a file and prints all the
    // information inside.
    func main() {
    	if len(os.Args) != 2 {
    		log.Fatalf("Usage:  %s ADDRESS_BOOK_FILE\n", os.Args[0])
    	}
    	fname := os.Args[1]
       
    	// [START unmarshal_proto]
    	// Read the existing address book.
    	in, err := ioutil.ReadFile(fname)
    	if err != nil {
    		log.Fatalln("Error reading file:", err)
    	}
    	book := &pb.AddressBook{}
    	if err := proto.Unmarshal(in, book); err != nil {
    		log.Fatalln("Failed to parse address book:", err)
    	}
    	// [END unmarshal_proto]
       
    	listPeople(os.Stdout, book)
    }
    
  3. 主模块初始化并补充依赖

    1
    2
    3
    4
    5
    6
    7
    8
    
    # 初始化模块
    go mod init example.com/list_people.go
          
    # 重定向依赖,addressbook 在 ../addressbook 目录
    go mod edit -replace example.com/addressbook=../addressbook
          
    # 添加缺少模块以及移除多余模块
    go mod tidy
    
  4. 执行程序

    1
    
    go run .