写给go开发者的gRPC教程-protobuf基础

科技   2024-08-06 08:45   北京  

gRPC是谷歌开源的一款高性能、支持多种开发语言的服务框架,对于一个rpc我们关注如下几方面:

序列化协议gRPC使用protobuf,首先使用protobuf定义服务,然后使用这个文件来生成客户端和服务端的代码。因为pb是跨语言的,因此即使服务端和客户端语言并不一致也是可以互相序列化和反序列化的

网络传输层。gRPC使用http2.0协议,http2.0相比于HTTP 1.x ,大幅度的提升了 web 性能。

Protobuf IDL

所谓序列化通俗来说就是把内存的一段数据转化成二进制并存储或者通过网络传输,而读取磁盘或另一端收到后可以在内存中重建这段数据

1、protobuf协议是跨语言跨平台的序列化协议。

2、protobuf本身并不是和gRPC绑定的。它也可以被用于非RPC场景,如存储等

json xml都是一种序列化的方式,只是他们不需要提前预定义idl,且具备可读性,当然他们传输的体积也因此较大,可以说是各有优劣

所以先来介绍下protobuf的idl怎么写。protobuf最新版本为proto3,在这里你可以看到详细的文档说明:https://protobuf.dev/programming-guides/proto3/

定义消息类型

protobuf里最基本的类型就是message,每一个message都会有一个或者多个字段(field),其中字段包含如下元素

  • 类型:类型不仅可以是标量类型(intstring等),也可以是复合类型(enum等),也可以是其他message

  • 字段名:字段名比较推荐的是使用下划线/分隔名称

  • 字段编号:一个message内每一个字段编号都必须唯一的,在编码后其实传递的是这个编号而不是字段名

  • 字段规则:消息字段可以是以下字段之一

    • singular:格式正确的消息可以有零个或一个字段(但不能超过一个)。使用 proto3 语法时,如果未为给定字段指定其他字段规则,则这是默认字段规则

    • optional:与 singular 相同,不过您可以检查该值是否明确设置

    • repeated:在格式正确的消息中,此字段类型可以重复零次或多次。系统会保留重复值的顺序

    • map:这是一个成对的键值对字段

  • 保留字段:为了避免再次使用到已移除的字段可以设定保留字段。如果任何未来用户尝试使用这些字段标识符,协议缓冲区编译器就会报错

标量值类

标量类型会涉及到不同语言和编码方式,后续有机会深入讲

.proto TypeGo TypeNotes
doublefloat64
floatfloat32
int32int32使用可变长度的编码。对负数的编码效率低下 - 如果您的字段可能包含负值,请改用 sint32。
int64int64使用可变长度的编码。对负数的编码效率低下 - 如果字段可能有负值,请改用 sint64。
uint32uint32使用可变长度的编码。
uint64uint64使用可变长度的编码。
sint32int32使用可变长度的编码。有符号整数值。与常规 int32 相比,这些函数可以更高效地对负数进行编码。
sint64int64使用可变长度的编码。有符号整数值。与常规 int64 相比,这些函数可以更高效地对负数进行编码。
fixed32uint32始终为 4 个字节。如果值通常大于 2^28,则比 uint32 更高效。
fixed64uint64始终为 8 个字节。如果值通常大于 2^56,则比 uint64 更高效。
sfixed32int32始终为 4 个字节。
sfixed64int64始终为 8 个字节。
boolbool
stringstring字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且长度不得超过 232。
bytes[]byte可以包含任意长度的 2^32 字节。

复合类型

数组

message SearchResponse {
repeated Result results = 1;
}

message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}

枚举

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;
}

服务

定义的method仅能有一个入参和出参数。如果需要传递多个参数需要定义成message

service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}

使用其他消息类型

使用import引用另外一个文件的pb

syntax = "proto3";

import "google/protobuf/wrappers.proto";

package ecommerce;

message Order {
string id = 1;
repeated string items = 2;
string description = 3;
float price = 4;
google.protobuf.StringValue destination = 5;
}

protoc使用

protoc就是protobuf的编译器,它把proto文件编译成不同的语言

📖 安装

  • Linux, using apt or apt-get, for example:
$ apt install -y protobuf-compiler
$ protoc --version  # Ensure compiler version is 3+
  • MacOS, using Homebrew[1]:
$ brew install protobuf
$ protoc --version  # Ensure compiler version is 3+

📖 使用

