嗯,又遇到新东西了, 再来认识下Protocol Buffer,用大白话总结最干的东西。
老规矩,最好的都是原汁原味。
ProtoBuf
是什么
Protocol Buffer是Google提供的一种结构化数据的序列化协议.
大白话: protobuf是用来定义消息/结构体(message)和服务/方法(service)的.
Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据的序列化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。 官网链接
大体了解下怎么用
Protobuf 包含序列化格式的定义、各种语言的库以及一个IDL编译器。正常情况下你需要
- step1:写proto文件, 定义 结构体
message和 方法service. - step2:然后使用IDL编译器编译成你需要的语言(本文是
go)。
基础规范需要了解一下
- 文件以
.proto作为文件后缀,除结构定义之外的语句以分号结尾. message结构体命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式.enums类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式.service与rpc方法名统一采用驼峰式命名.
定义message
syntax = "proto3"; // 指定proto的版本,默认proto2
package product.subsystem.module // 定义包名(import path),防止message重名
// 定义message(可以定义多个消息类型)
message SearchRequest {
string query = 1; // 定义字段: type fieldName "=" fieldNumber;
int32 page_number = 2;
int32 result_per_page = 3;
}
解释一下上面的代码的要点:
要点一: 字段的fieldNumber
string query = 1; 这里的1(fieldNumber),不是字段query(fieldName)的值, 而是字段query的Tag.(即: 在编码后1 就代表了 query这个字段).
换句话说,其实字段叫啥名在protobuf中并不重要, 因为在传输的时候,二进制里面用的是fieldNumber而不是fieldName.
NOTE: fieldNumber一旦被使用, 终生这个编号都不要改变。
fieldNumber 的取值范围是1~2^29-1.
而常用的fieldNumber范围是: 1-15(只用1个byte编码), 16-2047(采用2个byte编码).
所以为了节省编码后的长度, 经常使用的一些字段名(如:name, id等), 分配1-15的fieldNumber.
要点二: 字段的定义规则
protobuf的字段定义,必须满足两个规则之一:
singular单数字段: protobuf的默认字段规则, 就是说这个字段只能出现0或者1次.
repeated重复字段: 说的复杂, 其实就是该字段是一个数组或者list. 数组里面可以有任意数量的元素. 如果有多个元素, 元素的顺序会被保留.
message Person {
int32 id = 1;
string name = 2;
repeated string phone_number = 3; // 一个人会有多个电话
}
protobuf的字段类型type和各语言的对应关系,参见简单类型 Scalar Value Types
要点三: 保留字段
保留字段的意思就是, 这些字段保留下来, 后续在protobuf中,不能再次使用了.(即: 防止字段名一样, 但是字段含义不同)
syntax = "proto3";
message Person {
reserved 2, 3 to 7; // 保留这几个fieldNumber
reserved "foo", "bar"; // 保留这几个字段名
}
举个例子解释下为啥要保留字段, 大白话会啰嗦, 但一看就懂
syntax = "proto3";
// 一开始的需求, UserInfo绑定的是微信的账号和密码
message UserInfo {
int32 Id = 1;
stirng name = 2;
string wechat_account = 3;
string wechat_pwd = 4;
}
// 现在需求变了,要求用户信息绑定QQ账号密码
// 此时我删除了 wechat_account wechat_pwd两个字段, 并添加QQ_account, QQ_pwd
// 同时, 之前分配给wechat_account和wechat_pwd的fieldNumber 3 4, 又再一次分配给了 QQ_account和QQ_pwd.
message UserInfo {
int32 Id = 1;
stirng name = 2;
string QQ_account = 3;
string QQ_pwd = 4;
}
// 想想会有什么问题?
// 别想了,我直接说了, 假如server端修改了protobuf的定义,但是client端还没有更新.
// 此时, 客户端传给server微信的账号/密码, 服务端作为QQ的账号密码去验证,肯定是错的.
// 所以呢? 所以修改(如删掉)的字段和对应的fieldNumber都应该保留, 后续都不能在使用了.
要点四: enum 枚举类型
枚举可以定义在message里面,也可以定义在外面(便于复用)
在另一个message类型中,可以通过UserInfo.Gender, 使用枚举类型.
reserved同样也可以适用于枚举类型.
message UserInfo {
Gender gender = 1; // 使用Gender枚举类型
// 定义枚举类型
enum Gender {
FEMAIL = 0; // 必须从0开始
MAIL = 1;
}
}
要点五: message类型,也可以作为字段的类型
syntax = "proto3";
message Person {
int32 id = 1;
string name = 2;
Date birthday = 3; // 使用message类型作为字段的type
}
// 定义消息类型Date:生日
message Date {
int32 year = 1;
int32 mounth = 2;
int32 day = 3;
}
一般来说,不相关的消息, 每个message,创建一个proto文件.
如果需要用到其他.proto文件中定义的message, 要通过import进行引入.
编译器会在--proto_path参数指定的路径下寻找相应的需要导入的proto文件. 不写默认在当前目录寻找.
要点六: package
给一个.proto文件指定package, 是为了避免和其他的.proto文件的message名称冲突.
// bar.proto
package foo.bar;
message Open { ... }
后面可以使用该.proto文件的包名去使用message Open
// foo.proto
import "bar.proto"
message Foo {
...
foo.bar.Open open = 1;
...
}
当被IDL编译器翻译成GO语言后, Go代码的包名, 默认就是.proto文件的pacakge名称, 除非在.proto文件中显示的用go_pacakge指定IDL编译后的Go文件的import path.
要点七: import的搜寻路径是什么?
搜寻路径由protoc -I或者protoc --proto_path指定. 所以, import 要和 protoc -I/--proto_path 命令配合好.
定义service
// 请求结构体
message SearchRequest {
....
}
// 相应结构体
message SearchResponse {
....
}
// 服务api
service SearchApi {
rpc search(SearchRequest) returns (SearchResponse);
}
IDL编译器,生成(go)代码
安装IDL编译器
各个公司可能会有自己的IDL编译器。(基本上都会依赖protoc,所以要先安装protoc 和 protoc-gen-go)
NOTE: 如果你没有下载protoc-gen-go包,
则文中的命令go install google.golang.org/protobuf/cmd/protoc-gen-go不适用,
需要采用如下的命令:go get google.golang.org/protobuf/cmd/protoc-gen-go
生成代码命令
安装完后,就可以编译.proto文件了
IDL编译器,生成代码:protoc -I . -I /usr/local/include -I $(GOPATH)/src --go_out=. simple.proto
-I 或 --proto_path: 指定import搜索的文件夹路径
--go_out : 编译产生的 go代码 的生成位置
上面的命令的含义是:
- 根据
simple.proto去生成go代码 - go代码的生成目录为
. simple.proto导入的.proto文件, 去这些目录下下面搜索:./usr/local/include$(GOPATH)/src
代码生成规则
-
每个
xxx.proto文件, 生成一个xxx.pb.go文件. -
Go文件的
package的生成规则: 分为三种情况-
case1:
.proto文件不包含package如果一个
.proto文件中不包含package声明,生成的源代码将会使用.proto文件的文件名(去掉扩展名)作为Go包名,其中, 生成的Go的包名中会自动把
.转换为_。举例来说一个名为
high.score.proto不包含package声明的proto文件, 将会生成文件high.score.pb.go,他的Go包名是high_score。 -
case2:
.proto文件包含package经过编译的Go代码的包名默认使用
proto文件的package名称, 除非在.proto文件中使用option go_package显示进行指定Go文件的import path.其中, 生成的Go的包名中会自动把
.转换为_。举例来说
proto包名example.high_score将会生成Go包名example_high_score。 -
case3:
.proto文件包含package, 同时包含option go_packageoption go_package的作用是去指定生成的go文件的package的完整导入路径(import path)的, 如:// foo.proto option go_package = "example.com/foo/bar"; // 生成的foo.pb.go文件的package的import path就是 "example.com/foo/bar"并且Go文件的package的包名是
import path的最后一部分.例如: 如果
.proto文件是如上的定义, 则protoc --go_out build/gen foo.proto则有:- 生成的
go文件的package的完整导入路径import path是example.com/foo/bar. - 生成的
go文件的package的包名是bar - 生成的
go文件所在的路径是build/gen/example.com/foo/bar/foo.pb.go
- 生成的
-
-
生成Go文件所在的目录:
由两部分组成:
output_directory/sub_directory/- 输出目录:
output_directory - 输出目录子目录:
sub_directory
output_directory由protoc --go_out参数指定, 注意这个目录必须事先存在!!sub_directory由 编译器参数--go_opt和 proto文件的go_package共同决定,sub_directory可以编译器自己创建.假设我们有一个
test.proto文件// 文件 test.proto syntax = "proto3"; package = "test"; option go_package = "example.com/mytest/test";-
case1(默认情况): 命令
protoc --proto_path=. --go_out=build/gen test.proto按照生成的 go 代码的包的完整导入路径(import path)去创建目录层级则:
output_directory是build/gen/sub_directory是go文件的import path:example.com/mytest/test/即: go文件所在的路径是
build/gen/example.com/mytest/test/test.pb.go -
case2: 命令
protoc --proto_path=. --go_out=build/gen --go_opt=paths=source_relative test.proto按照proto源文件的相对路径去创建 go 代码的目录层级
这里的相对路径是指: 相对
--proto_path指定的路径.`output_directory` 是 `build/gen/` `sub_directory` 是 `test.proto`的相对路径(这里就是`./`) 生成的go文件路径是: `build/gen/test.pb.go` (和`test.proto`文件同级目录) - 输出目录:
-
message字段:message字段名用小写+下划线,转为go文件后- 首字母被大写: 表示可导出
- 以
_开头, 则_被替换为X - 如果在内部, 遇到
_[a-zA-Z]的形式, 则内部的下划线被去除, 并且下划线后的字母被大写.
举例:
message Test { string test_field = 1; string _my_field_name_2 = 2 }转换后
type Test struct { TestField string // test_field ==> TestField XMyFieldName_2 string // _my_field_name_2 ==> XMyFieldName_2 }
项目
这里推荐一篇文章, 方便加深对以下内容的理解.
Protobuf 的 import 功能在 Go 项目中的实践
- import
.proto文件的package和go_pacakgeprotoc命令的进一步理解
参考链接
protobuf-ultimate-tutorial-in-go