1、 概述

Protocol buffers 是语言中立、平台中立、可扩展的结构化数据序列化机制,就像 XML,但是它更小、更快、更简单。你只需定义一次数据的结构化方式,然后就可以使用特殊生成的源代码轻松地将结构化数据写入和读取各种数据流,支持各种语言。因为profobuf是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。

Protocol buffers安装配置请参见:Mac下安装配置Protocol Buffers

2、如何使用protobuf呢?

.protoprotoc.proto.pb.go

3、protobuf "hello world" 示例

假设,我们现在需要传输用户信息,其中有username和age两个字段,创建文件user.proto,文件内容如下:

// 指定的当前proto语法的版本,有2和3
syntax = "proto3";
// option go_package = "path;name"; path 表示生成的go文件的存放地址,会自动生成目录
// name 表示生成的go文件所属的包名
option go_package="../service";
// 指定生成出来的文件的package
package service;

message User {
  string username = 1;
  int32 age = 2;
}

运行protoc命令编译成go中间文件

# 编译user.proto之后输出到service文件夹
protoc --go_out=../service user.proto

项目结构:

测试:

package main

import (
	"Grpc-Protobuf/service"
	"fmt"
	"google.golang.org/protobuf/proto"
)

func main() {
	user := &service.User{
		Username: "zhangsan",
		Age:      20,
	}
	//转换为protobuf
	marshal, err := proto.Marshal(user)
	if err != nil {
		panic(err)
	}
	newUser := &service.User{}
	err = proto.Unmarshal(marshal, newUser)
	if err != nil {
		panic(err)
	}
	fmt.Println(newUser.String())
}

输出:

username:"zhangsan" age:20

4、proto文件语法详解

4.1 版本

Protocol Buffers文档的第一个非注释行,为版本声明,不填写的话默认为版本2。

syntax = "proto3";
或者
syntax = "proto2";

官方建议新项目采用proto3,proto3简化了proto2的开发,提高了开发的效能,但是也带来了版本不兼容的问题。老项目因为兼容性的问题继续使用proto2,并且会长时间的支持proto2。

4.2 包(package)

.protopackage

在没有为特定语言定义option xxx_package的时候,它还可以用来生成特定语言的包名,比如go package、Java package。

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

然后你可以在定义你的消息类型的字段时使用包说明符:

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

4.3 选项(option)

go_package(文件选项):用于生成的Go包。如果在.proto文件中没有给出显式的go_package选项,那么默认情况下将使用proto包(使用.proto文件中的“package”关键字指定)。如果不生成Go代码,则此选项无效。格式如下:

option go_package = "path;name"; path 

path表示生成的go文件的存放地址,会自动生成目录,name 表示生成的go文件所属的包名。如果.proto文件设置了package建议忽略name的配置,如果name也需要配置的话建议设置值和package值一致。

4.4 消息类型(message)

protobufmessage

message关键字类似于C++中的class,Java中的class,go中的struct

例如:

message User {
  string username = 1;
  int32 age = 2;
}

在消息中承载的数据分别对应于每一个字段。

其中每个字段都有一个名字和一种类型 。

 4.4.1 字段规则

requiredoptionalrepeated
message User {
  string username = 1;
  int32 age = 2;
  optional string password = 3;  // 生成的是指针
  repeated string address = 4;   // 生产的是切片
}

 4.4.2 字段映射 

.proto TypeNotesC++ TypePython TypeGo Type
double   double float float64
float   float float float32
int32 使用变长编码,对于负值的效率很低,如果你的域有 可能有负值,请使用sint64替代 int32 int int32
uint32 使用变长编码 uint32 int/long uint32
uint64 使用变长编码 uint64 int/long uint64
sint32 使用变长编码,这些编码在负值时比int32高效的多 int32 int int32
sint64 使用变长编码,有符号的整型值。编码时比通常的 int64高效。 int64 int/long int64
fixed32 总是4个字节,如果数值总是比总是比228大的话,这 个类型会比uint32高效。 uint32 int uint32
fixed64 总是8个字节,如果数值总是比总是比256大的话,这 个类型会比uint64高效。 uint64 int/long uint64
sfixed32 总是4个字节 int32 int int32
sfixed32 总是4个字节 int32 int int32
sfixed64 总是8个字节 int64 int/long int64
bool   bool bool bool
string 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文 本。 string str/unicode string
bytes 可能包含任意顺序的字节数据。 string str []byte

4.4.3 字段默认值 

