当前位置:   article > 正文

Protocol Buffers V3语法全解_protobuf v3

protobuf v3

protobuf介绍

Protobuf是Protocol Buffer的简称,它是Google公司于2008年开源的一种高效的平台无关、语言无关、可扩展的数据格式,目前Protobuf作为接口规范的描述语言,可以作为Go语言RPC接口的基础工具。

protobuf使用

protobuf是一个与语言无关的一个数据协议,所以我们需要先编写IDL文件然后借助专用工具生成指定语言的代码,从而实现数据的序列化与反序列化过程。

大致开发流程如下: 1. IDL编写 2. 生成指定语言的代码 3. 序列化和反序列化

protoc命令

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
  • 1
  1. IMPORT_PATH指定寻找proto 的目录去解决import 带来的依赖问题,如果省略,默认是当前文件夹。多个import 文件夹可以通过参数使用–proto_path多次来解决,编译器将会按顺序搜索。也可以用简写-I=IMPORT_PATH来表示–proto_path
  2. –cpp_out,–java_out,–go_out等等代表指定生成的语言,可以生成多个语言
  3. path/to/file.proto 代表输入的proto 文件,可以用*.proto 代表输入文件夹内多个文件

语法

基本规范

  • 文件以.proto做为文件后缀,除结构定义外的语句以分号结尾

  • 结构定义可以包含:message、service、enum

  • rpc方法定义结尾的分号可有可无

  • Message命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式

    message SongServerRequest {
          required string song_name = 1;
      }
    
    • 1
    • 2
    • 3
  • Enums类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式

    enum Foo {
          FIRST_VALUE = 1;
          SECOND_VALUE = 2;
      }
    
    • 1
    • 2
    • 3
    • 4
  • Service与rpc方法名统一采用驼峰式命名

字段规则

  • 字段格式:限定修饰符 | 数据类型 | 字段名称 | = | 字段编码值 | [字段默认值]
  • 限定修饰符包含 required\optional\repeated
  1. Required: 表示是一个必须字段
  2. Optional:表示是一个可选字段
  3. Repeated:表示是一个数组

数据类型

Protobuf定义了一套基本数据类型。

.protoC++JavaPythonGoRubyC#
doubledoubledoublefloatfloat64Floatdouble
floatfloatfloatfloatfloat32Floatfloat
int32int32intintint32Fixnum or Bignumint
int64int64longing/long[3]int64Bignumlong
uint32uint32int[1]int/long[3]uint32Fixnum or Bignumuint
uint64uint64long[1]int/long[3]uint64Bignumulong
sint32int32intintjint32Fixnum or Bignumint
sint64int64longint/long[3]int64Bignumlong
fixed32uint32int[1]intuint32Fixnum or Bignumuint
fixed64uint64long[1]int/long[3]uint64Bignumulong
sfixed32int32intintint32Fixnum or Bignumint
sfixed64int64longint/long[3]int64Bignumlong
boolboolbooleanbooleanboolTrueClass/FalseClassbool
stringstringStringstr/unicode[4]stringString(UTF-8)string
bytesstringByteStringstr[]byteString(ASCII-8BIT)ByteString

fixed32的打包效率比int32的效率高,但是使用的空间一般比int32多。因此一个属于时间效率高,一个属于空间效率高

  • 字段名称
  1. 字段名称的命名与C、C++、Java等语言的变量命名方式几乎是相同的
  2. protobuf建议字段的命名采用以下划线分割的驼峰式。例如 first_name 而不是firstName
  • 字段编码值
  1. 有了该值,通信双方才能互相识别对方的字段,相同的编码值,其限定修饰符和数据类型必须相同,编码值的取值范围为 1~2^32(4294967296)
  2. 其中 1~15的编码时间和空间效率都是最高的,编码值越大,其编码的时间和空间效率就越低,所以建议把经常要传递的值把其字段编码设置为1-15之间的值
  3. 1900~2000编码值为Google protobuf 系统内部保留值,建议不要在自己的项目中使用
  • 字段默认值
    当在传递数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端

message定义

字段名用小写,转为go文件后自动变为大写,message就相当于结构体

syntax = "proto3";