$ protoc --help
Usage: protoc [OPTION] PROTO_FILES

  -IPATH, --proto_path=PATH   指定搜索路径
  --plugin=EXECUTABLE:
  
  ....
 
  --cpp_out=OUT_DIR           Generate C++ header and source.
  --csharp_out=OUT_DIR        Generate C# source file.
  --java_out=OUT_DIR          Generate Java source file.
  --js_out=OUT_DIR            Generate JavaScript source.
  --objc_out=OUT_DIR          Generate Objective C header and source.
  --php_out=OUT_DIR           Generate PHP source file.
  --python_out=OUT_DIR        Generate Python source file.
  --ruby_out=OUT_DIR          Generate Ruby source file
  
   @<filename>                proto文件的具体位置

1.搜索路径参数

第一个比较重要的参数就是搜索路径参数,即上述展示的-IPATH, --proto_path=PATH。它表示的是我们要在哪个路径下搜索.proto文件,这个参数既可以用-I指定,也可以使用--proto_path=指定。

如果不指定该参数,则默认在当前路径下进行搜索;另外,该参数也可以指定多次,这也意味着我们可以指定多个路径进行搜索。

2.语言插件参数

语言参数即上述的--cpp_out=--python_out=等,protoc支持的语言长达13种,且都是比较常见的

运行help出现的语言参数,说明protoc本身已经内置该语言对应的编译插件,我们无需安装

Language
C++ (include C++ runtime and protoc)
Java
Python
Objective-C
C#
Ruby
PHP

下面的语言是由google维护,通过protoc的插件机制来实现,所以仓库单独维护

  • Dart
  • Go

3.proto文件位置参数

proto文件位置参数即上述的@<filename>参数,指定了我们proto文件的具体位置,如proto1/greeter/greeter.proto

📖 语言插件

✨ golang插件

非内置的语言支持就得自己单独安装语言插件,比如--go_out=对应的是protoc-gen-go,安装命令如下:

# 最新版
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

#
 指定版本
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.3.0

可以使用下面的命令来生成代码

$ protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto
注意
protoc-gen-go要求pb文件必须指定go包的路径,即
option go_package = "liangwt/note/grpc/example/ecommerce";
--go_out

指定go代码生成的基本路径

--go_opt:设定插件参数

protoc-gen-go提供了 --go_opt 来为其指定参数,并可以设置多个

1、如果使用 paths=import , 生成的文件会按go_package路径来生成,当然是在--go_out目录下,即

$go_out/$go_package/pb_filename.pb.go

2、如果使用 paths=source_relative , 就在当前pb文件同路径下生成代码。注意pb的目录也被包含进去了。即

$go_out/$pb_filedir/$pb_filename.pb.go

✨ grpc go插件

google.golang.org/protobuf中,protoc-gen-go纯粹用来生成pb序列化相关的文件,不再承载gRPC代码生成功能。

生成gRPC相关代码需要安装grpc-go相关的插件protoc-gen-go-grpc

 $ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

执行code gen命令

$ protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    routeguide/route_guide.proto
--go-grpc_out

指定grpc go代码生成的基本路径

命令会产生如下文件

  • route_guide.pb.go, protoc-gen-go的产出物,包含所有类型的序列化和反序列化代码

  • route_guide_grpc.pb.go, protoc-gen-go-grpc的产出物,包含

    • 定义在 RouteGuide service中的用来给client调用的接口定义
    • 定义在 RouteGuide service中的用来给服务端实现的接口定义
--go-grpc_opt

protoc-gen-go类似,protoc-gen-go-grpc提供 --go-grpc_opt 来指定参数,并可以设置多个


github.com/golang/protobuf vs google.golang.org/protobuf

github.com/golang/protobuf虽然已经废弃,但网上搜索时经常还能搜到,方便理解整理两者区别。

代码差异

这两个库,google.golang.org/protobufgithub.com/golang/protobuf的升级版本,v1.4.0之后github.com/golang/protobuf仅是google.golang.org/protobuf的包装

功能差异

google.golang.org/protobuf,纯粹用来生成pb序列化相关的文件,不再承载gRPC代码生成功能。生成gRPC相关代码需要安装grpc-go相关的插件protoc-gen-go-grpc

github.com/golang/protobuf ,可以同时生成pb和gRPC相关代码的

用法差异

google.golang.org/protobuf

$ protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    routeguide/route_guide.proto

github.com/golang/protobuf

$ protoc --go_out=plugins=grpc,paths=import:. \
    routeguide/route_guide.proto

--go_out的写法是,参数之间用逗号隔开,最后加上冒号来指定代码的生成位置,比如--go_out=plugins=grpc,paths=import:.

--go_out主要的两个参数为pluginspaths,分别表示生成Go代码所使用的插件,以及生成的Go代码的位置。

plugins参数有不带grpc和带grpc两种(应该还有其它的,目前知道的有这两种),两者的区别如下,带grpc的会多一些跟gRPC相关的代码,实现gRPC通信

