简介

Protocol Buffers 是 google 出品的一种数据交换格式, 缩写为 protobuf.

主要介绍 proto3 版本和 Golang 下的使用.

安装

protobuf 分为编译器和运行时两部分. 编译器直接使用预编译的二进制文件即可,
可以从 releases 上下载.

protobuf 运行时就是不同语言对应的库, 以 Golang 为例:

go get github.com/golang/protobuf/protoc-gen-go

语言定义

protobuf 现在有两个版本, proto2 和 proto3. 本着学新不学旧的原则, 这里只介绍 proto3.

既然是一种数据交换格式, 必然是要学习它的语法的, 就像学习 JSON 一样, 你总得知道如何定义.

.proto

下面是一个简单的例子, 来自于官方文档.

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

首行定义了语法版本, 即使用 proto3 版本. 然后定义了一个名为 SearchRequest 的 message, 以及它包含的字段(键值对).
message 的结构非常类似于各种语言中的 struct, dict 或者 map.

每个字段包括三个部分, 类型, 字段名和字段编号. 前两个部分非常易懂, 主要解释一下字段编号.
在同一个 message 中字段编号应该是唯一的, 用于在 message 的二进制格式(message binary format)中标识字段.
因此数字的大小决定了编码的长度, 1-15 的数字只占用一个字节.

///* ... */

特殊指令

reserved
message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}
repeated
message Test4 {
  repeated int32 d = 4 [packed=true];
}
enum
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}
allow_alias
enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;  // RUNNING 是 STARTED 的别名
}

类型也可以嵌套使用.

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}
import
import "myproject/other_protos.proto";
import public
import public "new.proto";
-I/--proto_path
AnyAny
import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}
type.googleapis.com/packagename.messagename
Oneofcase()WhichOneof()
message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}
repeated
map
map<key_type, value_type> map_field = N;
map<string, Project> projects = 3;

key_type 可以是 integral 或 string 类型, 即 scalar 类型中除了 floating point types 和 bytes 以外的类型.
value_type 可以是除 map 以外的任何类型.

package
package foo.bar;
message Open { ... }

在另一个 proto 文件中使用.

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

package 说明符会影响不同语言生成代码的方式:

option go_package

定义服务

如果你想要和 RPC 系统集成使用, 你可以定义 RPC 服务接口, 编译器会根据选择的语言, 生成对应的服务接口代码和存根.

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

最常见的是和 gRPC 一起使用.

JSON 支持

proto3 支持 JSON 中的规范编码, 具体的类型转换关系, 查看官方文档.

选项

google/protobuf/descriptor.proto

生成代码

要生成代码, 需要使用下面的命令, 对于 Golang 需要安装额外的插件.

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto

基础类型

标量值类型和各种语言间的转换可以参考 官方文档.

包括以下类型:

double
float
int32
int64
unit32
unit64
sint32  编码负数比 int32 更高效
sint64
fixed32 固定四字节. 如果数字通常大于 2^28, 比 uint32 更高效
fixed64
sfixed32 负数优化版本的 fixed32
sfixed64
bool
string
bytes

更新 message

官方指南翻译

OBSOLETE_reservedoneofoneofoneof

Golang 下使用

hello.proto
syntax = "proto3";

import "google/protobuf/any.proto";

package hello;
option go_package = "hello";

message HelloReq {
  string name = 1;
}

message HelloResp {
  int32 code = 1;
  string greet = 2;
  google.protobuf.Any details = 3;
}

service HelloService {
  rpc Greet(HelloReq) returns (HelloResp);
}

初始化项目, 并生成文件:

go mod init tzh.com/app
go get github.com/golang/protobuf/protoc-gen-go
mkdir hello
# 假设 protoc3 已经解压好了
.\protoc3\bin\protoc.exe  --proto_path=. --go_out=./hello hello.proto

main.go 如下:

package main

import (
    "crypto/rand"
    "fmt"

    "github.com/golang/protobuf/proto"
    "github.com/golang/protobuf/ptypes/any"
    hello "tzh.com/app/hello"
)

func main() {
    req := &hello.HelloReq{
        Name: "hello",
    }
    details := make([]byte, 10)
    rand.Read(details)
    resp := &hello.HelloResp{
        Code:    1,
        Greet:   "hello name",
        Details: &any.Any{Value: details},
    }
    fmt.Println(req.String())
    fmt.Println(resp.String())

    // 序列化
    data, _ := proto.Marshal(req)
    fmt.Println(data)

    // 反序列化
    newReq := &hello.HelloReq{}
    proto.Unmarshal(data, newReq)
    fmt.Println(newReq)

    fmt.Println("text format", proto.MarshalTextString(req))

}
HelloService

参考