message SearchRequest {
    string query = 1;            // 查询字符串
    int32  page_number = 2;     // 页码
    int32  result_per_page = 3;   // 每页条数
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

message嵌套

支持嵌套消息,消息可以包含另一个消息作为其字段。也可以在消息内定义一个新的消息
内部声明的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;
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

service定义

message定义结构体,service定义接口

service SearchService {
    rpc Search (SearchRequest) returns (SearchResponse) {}
}
  • 1
  • 2
  • 3

定义消息类型

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 文件第一行指定您使用的语法:如果不这样做,编译器将假定使用的是proto2。
  • 这必须是.proto3文件的第一个非空、非注释行。
  • 消息定义指定了三个字段(名称/值对),每个字段对应于要包含在该类型消息中的数据段。

指定字段类型

在上面的示例中,所有字段都是标量类型(scalar types): 两个整数(page_number和 result_per_page)和一个字符串(query)。但是也可以为字段指定组合类型,包括枚举和其他消息类型。

在这里插入图片描述

分配字段编号

  1. 消息定义中的每个字段都有一个唯一编号。
  2. 这些字段编号用于标识消息二进制格式的字段,并且在消息类型投入使用后不应更改。
  3. 请注意,1到15范围内的字段编号需要一个字节进行编码,编码内包括字段号和字段类型(参考协议缓冲区编码)。
  4. 16到2047范围内的字段编号需要两个字节(进行编码)。因此,您应该把1到15的消息编号留给非常频繁出现的消息元素。
  5. 请记住为将来可能添加的频繁出现的元素留出一些空间。可以指定的最小字段号为1,最大字段号为(2**29)-1(字段数字会作为key,key最后三位是类型)或536870911。您也不能使用数字1900019999(字段描述符),因为它们是协议缓冲区的保留数字,如果你在你的.proto中使用了这些数字,编译器会报错。同样,不能使用任何以前保留的字段号。

指定字段规则

消息字段可以是下列字段之一:

  1. singular: 格式正确的消息可以有这个字段的零个或一个(但不能多于一个)。这是 proto3语法的默认字段规则。
  2. repeated: 该字段可以在格式正确的消息中重复任意次数(包括零次)。重复值的顺序将被保留。

在 proto3中,标量数值类型的repeated字段默认使用packed编码。(参考Protocol Buffer Encoding,可以找到关于packed编码的更多信息)

添加更多消息类型

  1. 可以在一个.proto 文件中定义多个消息类型。
  2. 如果想定义与 SearchRequest 消息类型对应的应答消息格式SearchResponse,就可以将其添加到同一个.proto文件中。
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

message SearchResponse {
 ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注释

.proto文件使用了C/C++风格的///* ... */语法。

/* SearchRequest 表示一个分页查询 
 * 其中有一些字段指示响应中包含哪些结果 */

message SearchRequest {
  string query = 1;
  int32 page_number = 2;  // 页码数
  int32 result_per_page = 3;  // 每页返回的结果数
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

保留字段

  1. 如果通过完全删除某个字段或把它注释掉来更新消息类型,则将来的用户可以在对该类型进行自己的更新时重用该字段编号。
  2. 如果以后加载相同.proto的旧版本,这可能会导致数据损坏、隐私漏洞等严重问题。
  3. 确保不会发生这种情况的一种方法是使用reserved关键字指定已删除字段的字段编号为保留编号(也要指定已删除字段名称为保留名称(name),以规避JSON序列化问题)。将来有任何用户试图使用这些字段标识符时,协议缓冲区编译器将报错。
message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}
  • 1
  • 2
  • 3
  • 4

注意,不能在同一个reserved语句中混合字段名和字段编号。

从.proto文件生成了什么?

当使用 protocol buffer 编译器来运行.proto文件时,编译器用选择的语言生成描述的消息类型,包括获取和设置字段值,将消息序列化为输出流,以及从输入流解析消息的代码。

  1. 对于Python:Python 编译器为.proto文件中的每个消息类型生成一个带静态描述符的模块,然后与 metaclass 一起使用,在运行时创建必要的 Python 数据访问类。

  2. 对于 Go:编译器为文件中的每种消息类型生成一个类型(type)到一个.pb.go 文件。

  3. 其他…

值类型

.proto TypeNotesC++ TypeJava/Kotlin Type[1]Python Type[3]Go TypePHP Type
doubledoubledoublefloatfloat64float
floatfloatfloatfloatfloat32float
int32使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,则使用 sint32代替。int32intintint32integer
int64使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,则使用 sint64代替。int64longint/long[4]int64integer/string[6]
uint32使用变长编码。uint32int[2]int/long[4]uint32integer
uint64使用变长编码。uint64long[2]int/long[4]uint64integer/string[6]
sint32使用可变长度编码。带符号的 int 值。这些编码比普通的 int32更有效地编码负数。int32intintint32integer
sint64使用可变长度编码。带符号的 int 值。这些编码比普通的 int64更有效地编码负数。int64longint/long[4]int64integer/string[6]
fixed32总是四个字节。如果值经常大于228,则比 uint32更有效率。uint32int[2]int/long[4]uint32integer
fixed64总是8字节。如果值经常大于256,则比 uint64更有效率。uint64integer/string[6]
sfixed32总是四个字节。int32intintint32integer
sfixed64总是八个字节。int64integer/string[6]
boolboolbooleanboolboolboolean
string字符串必须始终包含 UTF-8编码的或7位 ASCII 文本,且不能长于232。stringStringstr/unicode[5]stringstring
bytes可以包含任何不超过232字节的任意字节序列。stringByteStringstr (Python 2) bytes (Python 3)[]bytestring

默认值

当解析消息时,如果编码消息不包含特定的 singular 元素,则解析对象中的相应字段将设置为该字段的默认值。

  1. string:默认值为空字符串
  2. bytes:默认值为空字节
  3. boolean:默认值为false
  4. 数值类型:默认值为0
  5. 枚举:默认值为第一个定义的枚举值,该值必须是0
  6. 消息字段:不设默认值,它的确切值取决于语言。
  7. repeated 字段的默认值为空(通常是适配语言中的空列表)。

枚举

定义消息类型时,可能希望其中一个字段只包含预定义值列表中的一个

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

Corpus enum 的第一个常量映射为零,每个 enum 定义必须包含一个常量,该常量映射为零且作为它的第一个元素。这是因为:

  1. 必须有一个零值,这样我们就可以使用0作为数值默认值。
  2. 零值必须是第一个元素,以便与 proto2语义兼容,其中第一个枚举值总是默认值。

可以通过将相同的值赋给不同的枚举常量来定义别名。为此,需要将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;  // 取消对此行的注释将导致内部出现编译错误,外部出现警告消息。
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  1. 枚举器常量必须在32位整数的范围内。由于enum使用可变编码,因此负值效率很低,所以不建议使用。您可以在定义的消息内部定义枚举,如上面的示例所示,也可以在外部定义枚举——这些枚举可以在.proto文件中的任何消息定义中重用。您还可以使用_MessageType_._EnumType_语法将一条消息中声明的枚举类型用作另一条消息中的字段类型。
  2. 当使用协议缓冲区编译器编译一个使用了枚举的.proto文件时,对于Python,会生成一个特殊EnumDescriptor类,用于在运行时生成的类中创建一组具有整数值的符号常量。
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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

使用其他消息类型

当使用其他消息的时候,如果在本文件,直接使用就可以了。Result代表自定义消息类型

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  // []string
  repeated string snippets = 3;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

导入定义

可以通过 import 来使用来自其他 .proto 文件的定义。

import "myproject/other_protos.proto";
  • 1

默认情况下,只能从直接导入的 .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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
// user.proto
syntax = "proto3";
option go_package = "protos/pbs";

//用户定义
message User {
	string Id=1;
	string Name=2;
	string Age=3;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

嵌套类型

可以在其他消息类型中定义和使用消息类型,在 SearchResponse消息中定义Result:

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

如果要在其父消息类型之外重用此消息类型,请通过_Parent_._Type_使用:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}
  • 1
  • 2
  • 3

可以随心所欲地将信息一层又一层嵌入其中:

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;
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

更新消息类型

如果现有的消息类型不再满足你的所有需要(例如,消息格式需要增加一个字段),但是仍然希望使用用旧格式创建的代码。在不破坏任何现有代码的情况下更新消息类型非常简单,只需遵守以下规则:

  1. 不要更改任何现有字段的字段编号
  2. 如果添加新字段,则使用“旧”消息格式的代码序列化的任何消息仍然可以由新生成的代码进行解析。你应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息交互。类似地,由新代码创建的消息也可以由旧代码解析:旧二进制文件在解析时忽略新字段。
  3. 可以删除字段,前提是在更新的消息类型中不再使用此字段编号。您可能需要重命名字段,或者添加前缀OBSOLETE_,或者使用reserved保留字段编号,以便.proto的未来用户不会意外地重用该编号。
  4. int32uint32int64uint64bool都是兼容的——这意味着你可以在不破坏向前或向后兼容性的情况下将一个字段从这些类型中的一个更改为另一个。
  5. 如果一个数字被解析到一个并不适当的类型中,你会得到与在 C++ 中将数字转换为该类型相同的效果(例如,如果一个64位的数字被读作 int32,它将被截断为32位)
  6. sint32sint64相互兼容,但与其他整数类型不兼容。
  7. stringbytes是兼容的,只要字节是有效的 UTF-8。
  8. 如果bytes包含消息的编码版本,则嵌入的消息与字节兼容。
  9. fixed32sfixed32兼容 fixed64sfixed64兼容。
  10. 对于stringbytes和消息字段,optional字段与repeated字段兼容。给定重复字段的序列化数据作为输入,如果该字段是基元类型字段,则期望该字段为optional(可选的)字段的客户端将获取最后一个输入值;如果该字段是消息类型字段,则合并所有输入元素。请注意,对于数值类型(包括boolenum),这通常是不安全的。数字类型的重复字段可以按压缩格式序列化,当需要可选字段时,将无法正确解析压缩格式。
  11. enum 在格式方面与 int32uint32int64uint64兼容(请注意,如果不适合,值将被截断)。但是要注意,当消息被反序列化时,客户端代码可能会对它们进行不同的处理:例如,未识别的proto3枚举类型将保留在消息中,但在反序列化消息时如何表示这些类型取决于客户端语言。整型字段总是保持它们的值。
  12. 将单个值更改为新的oneof成员是安全的,并且二进制兼容。如果确保没有代码一次设置多个字段,那么将多个字段移动到新的oneof字段中可能是安全的。将任何字段移到现有oneof字段中都不安全。

未知字段

  1. 未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当一个旧二进制代码解析一个带有新字段的新二进制代码发送的数据时,这些新字段在旧二进制代码中成为未知字段。
  2. 最初,proto3消息在解析过程中总是丢弃未知字段,但在3.5版中,重新引入了未知字段的保留,以匹配proto2的行为。
  3. 在版本3.5和更高版本中,解析期间保留未知字段,并将其包含在序列化输出中。

any任意类型

  1. Any 消息类型允许你将消息作为嵌入类型使用,而不需要其 .proto 定义。
  2. Any包含一个以字节表示的任意序列化消息,以及一个URL,该URL充当该消息的全局唯一标识符并解析为该消息的类型。
  3. 要使用 Any类型,需要导入google/protobuf/any.proto
  4. 如果编译报错,将上述文件下载好复制到自己的proto 目录再编译。
import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

给定消息类型的默认类型 URL 是type.googleapis.com/_packagename_._messagename_

不同的语言实现将支持运行库助手以类型安全的方式打包和解包 Any值。例如在java中,Any类型会有特殊的pack()和unpack()访问器,在C++中会有PackFrom()和UnpackTo()方法。

oneof

如果在平时在一个消息有许多字段,但是最多设置一个字段,我们可以使用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;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

要定义 oneof 字段需要在你的.proto文件中使用oneof关键字并在后面跟上名称,然后将其中一个字段添加到该字段的定义中。你可以添加任何类型的字段,除了map字段和repeated字段。

oneof 特性

  1. 设置一个字段将自动清除该字段的所有其他成员。如果设置了多个 oneof字段,那么只保留最后的一个值。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message();   // Will clear name field.
CHECK(!message.has_name());
  • 1
  • 2
  • 3
  • 4
  • 5
  1. oneof 不支持repeated。
  2. 反射作用于oneof的字段。
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())
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

兼容性问题

添加或删除其中一个字段时要小心。如果检查 oneof 的值返回 None/NOT_SET,则可能意味着 oneof 尚未设置或已设置为 oneof 的另一个字段。这种情况是无法区分的,因为无法知道未知字段是否是 oneof 成员。

标签重用问题

  1. 将 optional 可选字段移入或移出 oneof:在序列化和解析 message 后,你可能会丢失一些信息(某些字段将被清除)。但是,你可以安全地将单个字段移动到新的 oneof 中,并且如果已知只有一个字段被设置,则可以移动多个字段。
  2. 删除 oneof 字段并将其重新添加回去:在序列化和解析 message 后,这可能会清除当前设置的 oneof 字段。
  3. 拆分或合并 oneof:这与移动常规的 optional 字段有类似的问题。

Maps

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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

其中key_type可以是任何整型或字符串类型(因此,除了浮点类型和字节之外的任何标量类型)。注意enum不是一个有效的key_typevalue_type可以是除其他map以外的任何类型。

  1. 映射字段不能使用repeated关键字。
  2. 映射值的Wire格式排序和映射迭代排序未定义,因此不能依赖特定顺序的映射项。
    为.proto生成文本格式时,映射按键排序。数字键按数字排序。
  3. 如果为映射字段提供键但没有值,则序列化字段时的行为与语言有关。在C++、Java和Python中,类型的默认值被序列化,而在其他语言中没有任何序列化。
  4. 生成的map API目前适用于所有proto3支持的语言。

Packages

可以向.proto文件中添加可选的package明符,以防止协议消息类型之间的名称冲突,
可以理解为go的包。

package foo.bar;
message Open { ... }
  • 1
  • 2

定义服务

如果要在RPC(Remote Procedure Call,远程过程调用)系统中使用消息类型,可以在.proto文件中定义RPC服务接口。

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}
  • 1
  • 2
  • 3

