赞
踩
Protobuf是Protocol Buffer的简称,它是Google公司于2008年开源的一种高效的平台无关、语言无关、可扩展的数据格式,目前Protobuf作为接口规范的描述语言,可以作为Go语言RPC接口的基础工具。
protobuf是一个与语言无关的一个数据协议,所以我们需要先编写IDL文件然后借助专用工具生成指定语言的代码,从而实现数据的序列化与反序列化过程。
大致开发流程如下: 1. IDL编写 2. 生成指定语言的代码 3. 序列化和反序列化
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
文件以.proto做为文件后缀,除结构定义外的语句以分号结尾
结构定义可以包含:message、service、enum
rpc方法定义结尾的分号可有可无
Message命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式
message SongServerRequest {
required string song_name = 1;
}
Enums类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式
enum Foo {
FIRST_VALUE = 1;
SECOND_VALUE = 2;
}
Service与rpc方法名统一采用驼峰式命名
Protobuf定义了一套基本数据类型。
.proto | C++ | Java | Python | Go | Ruby | C# |
---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double |
float | float | float | float | float32 | Float | float |
int32 | int32 | int | int | int32 | Fixnum or Bignum | int |
int64 | int64 | long | ing/long[3] | int64 | Bignum | long |
uint32 | uint32 | int[1] | int/long[3] | uint32 | Fixnum or Bignum | uint |
uint64 | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong |
sint32 | int32 | int | intj | int32 | Fixnum or Bignum | int |
sint64 | int64 | long | int/long[3] | int64 | Bignum | long |
fixed32 | uint32 | int[1] | int | uint32 | Fixnum or Bignum | uint |
fixed64 | uint64 | long[1] | int/long[3] | uint64 | Bignum | ulong |
sfixed32 | int32 | int | int | int32 | Fixnum or Bignum | int |
sfixed64 | int64 | long | int/long[3] | int64 | Bignum | long |
bool | bool | boolean | boolean | bool | TrueClass/FalseClass | bool |
string | string | String | str/unicode[4] | string | String(UTF-8) | string |
bytes | string | ByteString | str | []byte | String(ASCII-8BIT) | ByteString |
fixed32的打包效率比int32的效率高,但是使用的空间一般比int32多。因此一个属于时间效率高,一个属于空间效率高
字段名用小写,转为go文件后自动变为大写,message就相当于结构体
syntax = "proto3";
message SearchRequest {
string query = 1; // 查询字符串
int32 page_number = 2; // 页码
int32 result_per_page = 3; // 每页条数
}
支持嵌套消息,消息可以包含另一个消息作为其字段。也可以在消息内定义一个新的消息
内部声明的message类型名称只可在内部直接使用
message SearchResponse { message Result { string url = 1; string title = 2; repeated string snippets = 3; } repeated Result results = 1; } message Outer { // Level 0 message MiddleAA { // Level 1 message Inner { // Level 2 int64 ival = 1; bool booly = 2; } } message MiddleBB { // Level 1 message Inner { // Level 2 int32 ival = 1; bool booly = 2; } } }
message定义结构体,service定义接口
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse) {}
}
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
在上面的示例中,所有字段都是标量类型(scalar types): 两个整数(page_number和 result_per_page)和一个字符串(query)。但是也可以为字段指定组合类型,包括枚举和其他消息类型。
(2**29)-1
(字段数字会作为key,key最后三位是类型)或536870911
。您也不能使用数字19000
到19999
(字段描述符),因为它们是协议缓冲区的保留数字,如果你在你的.proto中使用了这些数字,编译器会报错。同样,不能使用任何以前保留的字段号。消息字段可以是下列字段之一:
在 proto3中,标量数值类型的repeated
字段默认使用packed
编码。(参考Protocol Buffer Encoding,可以找到关于packed编码的更多信息)
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
.proto文件使用了C/C++风格的//
和/* ... */
语法。
/* SearchRequest 表示一个分页查询
* 其中有一些字段指示响应中包含哪些结果 */
message SearchRequest {
string query = 1;
int32 page_number = 2; // 页码数
int32 result_per_page = 3; // 每页返回的结果数
}
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注意,不能在同一个reserved语句中混合字段名和字段编号。
当使用 protocol buffer 编译器来运行.proto文件时,编译器用选择的语言生成描述的消息类型
,包括获取和设置字段值,将消息序列化为输出流,以及从输入流解析消息的代码。
对于Python:Python 编译器为.proto文件中的每个消息类型生成一个带静态描述符的模块,然后与 metaclass 一起使用,在运行时创建必要的 Python 数据访问类。
对于 Go:编译器为文件中的每种消息类型生成一个类型(type)到一个.pb.go 文件。
其他…
.proto Type | Notes | C++ Type | Java/Kotlin Type[1] | Python Type[3] | Go Type | PHP Type |
---|---|---|---|---|---|---|
double | double | double | float | float64 | float | |
float | float | float | float | float32 | float | |
int32 | 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,则使用 sint32代替。 | int32 | int | int | int32 | integer |
int64 | 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,则使用 sint64代替。 | int64 | long | int/long[4] | int64 | integer/string[6] |
uint32 | 使用变长编码。 | uint32 | int[2] | int/long[4] | uint32 | integer |
uint64 | 使用变长编码。 | uint64 | long[2] | int/long[4] | uint64 | integer/string[6] |
sint32 | 使用可变长度编码。带符号的 int 值。这些编码比普通的 int32更有效地编码负数。 | int32 | int | int | int32 | integer |
sint64 | 使用可变长度编码。带符号的 int 值。这些编码比普通的 int64更有效地编码负数。 | int64 | long | int/long[4] | int64 | integer/string[6] |
fixed32 | 总是四个字节。如果值经常大于228,则比 uint32更有效率。 | uint32 | int[2] | int/long[4] | uint32 | integer |
fixed64 | 总是8字节。如果值经常大于256,则比 uint64更有效率。 | uint64 | integer/string[6] | |||
sfixed32 | 总是四个字节。 | int32 | int | int | int32 | integer |
sfixed64 | 总是八个字节。 | int64 | integer/string[6] | |||
bool | bool | boolean | bool | bool | boolean | |
string | 字符串必须始终包含 UTF-8编码的或7位 ASCII 文本,且不能长于232。 | string | String | str/unicode[5] | string | string |
bytes | 可以包含任何不超过232字节的任意字节序列。 | string | ByteString | str (Python 2) bytes (Python 3) | []byte | string |
当解析消息时,如果编码消息不包含特定的 singular 元素,则解析对象中的相应字段将设置为该字段的默认值。
定义消息类型时,可能希望其中一个字段只包含预定义值列表中的一个
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;
}
Corpus enum 的第一个常量映射为零,每个 enum 定义必须包含一个常量,该常量映射为零且作为它的第一个元素。这是因为:
可以通过将相同的值赋给不同的枚举常量来定义别名。为此,需要将allow_alias选项设置为true,否则协议编译器将在找到别名时将生成错误消息。
message MyMessage1 { enum EnumAllowingAlias { // 允许别名 option allow_alias = true; UNKNOWN = 0; STARTED = 1; RUNNING = 1; } } message MyMessage2 { enum EnumNotAllowingAlias { UNKNOWN = 0; STARTED = 1; // RUNNING = 1; // 取消对此行的注释将导致内部出现编译错误,外部出现警告消息。 } }
_MessageType_._EnumType_
语法将一条消息中声明的枚举类型用作另一条消息中的字段类型。syntax = "proto3"; option go_package = "protos/pbs"; enum TestType { Hello1=0; Hello2=1; Hello3=2; Hello4=3; } message HelloRequest { string greeting = 1; TestType en=2; } message HelloResponse { string reply = 1; }
当使用其他消息的时候,如果在本文件,直接使用就可以了。Result代表自定义消息类型
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
// []string
repeated string snippets = 3;
}
可以通过 import 来使用来自其他 .proto 文件的定义。
import "myproject/other_protos.proto";
默认情况下,只能从直接导入的 .proto 文件中使用定义。但是,有时你可能需要将 .proto 文件移动到新的位置,这样就可以在旧目录放一个占位的.proto文件,使用import public
将所有导入转发到新位置,而不必直接移动.proto文件并修改所有的地方。
任何导入包含import public语句的proto的人都可以传递地依赖import public依赖项
// 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
protocol 编译器使用命令行-I/–proto_path参数指定的一组目录中搜索导入的文件。如果没有给该命令行参数,则查看调用编译器的目录。
一般来说,应该将 --proto_path 参数设置为项目的根目录并为所有导入使用正确的名称。
// user_business.proto
syntax = "proto3";
option go_package = "protos/pbs";
import "share/user.proto";
//获取角色信息请求
message GetUserRequest {
}
//获取角色信息响应
message GetUserResponse {
User user=1;
}
// user.proto
syntax = "proto3";
option go_package = "protos/pbs";
//用户定义
message User {
string Id=1;
string Name=2;
string Age=3;
}
可以在其他消息类型中定义和使用消息类型,在 SearchResponse消息中定义Result
:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果要在其父消息类型之外重用此消息类型,请通过_Parent_._Type_
使用:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
可以随心所欲地将信息一层又一层嵌入其中:
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}
如果现有的消息类型不再满足你的所有需要(例如,消息格式需要增加一个字段),但是仍然希望使用用旧格式创建的代码。在不破坏任何现有代码的情况下更新消息类型非常简单,只需遵守以下规则:
OBSOLETE_
,或者使用reserved保留字段
编号,以便.proto的未来用户不会意外地重用该编号。int32
、 uint32
、 int64
、 uint64
和 bool
都是兼容的——这意味着你可以在不破坏向前或向后兼容性的情况下将一个字段从这些类型中的一个更改为另一个。sint32
和 sint64
相互兼容,但与其他整数类型不兼容。string
和bytes
是兼容的,只要字节是有效的 UTF-8。bytes
包含消息的编码版本,则嵌入的消息与字节兼容。fixed32
与 sfixed32
兼容 fixed64
与 sfixed64
兼容。string
、bytes
和消息字段,optional
字段与repeated
字段兼容。给定重复字段的序列化数据作为输入,如果该字段是基元类型
字段,则期望该字段为optional(可选的)
字段的客户端将获取最后一个输入值;如果该字段是消息类型字段,则合并所有输入元素。请注意,对于数值类型(包括bool
和enum
),这通常是不安全的。数字类型的重复字段可以按压缩格式序列化,当需要可选字段时,将无法正确解析压缩格式。enum
在格式方面与 int32
、 uint32
、int64
和 uint64
兼容(请注意,如果不适合,值将被截断)。但是要注意,当消息被反序列化时,客户端代码可能会对它们进行不同的处理:例如,未识别的proto3枚举类型将保留在消息中,但在反序列化消息时如何表示这些类型取决于客户端语言。整型字段总是保持它们的值。oneof
成员是安全的,并且二进制兼容。如果确保没有代码一次设置多个字段,那么将多个字段移动到新的oneof
字段中可能是安全的。将任何字段移到现有oneof
字段中都不安全。import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
给定消息类型的默认类型 URL 是type.googleapis.com/_packagename_._messagename_
。
不同的语言实现将支持运行库助手以类型安全的方式打包和解包 Any值。例如在java中,Any类型会有特殊的pack()和unpack()访问器,在C++中会有PackFrom()和UnpackTo()方法。
如果在平时在一个消息有许多字段,但是最多设置一个字段,我们可以使用oneof 来执行并节省内存。
Oneof 字段类似于常规字段,除了Oneof共享内存的所有字段之外,最多可以同时设置一个字段。设置Oneof 的任何成员都会自动清除所有其他成员。您可以使用case()或WhichOneof()方法检查Oneof 中的哪个值被设置(如果有的话),具体取决于选择的语言。
syntax = "proto3";
option go_package = "protos/pbs";
message SubMessage {
int32 Id=1;
string Age2=2;
}
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
要定义 oneof 字段需要在你的.proto文件中使用oneof关键字并在后面跟上名称,然后将其中一个字段添加到该字段的定义中。你可以添加任何类型的字段,除了map字段和repeated字段。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
package main import ( "fmt" "grpcdemo/protobuf/any/protos/pbs" ) func main() { p:=&pbs.SampleMessage{ TestOneof: &pbs.SampleMessage_Name{Name: "hello"}, } fmt.Println(p) fmt.Println(p.GetTestOneof()) p.TestOneof=&pbs.SampleMessage_SubMessage{SubMessage: &pbs.SubMessage{Id: 1}} fmt.Println(p) fmt.Println(p.GetTestOneof()) }
添加或删除其中一个字段时要小心。如果检查 oneof 的值返回 None/NOT_SET,则可能意味着 oneof 尚未设置或已设置为 oneof 的另一个字段。这种情况是无法区分的,因为无法知道未知字段是否是 oneof 成员。
标签重用问题
map<key_type, value_type> map_field = N;
message Project {
key_type key = 1;
value_type value = 2;
}
// 比如key是string val是Project类型
map<string, Project> projects = 3;
其中key_type
可以是任何整型或字符串类型(因此,除了浮点类型和字节之外的任何标量类型)。注意enum
不是一个有效的key_type
。value_type
可以是除其他map
以外的任何类型。
可以向.proto文件中添加可选的package明符,以防止协议消息类型之间的名称冲突,
可以理解为go的包。
package foo.bar;
message Open { ... }
如果要在RPC(Remote Procedure Call,远程过程调用)系统中使用消息类型,可以在.proto文件中定义RPC服务接口。
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
proto3 | JSON | JSON example | 描述【译】 |
---|---|---|---|
message | object | {"fooBar": v, | 生成JSON对象。消息字段名被映射到lowerCamelCase并成为JSON对象键。如果指定了json_name 选项,则指定的值将用作键。解析器接受小驼峰命秘法名称(或由json_name 选项指定的名称)和原始proto字段名称。null是所有字段类型的可接受值,并被视为相应字段类型的默认值。 |
enum | string | "FOO_ | 使用proto中指定的枚举值的名称。解析器接受枚举名和整数值。 |
map<K,V> | object | {"k": v, | 所有键都转换为字符串。 |
repeated V | array | [v, | null被接受为空列表[]。 |
bool | true, false | true, | |
string | string | "Hello World!" | |
bytes | base64 string | "YWJjMTIzIT8kKiYoKSctPUB+" | JSON值将是使用带填充的标准base64编码的字符串编码的数据。包含或不包含填充的标准或url安全base64编码都可以接受。 |
int32, fixed32, uint32 | number | 1, | JSON值将是一个十进制数。可接受数字或字符串。 |
int64, fixed64, uint64 | string | "1", | JSON值将是十进制字符串。可接受数字或字符串。 |
float, double | number | 1. | JSON值将是一个数字或特殊字符串值"NaN", "Infinity",和"-Infinity"中的一个。数字或字符串都可以接受。指数符号也被接受。-0被认为等于0。 |
Any | object | {"@type": "url", | 如果`Any`类型包含一个具有特殊JSON映射的值,它将按如下方式转换:{"@type":xxx,"value":yyy} 。否则,该值将转换为JSON对象,并插入“@type”字段以指示实际的数据类型。 |
Timestamp | string | "1972-01-01T10:00:20. | 使用RFC3339,其中生成的输出将始终是**Z**规格化的,并使用0、3、6或9个小数位数。也接受“Z”以外的偏移。 |
Duration | string | "1. | 生成的输出总是包含0、3、6或9个小数位数(取决于所需的精度),后跟“s”后缀。接受任何小数位数(没有小数也可以),只要它们符合纳秒精度,并且需要“s”后缀。 |
Struct | object | { … } | 任何JSON对象。见struct.proto 。 |
Wrapper types | various types | 2, | 包装器在JSON中使用与包装原语类型相同的表示形式,只是在数据转换和传输期间允许并保留null。 |
FieldMask | string | "f. | 见field_mask.proto |
ListValue | array | [foo, | |
Value | value | 任何JSON值。详见google.protobuf.Value | |
NullValue | null | JSON null | |
Empty | object | {} | 空的JSON对象 |
.proto文件中的单个声明可以使用许多 选项 进行注释。选项不会更改声明的总体含义,但可能会影响在特定上下文中处理声明的方式。可用选项的完整列表在google/protobuf/descriptor.proto中定义。
deprecated选项: 设为true 代表字段被废弃,在新代码不应该被使用,在大多数语言都没有实际的效果
option go_package = "tmp/pb"; // 指定生成的Go代码在你项目中的导入路径
int32 old_field = 6 [deprecated = true];
当使用go_out
标志调用 protoc 时,protocol buffer编译器将生成 Go 代码。protocol buffer编译器会将生成的Go代码输出到命令行参数go_out
指定的位置。go_out标志
的参数是你希望编译器编写 Go 输出的目录。编译器为每个.proto
文件输入创建一个源文件。输出文件的名称是通过将.proto
扩展名替换为.pb.go
而创建的。
生成的.pb.go
文件放置的目录取决于编译器标志。有以下几种输出模式:
paths=import
:输出文件放在以 Go 包的导入路径命名的目录中。例如,protos/buzz.proto
文件中带有example.com/project/protos/fizz
的导入路径,则输出的生成文件会保存在example.com/project/protos/fizz/buzz.pb.go
。如果未指定路径标志,这就是默认输出模式。module=$PREFIX
:输出文件放在以 Go 包的导入路径命名的目录中,但是从输出文件名中删除了指定的目录前缀。例如,输入文件 pros/buzz.proto
,其导入路径为 example.com/project/protos/fizz
并指定example.com/project
为module前缀,结果会产生一个名为 pros/fizz/buzz.pb.go
的输出文件。在module路径之外生成任何 Go 包都会导致错误。此模式对于将生成的文件直接输出到 Go 模块非常有用。paths=source_relative
:输出文件与输入文件放在相同的相对目录中。例如,一个protos/buzz.proto
输入文件会产生一个位于protos/buzz.pb.go
的输出文件。在调用protoc时,通过传递 go_opt
标志来提供特定于 protocol-gen-go
的标志位参数。可以传递多个go_opt标志位参数。例如,当执行下面的命令时:
protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto
编译器将从 src 目录中读取输入文件 foo.proto
和 bar/baz.proto
,并将输出文件 foo.pb.go
和 bar/baz.pb.go
写入 out 目录。如果需要,编译器会自动创建嵌套的输出子目录,但不会创建输出目录本身。
为了生成 Go 代码,必须为每个 .proto 文件(包括那些被生成的 .proto 文件传递依赖的文件)提供 Go 包的导入路径。有两种方法可以指定 Go 导入路径:
建议在 .proto 文件中声明它,以便 .proto 文件的 Go 包可以与 .proto 文件本身集中标识,并简化调用 protoc 时传递的标志集。 如果给定 .proto 文件的 Go 导入路径由 .proto 文件本身和命令行提供,则后者优先于前者。
Go 导入路径是在 .proto 文件中指定的,通过声明带有 Go 包的完整导入路径的 go_package 选项来创建 proto 文件。用法示例:
option go_package = "example.com/project/protos/fizz";
调用编译器时,可以在命令行上指定 Go 导入路径,方法是传递一个或多个 M P R O T O F I L E = {PROTO_FILE}= PROTOFILE={GO_IMPORT_PATH} 标志位。用法示例:
protoc --proto_path=src \
--go_opt=Mprotos/buzz.proto=example.com/project/protos/fizz \
--go_opt=Mprotos/bar.proto=example.com/project/protos/foo \
protos/buzz.proto protos/bar.proto
由于所有 .proto 文件到其 Go 导入路径的映射可能非常大,这种指定 Go 导入路径的模式通常由控制整个依赖树的某些构建工具(例如 Bazel)执行。 如果给定的 .proto 文件有重复条目,则指定的最后一个条目优先。
对于 go_package 选项和 M 标志位,它们的值可以包含一个显式的包名称,该名称与导入路径之间用分号分隔。 例如:“example.com/protos/foo;package_name”
。 不鼓励这种用法,因为默认情况下包名称将以合理的方式从导入路径派生。
导入路径用于确定一个 .proto 文件导入另一个 .proto 文件时必须生成哪些导入语句。 例如,如果 a.proto导入 b.proto,则生成的 a.pb.go 文件需要导入包含生成的 b.pb.go 文件的 Go 包(除非两个文件在同一个包中)。 导入路径也用于构造输出文件名。 有关详细信息,请参阅上面的“编译器调用”部分。
Go 导入路径和 .proto 文件中的package说明符之间没有关联。 后者仅与 protobuf 命名空间相关,而前者仅与 Go 命名空间相关。 此外,Go 导入路径和 .proto 导入路径之间没有关联。
新建一个名为demo的项目,并且将项目中定义的.proto文件都保存在proto目录下。
本文后续的操作命令默认都在demo目录下执行。
新建一个price.proto文件。
// proto/book/price.proto
syntax = "proto3";
package book;
// 声明生成Go代码的导入路径(import path)
option go_package = "github.com/Generalzy/demo/proto/book";
message Price {
int64 market_price = 1; // 建议使用下划线的命名方式
int64 sale_price = 2;
}
项目当前的目录结构如下:
demo
└── proto
└── book
└── price.proto
生成代码把最终生成的Go代码还保存在proto文件夹中:
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/price.proto
其中:
此外,–proto_path有一个别名-I,上述编译命令也可以这样写。
protoc -I=proto --go_out=proto --go_opt=paths=source_relative book/price.proto
执行上述命令将会在proto目录下生成book/price.pb.go文件。
demo
└── proto
└── book
├── price.pb.go
└── price.proto
此处如果不指定–proto_path参数那么编译命令可以简写为:
protoc --go_out=. --go_opt=paths=source_relative proto/book/price.proto
上面的命令都是将代码生成到demo/proto目录,如果想要将生成的Go代码保存在其他文件夹中(例如pb文件夹),那么我们需要先在demo目录下创建一个pb文件夹。然后在命令行通过–go_out=pb指定生成的Go代码保存的路径。完整命令如下:
protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative book/price.proto
执行上面的命令便会在demo/pb文件夹下生成Go代码。
demo
├── pb
│ └── book
│ └── price.pb.go
└── proto
└── book
├── price.pb.go
└── price.proto
随着业务的复杂度上升,我们可能会定义多个.proto源文件,然后根据需要引入其他的protobuf文件。
在这个示例中,我们在demo/proto/book目录下新建一个book.proto文件,它通过import “book/price.proto”;语句引用了同目录下的price.proto文件。
// demo/proto/book/book.proto syntax = "proto3"; // 声明protobuf中的包名 package book; // 声明生成的Go代码的导入路径 option go_package = "github.com/Generalzy/demo/proto/book"; // 引入同目录下的protobuf文件(注意起始位置为proto_path的下层) import "book/price.proto"; message Book { string title = 1; Price price = 2; }
编译命令如下:
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto
这里有几点需要注意:
因为我们通过编译命令指定–proto_path=proto,所以import导入语句需要从demo/proto文件夹的下层目录book这一层开始写。
因为导入的price.proto与book.proto同属于一个package book;,所以可以直接使用price作为类型,无需添加 package 前缀(即无需写成book.price)。
上述编译命令最终会生成demo/proto/book/book.pb.go文件。
demo
└── proto
└── book
├── book.pb.go
├── book.proto
├── price.pb.go
└── price.proto
在demo/proto目录下新建了一个author文件夹,用来存放与 author 相关的 protobuf 文件。
// demo/proto/author/author.proto
syntax = "proto3";
// 声明protobuf中的包名
package author;
// 声明生成的Go代码的导入路径
option go_package = "github.com/Generalzy/demo/proto/author";
message Info {
string name = 1;
}
此时的目录结构:
demo
└── proto
├── author
│ └── author.proto
└── book
├── book.pb.go
├── book.proto
├── price.pb.go
└── price.proto
假设 book 需要增加一个作者信息的字段——authorInfo,这时需要在demo/proto/book/book.proto中导入其他目录下的 author.proto 文件。具体改动如下。
// proto/proto/book/book.proto syntax = "proto3"; // 声明protobuf中的包名 package book; // 声明生成的Go代码的导入路径 option go_package = "github.com/Generalzy/demo/proto/book"; // 引入同目录下的protobuf文件(注意起始位置为proto_path的下层) import "book/price.proto"; // 引入其他目录下的protobuf文件 import "author/author.proto"; message Book { string title = 1; Price price = 2; author.Info authorInfo = 3; // 需要带package前缀 }
通过import "author/author.proto";
导入了author
包的author.proto
文件,所以在book包下使用Info类型时需要添加其包名前缀即author.Info
。
编译命令如下:
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
此时的目录结构:
demo
└── proto
├── author
│ ├── author.pb.go
│ └── author.proto
└── book
├── book.pb.go
├── book.proto
├── price.pb.go
└── price.proto
有时候需要在定义的 protobuf 文件中使用 Google 定义的类型,例如Timestamp、Any等。
// demo/proto/book/book.proto syntax = "proto3"; // 声明protobuf中的包名 package book; // 声明生成的Go代码的导入路径 option go_package = "github.com/Generalzy/demo/proto/book"; // 引入同目录下的protobuf文件(注意起始位置为proto_path的下层) import "book/price.proto"; // 引入其他目录下的protobuf文件 import "author/author.proto"; // 引入google/protobuf/timestamp.proto文件 import "google/protobuf/timestamp.proto"; message Book { string title = 1; Price price =2; author.Info authorInfo = 3; // 需要带package前缀 // Timestamp是大写T!大写T!大写T! google.protobuf.Timestamp date = 4; // 注意包名前缀 }
通常下载 protobuf编译器的时候会解压得到如下文件:
其中:
import "google/protobuf/timestamp.proto"
就是从此处导入。并且需要将下载得到的可执行文件protoc所在的 bin 目录加到电脑的环境变量中。
如果不是通过这种方式安装的 protobuf ,那么也可以手动将 Google 定义的protobuf文件下载到本地(git clone或者go get,protobuf文件在src下),然后通过 --proto_path指定其路径:
protoc --proto_path=/Users/liwenzhou/workspace/go/pkg/mod/github.com/protocolbuffers/protobuf@v3.21.2+incompatible/src/ --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
或者还可以简单的把下载好的 protobuf 文件拷贝到项目的 proto 目录下:
demo
└── proto
├── author
│ ├── author.pb.go
│ └── author.proto
├── book
│ ├── book.pb.go
│ ├── book.proto
│ ├── price.pb.go
│ └── price.proto
└── google
└── protobuf
└── timestamp.proto
然后执行下面的编译命令:
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
由于通常都是配合 gRPC 来使用 protobuf ,所以也需要基于.proto文件生成Go代码的同时生成 gRPC 代码。
要想生成 gRPC 代码就需要先安装 protoc-gen-go-grpc
插件。(详见rpc/grpc简介)
假设现在要提供一个创建书籍的 RPC 方法,那么在book.proto中添加如下定义:
// demo/proto/book/book.proto
// ...省略...
service BookService{
rpc Create(Book)returns(Book);
}
然后在 protoc 的编译命令添加 gRPC相关输出的参数:
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative --go-grpc_out=proto --go-grpc_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
上述命令就会生成book_grpc.pb.go文件
demo
└── proto
├── author
│ ├── author.pb.go
│ └── author.proto
└── book
├── book.pb.go
├── book.proto
├── book_grpc.pb.go
├── price.pb.go
└── price.proto
gRPC-Gateway 也是日常开发中比较常用的一个工具,它同样也是根据 protobuf 生成相应的代码。
结构如图:
go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
如果找不到命令,用:
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2
// demo/proto/book/book.proto syntax = "proto3"; // 声明protobuf中的包名 package book; // 声明生成的Go代码的导入路径 option go_package = "github.com/Q1mi/demo/proto/book"; // 引入同目录下的protobuf文件(注意起始位置为proto_path的下层) import "book/price.proto"; // 引入其他目录下的protobuf文件 import "author/author.proto"; // 引入google/protobuf/timestamp.proto文件 import "google/protobuf/timestamp.proto"; // 引入google/api/annotations.proto文件 import "google/api/annotations.proto"; message Book { string title = 1; Price price = 2; author.Info authorInfo = 3; // 需要带package前缀 // Timestamp是大写T!大写T!大写T! google.protobuf.Timestamp date = 4; // 注意包名前缀 } service BookService{ rpc Create(Book)returns(Book){ option (google.api.http) = { post: "/v1/book" body: "*" }; }; }
此时的项目目录如下:
demo
└── proto
├── author
│ ├── author.pb.go
│ └── author.proto
├── book
│ ├── book.pb.go
│ ├── book.proto
│ ├── book_grpc.pb.go
│ ├── price.pb.go
│ └── price.proto
└── google
└── api
├── annotations.proto
└── http.proto
这里用到了google官方Api中的两个proto描述文件,直接拷贝不要做修改,里面定义了
protocol buffer扩展的HTTP option,为grpc的http转换提供支持。
如果遇到飘红,不要理会,是可以正常编译的:
这一次编译命令在之前的基础上要继续加上 gRPC-Gateway相关的--grpc-gateway_out=proto --grpc-gateway_opt=paths=source_relative
参数。
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative --go-grpc_out=proto --go-grpc_opt=paths=source_relative --grpc-gateway_out=proto --grpc-gateway_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
最终会编译得到一个book.pb.gw.go文件:
demo └── proto ├── author │ ├── author.pb.go │ └── author.proto ├── book │ ├── book.pb.go │ ├── book.pb.gw.go │ ├── book.proto │ ├── book_grpc.pb.go │ ├── price.pb.go │ └── price.proto └── google └── api ├── annotations.proto └── http.proto
为了方便编译可以在项目下定义Makefile:
.PHONY: gen help PROTO_DIR=proto gen: protoc \ --proto_path=$(PROTO_DIR) \ --go_out=$(PROTO_DIR) \ --go_opt=paths=source_relative \ --go-grpc_out=$(PROTO_DIR) \ --go-grpc_opt=paths=source_relative \ --grpc-gateway_out=$(PROTO_DIR) \ --grpc-gateway_opt=paths=source_relative \ $(shell find $(PROTO_DIR) -iname "*.proto") help: @echo "make gen - 生成pb及grpc代码"
后续想要编译只需在项目目录下执行make gen即可。
在企业的项目开发中,通常会把 protobuf 文件存储到一个单独的代码库中,并在具体项目中通过git submodule
引入。这样做的好处是能够将 protobuf 文件统一管理和维护,避免因 protobuf 文件改动导致的问题。
package main import ( "net/http" "github.com/grpc-ecosystem/grpc-gateway/runtime" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/grpclog" gw "github.com/jergoo/go-grpc-example/proto/hello_http" ) func main() { ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() // grpc服务地址 endpoint := "127.0.0.1:50052" mux := runtime.NewServeMux() opts := []grpc.DialOption{grpc.WithInsecure()} // HTTP转grpc err := gw.RegisterHelloHTTPHandlerFromEndpoint(ctx, mux, endpoint, opts) if err != nil { grpclog.Fatalf("Register handler err:%v\n", err) } grpclog.Println("HTTP Listen on 8080") http.ListenAndServe(":8080", mux) }
开启了一个http server,收到请求后根据路由转发请求到对应的RPC接口获得结果。grpc-gateway做的事情就是帮我们自动生成了转换过程的实现。
这样后台需要开启两个服务两个端口。其实我们也可以只开启一个服务,同时提供http和gRPC调用方式。
package main import ( "crypto/tls" "io/ioutil" "net" "net/http" "strings" "github.com/grpc-ecosystem/grpc-gateway/runtime" pb "github.com/jergoo/go-grpc-example/proto/hello_http" "golang.org/x/net/context" "golang.org/x/net/http2" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/grpclog" ) // 定义helloHTTPService并实现约定的接口 type helloHTTPService struct{} // HelloHTTPService Hello HTTP服务 var HelloHTTPService = helloHTTPService{} // SayHello 实现Hello服务接口 func (h helloHTTPService) SayHello(ctx context.Context, in *pb.HelloHTTPRequest) (*pb.HelloHTTPResponse, error) { resp := new(pb.HelloHTTPResponse) resp.Message = "Hello " + in.Name + "." return resp, nil } func main() { endpoint := "127.0.0.1:50052" conn, err := net.Listen("tcp", endpoint) if err != nil { grpclog.Fatalf("TCP Listen err:%v\n", err) } // grpc tls server creds, err := credentials.NewServerTLSFromFile("../../keys/server.pem","../../keys/server.key") if err != nil { grpclog.Fatalf("Failed to create server TLS credentials %v", err) } grpcServer := grpc.NewServer(grpc.Creds(creds)) pb.RegisterHelloHTTPServer(grpcServer, HelloHTTPService) // gw server ctx := context.Background() dcreds, err := credentials.NewClientTLSFromFile("../../keys/server.pem", "server name") if err != nil { grpclog.Fatalf("Failed to create client TLS credentials %v", err) } dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)} gwmux := runtime.NewServeMux() if err = pb.RegisterHelloHTTPHandlerFromEndpoint(ctx, gwmux, endpoint, dopts); err != nil { grpclog.Fatalf("Failed to register gw server: %v\n", err) } // http服务 mux := http.NewServeMux() mux.Handle("/", gwmux) srv := &http.Server{ Addr: endpoint, Handler: grpcHandlerFunc(grpcServer, mux), TLSConfig: getTLSConfig(), } grpclog.Infof("gRPC and https listen on: %s\n", endpoint) if err = srv.Serve(tls.NewListener(conn, srv.TLSConfig)); err != nil { grpclog.Fatal("ListenAndServe: ", err) } return } func getTLSConfig() *tls.Config { cert, _ := ioutil.ReadFile("../../keys/server.pem") key, _ := ioutil.ReadFile("../../keys/server.key") var demoKeyPair *tls.Certificate pair, err := tls.X509KeyPair(cert, key) if err != nil { grpclog.Fatalf("TLS KeyPair err: %v\n", err) } demoKeyPair = &pair return &tls.Config{ Certificates: []tls.Certificate{*demoKeyPair}, NextProtos: []string{http2.NextProtoTLS}, // HTTP2 TLS支持 } } // grpcHandlerFunc returns an http.Handler that delegates to grpcServer on incoming gRPC // connections or otherHandler otherwise. Copied from cockroachdb. func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler { if otherHandler == nil { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { grpcServer.ServeHTTP(w, r) }) } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"),"application/grpc") { grpcServer.ServeHTTP(w, r) } else { otherHandler.ServeHTTP(w, r) } }) }
gRPC服务端接口的实现没有区别,重点在于HTTP服务的实现。gRPC是基于http2实现
的, net/http 包也实现了http2,所以我们可以开启一个HTTP服务同时服务两个版本的
协议,在注册http handler的时候,在方法 grpcHandlerFunc 中检测请求头信息,决定是
直接调用gRPC服务,还是使用gateway的HTTP服务。 net/http 中对http2的支持要求开
启https,所以这里要求使用https服务。
步骤:
module github.com/Generalzy/tmp go 1.19 require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.0 google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc google.golang.org/grpc v1.53.0 google.golang.org/protobuf v1.28.1 ) require ( github.com/golang/protobuf v1.5.2 // indirect golang.org/x/net v0.5.0 // indirect golang.org/x/sys v0.4.0 // indirect golang.org/x/text v0.6.0 // indirect )
syntax = "proto3"; // 版本声明,使用Protocol Buffers v3版本 option go_package = "github.com/Generalzy/tmp/pb/hello"; // 指定生成的Go代码在你项目中的导入路径 package pb; // 包名 // 导入google/api/annotations.proto import "google/api/annotations.proto"; // 定义服务 service Greeter { // SayHello 方法 rpc SayHello (HelloRequest) returns (HelloResponse) { option (google.api.http) = { post:"/v1/hello" body:"*" }; } } // 请求消息 message HelloRequest { string name = 1; } // 响应消息 message HelloResponse { string reply = 1; }
protoc -I=pb --go_out=pb --go_opt=paths=source_relative --go-grpc_out=pb --go-grpc_opt=paths=source_relative --grpc-gateway_out=pb --grpc-gateway_opt=paths=source_relative hello/hello.proto
go mod tidy
下载需要的包,之后实现serverpackage main import ( "context" "log" "net" "net/http" helloworldpb "github.com/Generalzy/tmp/pb/hello" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" // 注意v2版本 "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) type Server struct { helloworldpb.UnimplementedGreeterServer } func NewServer() *Server { return &Server{} } func (s *Server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloResponse, error) { return &helloworldpb.HelloResponse{Reply: in.Name + " world"}, nil } func main() { // Create a listener on TCP port lis, err := net.Listen("tcp", ":8080") if err != nil { log.Fatalln("Failed to listen:", err) } // 创建一个gRPC server对象 s := grpc.NewServer() // 注册Greeter service到server helloworldpb.RegisterGreeterServer(s, &Server{}) // 8080端口启动gRPC Server log.Println("Serving gRPC on 0.0.0.0:8080") go func() { log.Fatalln(s.Serve(lis)) }() // 创建一个连接到我们刚刚启动的 gRPC 服务器的客户端连接 // gRPC-Gateway 就是通过它来代理请求(将HTTP请求转为RPC请求) conn, err := grpc.DialContext( context.Background(), "0.0.0.0:8080", grpc.WithBlock(), grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { log.Fatalln("Failed to dial server:", err) } gwmux := runtime.NewServeMux() // 注册Greeter err = helloworldpb.RegisterGreeterHandler(context.Background(), gwmux, conn) if err != nil { log.Fatalln("Failed to register gateway:", err) } gwServer := &http.Server{ Addr: ":8090", Handler: gwmux, } // 8090端口提供gRPC-Gateway服务 log.Println("Serving gRPC-Gateway on http://0.0.0.0:8090") log.Fatalln(gwServer.ListenAndServe()) }
示例中没有启用 TLS加密通信,所以这里使用h2c包实现对HTTP/2的支持。h2c 协议是 HTTP/2的非 TLS 版本。
package main import ( "context" "log" "net" "net/http" "strings" helloworldpb "github.com/Generalzy/tmp/pb/hello" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" // 注意v2版本 "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) type Server struct { helloworldpb.UnimplementedGreeterServer } func NewServer() *Server { return &Server{} } func (s *Server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloResponse, error) { return &helloworldpb.HelloResponse{Reply: in.Name + " world"}, nil } func main() { // Create a listener on TCP port lis, err := net.Listen("tcp", ":8091") if err != nil { log.Fatalln("Failed to listen:", err) } // 创建一个gRPC server对象 s := grpc.NewServer() // 注册Greeter service到server helloworldpb.RegisterGreeterServer(s, &Server{}) // gRPC-Gateway mux gwmux := runtime.NewServeMux() dops := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} err = helloworldpb.RegisterGreeterHandlerFromEndpoint(context.Background(), gwmux, "127.0.0.1:8091", dops) if err != nil { log.Fatalln("Failed to register gwmux:", err) } mux := http.NewServeMux() mux.Handle("/", gwmux) // 定义HTTP server配置 gwServer := &http.Server{ Addr: "127.0.0.1:8091", Handler: grpcHandlerFunc(s, mux), // 请求的统一入口 } log.Println("Serving on http://127.0.0.1:8091") log.Fatalln(gwServer.Serve(lis)) // 启动HTTP服务 } // grpcHandlerFunc 将gRPC请求和HTTP请求分别调用不同的handler处理 func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler { return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") { grpcServer.ServeHTTP(w, r) } else { otherHandler.ServeHTTP(w, r) } }), &http2.Server{}) }
[1] 深入解析protobuf
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。