paths参数有两个选项,分别是 importsource_relative,默认为 import

  • import表示按照生成的Go代码的包的全路径去创建目录层级

  • source_relative 表示按照 proto源文件的目录层级去创建Go代码的目录层级,如果目录已存在则不用创建。

总之,用google.golang.org/protobuf就对了!


Buf 工具

可以看到使用protoc的时候,当使用的插件逐渐变多,插件参数逐渐变多时,命令行执行并不是很方便和直观。例如后面使用到了grpc-gateway+swagger插件时

$ protoc -I ./pb \
  --go_out ./ecommerce --go_opt paths=source_relative \
  --go-grpc_out ./ecommerce --go-grpc_opt paths=source_relative \
  --grpc-gateway_out ./ecommerce --grpc-gateway_opt paths=source_relative \
  --openapiv2_out ./doc --openapiv2_opt logtostderr=true \
  ./pb/ecommerce/v1/product.proto

其次依赖某些外部的protobuf文件时,只能通过拷贝到本地的方式,也不够方便

因此诞生了✨ Buf 这个项目,它除了能解决上述问题,还有额外的功能

  • 不兼容破坏检查
  • linter
  • 集中式的版本管理

初始化模块

在pb文件的根目录执行,为这个pb目录创建一个buf的模块。此后便可以使用buf的各种命令来管理这个buf模块了

$ buf mod init

此时会在根目录多出一个buf.yaml文件,内容为

# buf.yaml
version: v1
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT

Lint pb文件

$ buf lint
ecommerce/v1/product.proto:10:9:Service name "ServiceOrderManagement" should be suffixed with "Service".
ecommerce/v1/product.proto:11:18:RPC request type "getOrderReq" should be named "GetOrderRequest" or "ServiceOrderManagementGetOrderRequest".

调整lint规则

 # buf.yaml
 version: v1
 breaking:
   use:
     - FILE
 lint:
   use:
     - DEFAULT
+  except:
+    - PACKAGE_VERSION_SUFFIX
+    - FIELD_LOWER_SNAKE_CASE
+    - SERVICE_SUFFIX

生成代码

插件:和使用protoc一样,该装的插件一样要装

插件模版

创建一个buf.gen.yaml ,它是buf生成代码的配置。上面的protoc同等功能的buf.gen.yaml可以写成如下形式,相对protoc更加直观

# buf.gen.yaml
version: v1
plugins:
  - plugin: go
    out: ecommerce
    opt:
      - paths=source_relative
  - plugin: go-grpc
    out: ecommerce
    opt:
      - paths=source_relative
  - name: grpc-gateway
    out: ecommerce
    opt:
      - paths=source_relative
      - generate_unbound_methods=true
  - name: openapiv2
    out: doc
    opt:
      - logtostderr=true

生成代码

buf generate pb

buf generate 命令将会

  • 搜索每一个buf.yaml配置里的所有protobuf文件
  • 复制所有protobuf文件到内存
  • 编译所有protobuf文件
  • 执行模版文件里的每一个插件

添加依赖

在使用grpc-gateway时依赖了google.api.http,在不使用buf的场景,我们需要手动复制.proto到本地。

buf为我们提供了 Buf Schema Registry (BSR),除了可以使用其他人发布的模块,也可以把我们自己的模块发布到BSR

在模块的文件里声明依赖项

 # buf.yaml
 version: v1
 breaking:
   use:
     - FILE
 lint:
   use:
     - DEFAULT
deps:
- buf.build/googleapis/googleapis

然后执行

buf mod update

buf mod update 把你所有的 deps 更新到最新版。并且会生成 buf.lock 来固定版本

# Generated by buf. DO NOT EDIT.
version: v1
deps:
  - remote: buf.build
    owner: googleapis
    repository: googleapis
    commit: 75b4300737fb4efca0831636be94e517

此时执行buf generate pb 即使本地没有依赖,也不会再报错缺少依赖了

参考资料

[1]

Buf 官方文档: https://docs.buf.build/introduction

[2]

Protocol Buffers Documentation: https://protobuf.dev/



最后在结尾推荐一下我专栏--程序员的全能画图课课程有针对大型项目的技术评审案例分析和带练演示,无论是小型项目和大型项目都通过实际的案例分析讲解带你掌握下面这些技能


课程采用理解优于记忆的方式,用平时开发中常见的案例分析,教会大家对他们的使用。甚至还会手把手教你画系统架构图,业务架构图的心法要诀,让你能轻松应对各种研发工作中的非编码需求

现在可在公众号专栏《程序员的全能画图课》上直接订阅或者扫描上方海报二维码订阅,课程内容已经更新完成,不存在烂尾的风险,后面会不定时加更一些新的总结。



网管叨bi叨
分享软件开发和系统架构设计基础、Go 语言和Kubernetes。
 最新文章