JSON 映射

proto3JSONJSON example描述【译】
messageobject{"fooBar": v, "g": null, …}生成JSON对象。消息字段名被映射到lowerCamelCase并成为JSON对象键。如果指定了json_name选项,则指定的值将用作键。解析器接受小驼峰命秘法名称(或由json_name选项指定的名称)和原始proto字段名称。null是所有字段类型的可接受值,并被视为相应字段类型的默认值。
enumstring"FOO_BAR"使用proto中指定的枚举值的名称。解析器接受枚举名和整数值。
map<K,V>object{"k": v, …}所有键都转换为字符串。
repeated Varray[v, …]null被接受为空列表[]
booltrue, falsetrue, false
stringstring"Hello World!"
bytesbase64 string"YWJjMTIzIT8kKiYoKSctPUB+"JSON值将是使用带填充的标准base64编码的字符串编码的数据。包含或不包含填充的标准或url安全base64编码都可以接受。
int32, fixed32, uint32number1, -10, 0JSON值将是一个十进制数。可接受数字或字符串。
int64, fixed64, uint64string"1", "-10"JSON值将是十进制字符串。可接受数字或字符串。
float, doublenumber1.1, -10.0, 0, "NaN", "Infinity"JSON值将是一个数字或特殊字符串值"NaN", "Infinity",和"-Infinity"中的一个。数字或字符串都可以接受。指数符号也被接受。-0被认为等于0。
Anyobject{"@type": "url", "f": v, … }如果`Any`类型包含一个具有特殊JSON映射的值,它将按如下方式转换:{"@type":xxx,"value":yyy}。否则,该值将转换为JSON对象,并插入“@type”字段以指示实际的数据类型。
Timestampstring"1972-01-01T10:00:20.021Z"使用RFC3339,其中生成的输出将始终是**Z**规格化的,并使用0、3、6或9个小数位数。也接受“Z”以外的偏移。
Durationstring"1.000340012s", "1s"生成的输出总是包含0、3、6或9个小数位数(取决于所需的精度),后跟“s”后缀。接受任何小数位数(没有小数也可以),只要它们符合纳秒精度,并且需要“s”后缀。
Structobject{ … }任何JSON对象。见struct.proto
Wrapper typesvarious types2, "2", "foo", true, "true", null, 0,包装器在JSON中使用与包装原语类型相同的表示形式,只是在数据转换和传输期间允许并保留null
FieldMaskstring"f.fooBar,h"field_mask.proto
ListValuearray[foo, bar, …]
Valuevalue任何JSON值。详见google.protobuf.Value
NullValuenullJSON null
Emptyobject{}空的JSON对象

