Protobuf 学习

1,429 阅读5分钟

前期准备 : github.com/protocolbuf… 下载编译器. protoc 编译器.

官方文档 , 也就是proto3 / proto2文档, 在 developers.google.com/protocol-bu… 这里, 如果英文水平差的同志呢, 可以看看这个 www.jianshu.com/p/bdd94a32f… , 其实proto3 更加写起来方便.

我写了一篇文章 github.com/Anthony-Don… , 就是基本的使用.

​ 其中要区分开 message 和 service. 前者是序列化, 后者提供了rpc服务. 大部分场景只需要message . 同时编译service. 也需要插件进行编译 . 普通的protoc --go_out --java_out 都不行.

如果玩 golang的 , 可以使用 gofast 插件编译 . 也不错. 但是和官方自带的一些库冲突. 最好别混合使用.

文件结构

基本结构

其实我大致说一下protobuf文件的基本结构 .

写protobuf文件 , 使用vscode 写比较棒 . 有代码提示, 下载一个插件就行了. 我觉得protobuf文件最好和业务抽离出来. 别混在一起.

syntax = "proto3"; // 语法版本

// 这个属于package . 别的protobuf文件引用的时候需要指定这个. 最好自己给 protobuf定义一个path.
package com.anthony.protobuf;

// Java选项
// Java类输出的目的地
option java_package = "com.example.grpc.api";
// 输出的Class文件. 如果java_multiple_files=false,则只输出这一个文件
// 否则输出 三个文件. Builder , RPCDateRequest ,Request 三个文件, 所以 java_outer_classname不能和RPCDateRequest名字重复
option java_outer_classname = "Request";
// 上面解释过了
option java_multiple_files = true;

// Go 选项 , 也是它指的是生成后的 ,目录位置. 相对于protoc指定的地址.
option go_package = "github.com/golang/protobuf/ptypes/timestamp";


// 定义消息 , 这里需要注意的是这个对象可以嵌套 互相引用.
message RPCDateRequest {
    uint64 id = 1;
    string msg = 2;
}

如何编译

​ 最好熟练掌握编译命令 .

第三步. 就是编译了 . protoc 命令

protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.proto

第一个意思是 : -I--proto_path 含义一样 . 有些时候我们需要导入别的proto文件. 此时就需要使用这个了 . 这个可以指定多个路径.

第二个就是 , $DST_DIR 指定的是 go_package的输出路径. 或者java_package 输出路径

第三个就是. SRC_DIR 指的是 我们protobuf源文件的地址.

第四个比如说 --go_out 指的是 go的输出路径. --go_out=plugins=grpc: 这个指的是生成 grpc 相关的配置.

基本就是上诉四个命令.

Message

​ 其中 message 有三个修饰词, 但是在proto3中, 只留下了一个, 默认一个缺醒值 . required 表示必须出现一次, optional 出现零次或者一次, repeated : 出现零次或者多次 , 其实就是数组. 也就是前面三个, 但是proto3中只保留了 第三个repeated . 默认就是optional ,不需要显示写出来,写出来就异常了.

​ 同时proto2支持默认模式, 也就是 default 关键字 , 但是在 proto3中取消了.

基本写法

syntax="proto3";

package  com.anthony.protobuf;

// 引入其他的protobuf文件
import "google/protobuf/empty.proto";

option java_multiple_files=true;
option java_package="com.anthony.api";
option java_outer_classname="PeopleApi";

message People{
    // 1 ,2 ,3 ,4 的意思是index , 所以传输的时候可以直接考这个传输位置. 这个是死的, 千万别随意改变. 
    // 1-15 占用一个字节.  后面的占用两个字节. 所以最好使用前面的15个字段.
    string name=1;
    int32 age=2;
    // 枚举
    enum Gender{
        // 这个必须从0开始
        Female = 0;
        Male = 1;
        Null=2;
    }
    Gender gender=3 ;
    // 0-1个 ,repeated 其实就是一个数组.
    // 还可以使用其他类型.
    repeated Hobby hobbies=4;

    // map
    map<string,Salary>  job=5;
}

message Salary{
     int32 salary=1;
}

service PeopleService{
    rpc addUser (People) returns (google.protobuf.Empty){}
}

message Hobby{
    string name=1;
}

message Student{
    
}

保留字 reserved 用法

比如 我的第一代版本中, 有一个字段, 比如下面这种情况