protobuf3 删除了 protobuf2 中用来设置默认值的 default 关键字,取而代之的是protobuf3为各类型定义的默认值,也就是约定的默认值,如下表所示:

类型默认值
bool false
整型 0
string 空字符串""
枚举enum 第一个枚举元素的值,因为Protobuf3强制要求第一个枚举元素的值必须是0,所以枚举的默认值就是0;
message 不是null,而是DEFAULT_INSTANCE

4.4.5 标识符 

在消息体的定义中,每个字段都必须要有一个唯一的标识号,标识号是[0,2^29-1]范围内的一个整数。

message Person { 

  string name = 1;  // (位置1)
  int32 id = 2;  
  optional string email = 3;  
  repeated string phones = 4; // (位置4)
}

以Person为例,name=1,id=2, email=3, phones=4 中的1-4就是标识号。

[1-15]内的标识号在编码时只占用一个字节,包含标识符和字段类型,[16-2047]之间的标识符占用2个字节。建议为频繁出现的字段使用[1-15]间的标识符。

4.4.6 定义多个消息类型

一个proto文件中可以定义多个消息类型

message UserRequest {
  string username = 1;
  int32 age = 2;
  optional string password = 3;
  repeated string address = 4;
}

message UserResponse {
  string username = 1;
  int32 age = 2;
  optional string password = 3;
  repeated string address = 4;
}

4.4.7 嵌套消息

可以在其他消息类型中定义、使用消息类型,在下面的例子中,Person消息就定义在PersonInfo消息内,如 :

message PersonInfo {
    message Person {
        string name = 1;
        int32 height = 2;
        repeated int32 weight = 3;
    } 
	repeated Person info = 1;
}

如果你想在它的父消息类型的外部重用这个消息类型,你需要以PersonInfo.Person的形式使用它,如:

message PersonMessage {
	PersonInfo.Person info = 1;
}

当然,你也可以将消息嵌套任意多层,如 :

message Grandpa { // Level 0
    message Father { // Level 1
        message son { // Level 2
            string name = 1;
            int32 age = 2;
    	}
	} 
    message Uncle { // Level 1
        message Son { // Level 2
            string name = 1;
            int32 age = 2;
        }
    }
}

4.4.7 注释

.proto///*...*/

4.5 定义服务(Service) 

.proto
service SearchService {
	//rpc 服务的函数名 (传入参数)返回(返回参数)
	rpc Search (SearchRequest) returns (SearchResponse);
}

上述代表表示,定义了一个RPC服务,该方法接收SearchRequest返回SearchResponse。

与协议缓冲区一起使用的最直接的RPC系统是gRPC:Google开发的一个与语言和平台无关的开源RPC系统。gRPC与协议缓冲区配合得特别好,允许您使用特殊的协议缓冲区编译器插件直接从.proto文件生成相关的RPC代码。

4.6 使用其他消息类型(import)

SearchResponseResult.protoResultSearchResponseResult
message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}
ResultSearchResponse.proto.proto.protoimport
import "myproject/other_protos.proto";
.proto.proto.protoimport.protoimport publicimport publicproto
// new.proto
// All definitions are moved here  
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
-I/--proto_path--proto_path

4.7 生成类(.proto->.pb.go)

要生成Go代码,需要使用.proto文件中定义的消息类型和服务,还需要在.proto上运行协议缓冲区编译器protoc。编译器和protobuf的golang插件安装请参见:Mac下安装配置Protocol Buffers 

协议编译器的调用方式如下:

protoc --proto_path=IMPORT_PATH  --go_out=DST_DIR path/to/file.proto
  • IMPORT_PATH:指定解析import指令时要在其中查找.proto文件的目录。如果省略,则使用当前目录。通过多次传递-proto_path选项可以指定多个导入目录;它们将按顺序进行搜索。-I=_IMPORT_PATH_可以用作--proto_path的缩写形式。

  • --go_out表示在DST_DIR中生成Go代码。

  • 必须提供一个或多个.proto文件作为输入。可以一次指定多个.proto文件。尽管这些文件是相对于当前目录命名的,但每个文件都必须驻留在IMPORT_PATH导入的其中一个路径中,以便编译器可以确定其规范名称。

示例:

编译simple.proto,生成simple.pb.go

syntax = "proto3";
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
protoc -I=. -I/usr/local/include -I=$(GOPATH)/pkg --go_out=. simple.protogo_out
-IMacOS/usr/local/include