Options

  1. .proto文件中的单个声明可以使用许多 选项 进行注释。选项不会更改声明的总体含义,但可能会影响在特定上下文中处理声明的方式。可用选项的完整列表在google/protobuf/descriptor.proto中定义。

  2. deprecated选项: 设为true 代表字段被废弃,在新代码不应该被使用,在大多数语言都没有实际的效果

option go_package = "tmp/pb";  // 指定生成的Go代码在你项目中的导入路径

int32 old_field = 6 [deprecated = true];
  • 1
  • 2
  • 3

Go使用pb3注意点

编译器调用(protoc-gen-go)

当使用go_out 标志调用 protoc 时,protocol buffer编译器将生成 Go 代码。protocol buffer编译器会将生成的Go代码输出到命令行参数go_out指定的位置。go_out标志的参数是你希望编译器编写 Go 输出的目录。编译器为每个.proto 文件输入创建一个源文件。输出文件的名称是通过将.proto 扩展名替换为.pb.go 而创建的。

生成的.pb.go文件放置的目录取决于编译器标志。有以下几种输出模式:

  1. paths=import:输出文件放在以 Go 包的导入路径命名的目录中。例如,protos/buzz.proto文件中带有example.com/project/protos/fizz的导入路径,则输出的生成文件会保存在example.com/project/protos/fizz/buzz.pb.go。如果未指定路径标志,这就是默认输出模式。
  2. module=$PREFIX:输出文件放在以 Go 包的导入路径命名的目录中,但是从输出文件名中删除了指定的目录前缀。例如,输入文件 pros/buzz.proto,其导入路径为 example.com/project/protos/fizz 并指定example.com/project为module前缀,结果会产生一个名为 pros/fizz/buzz.pb.go 的输出文件。在module路径之外生成任何 Go 包都会导致错误。此模式对于将生成的文件直接输出到 Go 模块非常有用。
  3. 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
  • 1