syntax="proto2";

package  com.anthony.protobuf;

option go_package="api";

message People{
    required string name=1;
    optional int32 age=2;
    // 第二个版本取消掉.
    optional string info=3;
}

执行 protoc -I ./src --go_out=./go_out ./src/com/anthony/protobuf/user.proto然后我们为了验证. 此时需要保存到文件中 , 调用这个方法.

func write() {
	people := api.People{}
	name := "tom"
	people.Name = &name
	age := int32(20)
	people.Age = &age
	info := "是个男的"
	people.Info = &info

	// 序列化
	bytes, _ := proto.Marshal(&people)

	// 保存到文件中
	file, e := os.OpenFile("save.txt", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
	if e != nil {
		log.Fatal(e)
	}
	file.Write(bytes)
	file.Close()
}

此时下一代来了, 我们舍弃掉了 info字段. 此时修改了proto文件 .

syntax="proto3";
package  com.anthony.protobuf;
option go_package="api";

message People{
    string name=1;
    int32 age=2;
    // 第一代版本中的.
    // string info=3;
    reserved 3;
}

改成了上诉这个, 然后编译

此时 调用这个方法.

func read() {
	bytes, _ := ioutil.ReadFile("save.txt")
	var rest api.People
	proto.Unmarshal(bytes,&rest)
	fmt.Printf("%#v\n",rest)
	marshal, _ := json.Marshal(&rest)
	fmt.Printf("%s", marshal)
}

输出

api.People{Name:"tom", Age:20, XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8{0x1a, 0xc, 0xe6, 0x98, 0xaf, 0xe4, 0xb8, 0xaa, 0xe7, 0x94, 0xb7, 0xe7, 0x9a, 0x84}, XXX_sizecache:0}
{"name":"tom","age":20}

说明个问题, 完全兼容的. 其实就算是不加上 reserved . 也没啥事, 不信你删了它试试. 可能是我理解的出入问题.

官方的意思是 : 如果删除了某一个字段,protobuf允许重新使用该数值作为新的属性的标签,但是为了保证向后兼容,读取旧的数据的时候不会出现问题,一般使用reserved来声明该数值为保留,不能被使用。 反正就是个保留字, 是一种协议, 为了拓展罢了. 告诉对方这个是保留位.

支持JSON

所以反正推荐使用 proto3 , 初次使用的话. 同时他还支持json. 原因是因为 , tag标签中有 json.

type People struct {
	Name                 string   `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
	Age                  int32    `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
	XXX_NoUnkeyedLiteral struct{} `json:"-"`
	XXX_unrecognized     []byte   `json:"-"`
	XXX_sizecache        int32    `json:"-"`
}

Java如何做json呢.

需要 如下这么做. 使用第三方显然不可能. 需要使用 JsonFormat 工具类

public static void main(String[] args) throws InvalidProtocolBufferException {
    NPack hello = NPack.newBuilder().setBody(ByteString.copyFrom("helloworld".getBytes()))
            .setUrl("hello")
            .setTimeStamp(System.currentTimeMillis()).build();


    // 序列化成 json
    String print = JsonFormat.printer().print(hello);
    System.out.println(print);


    // 反序列化成 result
    NPack.Builder result = NPack.newBuilder();
    JsonFormat.parser().merge(print, result);

    NPack build = result.build();
    System.out.println(build);
}

service

​ 记住一点就行了, service 不能有普通字段, 必须是 包装类型, 也就是必须是message. 也不能返回多个对象.

书写规范

message字段

使用驼峰命名法(首字母大写)命名 message,例子:SongServerRequest 使用下划线命名字段,栗子:song_name

message SongServerRequest {
  required string song_name = 1;
}

枚举

使用驼峰命名法(首字母大写)命名枚举类型,使用 “大写_下划线_大写” 的方式命名枚举值:

enum Foo {
  FIRST_VALUE = 0;
  SECOND_VALUE = 1;
}

service 字段

如果你在 .proto 文件中定义 RPC 服务,你应该使用驼峰命名法(首字母大写)命名 RPC 服务以及其中的 RPC 方法:

service FooService {
  rpc GetSomething(FooRequest) returns (FooResponse);
}

总结

多写 , 多敲. 别把protobuf 作为rpc. 而是当做序列化协议. 我们大部分场景只需要它的序列化功能.