编译器将从 src 目录中读取输入文件 foo.protobar/baz.proto,并将输出文件 foo.pb.gobar/baz.pb.go 写入 out 目录。如果需要,编译器会自动创建嵌套的输出子目录,但不会创建输出目录本身。

package

为了生成 Go 代码,必须为每个 .proto 文件(包括那些被生成的 .proto 文件传递依赖的文件)提供 Go 包的导入路径。有两种方法可以指定 Go 导入路径:

  • 通过在 .proto 文件中声明它。
  • 通过在调用 protoc 时在命令行上声明它。

建议在 .proto 文件中声明它,以便 .proto 文件的 Go 包可以与 .proto 文件本身集中标识,并简化调用 protoc 时传递的标志集。 如果给定 .proto 文件的 Go 导入路径由 .proto 文件本身和命令行提供,则后者优先于前者。

Go 导入路径是在 .proto 文件中指定的,通过声明带有 Go 包的完整导入路径的 go_package 选项来创建 proto 文件。用法示例:

option go_package = "example.com/project/protos/fizz";
  • 1

调用编译器时,可以在命令行上指定 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
  • 1
  • 2
  • 3
  • 4

由于所有 .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 导入路径之间没有关联。

Go语言使用protoc示例

新建一个名为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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

项目当前的目录结构如下:

demo
└── proto
    └── book
        └── price.proto
  • 1
  • 2
  • 3
  • 4

生成代码把最终生成的Go代码还保存在proto文件夹中:

protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/price.proto
  • 1

其中:

  • –proto_path=proto 表示从proto目录下读取proto文件。
  • –go_out=proto 表示生成的Go代码保存的路径。
  • –go_opt=paths=source_relative 表示输出文件与输入文件放在相同的相对目录中。
  • book/price.proto 表示在proto目录下的book/price.proto文件。

此外,–proto_path有一个别名-I,上述编译命令也可以这样写。

protoc -I=proto --go_out=proto --go_opt=paths=source_relative book/price.proto
  • 1

执行上述命令将会在proto目录下生成book/price.pb.go文件。

demo
└── proto
    └── book
        ├── price.pb.go
        └── price.proto
  • 1
  • 2
  • 3
  • 4
  • 5

此处如果不指定–proto_path参数那么编译命令可以简写为:

protoc --go_out=. --go_opt=paths=source_relative proto/book/price.proto
  • 1

上面的命令都是将代码生成到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
  • 1

执行上面的命令便会在demo/pb文件夹下生成Go代码。

demo
├── pb
│   └── book
│       └── price.pb.go
└── proto
    └── book
        ├── price.pb.go
        └── price.proto
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

import同目录下protobuf文件

随着业务的复杂度上升,我们可能会定义多个.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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

编译命令如下:

protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto
  • 1

这里有几点需要注意:

  1. 因为我们通过编译命令指定–proto_path=proto,所以import导入语句需要从demo/proto文件夹的下层目录book这一层开始写。

  2. 因为导入的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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

import其他目录下文件

在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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

此时的目录结构:

demo
└── proto
    ├── author
    │   └── author.proto
    └── book
        ├── book.pb.go
        ├── book.proto
        ├── price.pb.go
        └── price.proto
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

假设 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前缀
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

通过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
  • 1

此时的目录结构:

demo
└── proto
    ├── author
    │   ├── author.pb.go
    │   └── author.proto
    └── book
        ├── book.pb.go
        ├── book.proto
        ├── price.pb.go
        └── price.proto
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

import google 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;  // 注意包名前缀
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

通常下载 protobuf编译器的时候会解压得到如下文件:
在这里插入图片描述
其中:

  1. bin 目录下的 protoc 是可执行文件。
  2. include 目录下的是 google 定义的.proto文件,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
  • 1

或者还可以简单的把下载好的 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

然后执行下面的编译命令:

protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
  • 1

生成gRPC代码

由于通常都是配合 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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

然后在 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
  • 1

上述命令就会生成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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

gRPC-Gateway(网关)

gRPC-Gateway 也是日常开发中比较常用的一个工具,它同样也是根据 protobuf 生成相应的代码。

在这里插入图片描述
结构如图:
在这里插入图片描述

安装

go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
  • 1

如果找不到命令,用:

go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2
  • 1

为protobuf文件添加注释

// 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: "*"
        };
    };
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

此时的项目目录如下:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这里用到了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
  • 1

最终会编译得到一个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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

为了方便编译可以在项目下定义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代码"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

后续想要编译只需在项目目录下执行make gen即可。

在企业的项目开发中,通常会把 protobuf 文件存储到一个单独的代码库中,并在具体项目中通过git submodule引入。这样做的好处是能够将 protobuf 文件统一管理和维护,避免因 protobuf 文件改动导致的问题。

实现server

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)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

开启了一个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)
		}
	})
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98

gRPC服务端接口的实现没有区别,重点在于HTTP服务的实现。gRPC是基于http2实现
的, net/http 包也实现了http2,所以我们可以开启一个HTTP服务同时服务两个版本的
协议,在注册http handler的时候,在方法 grpcHandlerFunc 中检测请求头信息,决定是
直接调用gRPC服务,还是使用gateway的HTTP服务。 net/http 中对http2的支持要求开
启https,所以这里要求使用https服务。

步骤:

  1. 注册开启TLS的grpc服务
  2. 注册开启TLS的gateway服务,地址指向grpc服务
  3. 开启HTTP server

hello world实例

目录结构

在这里插入图片描述

mod文件

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
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

具体步骤

  1. 编写hello.proto文件
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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  1. 执行pb和网关编译:
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
  • 1

在这里插入图片描述

  1. go mod tidy下载需要的包,之后实现server
package 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())
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

同一个端口提供HTTP API和gRPC API

示例中没有启用 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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

调用接口

在这里插入图片描述

参考文档

[1] 深入解析protobuf

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号