go语言的gRPC教程-protobuf基础
一、前言
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/
2.1、定义消息类型
protobuf里最基本的类型就是message,每一个messgae都会有一个或者多个字段(field),其中字段包含如下元素

- 
类型:类型不仅可以是标量类型(
int、string等),也可以是复合类型(enum等),也可以是其他message - 
字段名:字段名比较推荐的是使用下划线/分隔名称
 - 
字段编号:一个messgae内每一个字段编号都必须唯一的,在编码后其实传递的是这个编号而不是字段名
 - 
字段规则:消息字段可以是以下字段之一
singular:格式正确的消息可以有零个或一个字段(但不能超过一个)。使用 proto3 语法时,如果未为给定字段指定其他字段规则,则这是默认字段规则optional:与singular相同,不过您可以检查该值是否明确设置repeated:在格式正确的消息中,此字段类型可以重复零次或多次。系统会保留重复值的顺序map:这是一个成对的键值对字段
 - 
保留字段:为了避免再次使用到已移除的字段可以设定保留字段。如果任何未来用户尝试使用这些字段标识符,编译器就会报错
 
2.2、标量值类
标量类型会涉及到不同语言和编码方式:
2.3、复合类型
(1)数组
message SearchResponse { repeated Result results = 1;}message Result { string url = 1; string title = 2; repeated string snippets = 3;}
(2)枚举
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;}
(3)服务
定义的method仅能有一个入参和出参数。如果需要传递多个参数需要定义成message
service SearchService { rpc Search(SearchRequest) returns (SearchResponse);}
2.4、使用其他消息类型
使用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文件编译成不同的语言
3.1、安装
https://github.com/google/protobuf/releases
 
- 
Windows 下载压缩包解压,并添加解压路径中的 bin 文件夹路径到环境变量Path中,新开终端
protoc --version验证安装。 - 
Linux, using
aptorapt-get, for example: 
$ apt install -y protobuf-compiler$ protoc --version # Ensure compiler version is 3+
- MacOS, using Homebrew:
 
$ brew install protobuf$ protoc --version # Ensure compiler version is 3+
3.2、使用
$ protoc --helpUsage: protoc [OPTION] PROTO_FILES -IPATH, --proto_path=PATH #指定搜索路径 --plugin=EXECUTABLE: # 指定要使用的插件可执行文件。通常,protocol会在PATH中搜索插件 .... --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本身已经内置该语言对应的编译插件,我们无需安装
下面的语言是由google维护,通过protoc的插件机制来实现,所以仓库单独维护
- Dart
 - Go
 
(3) proto文件位置参数
proto文件位置参数即上述的@参数,指定了我们proto文件的具体位置,如proto1/greeter/greeter.proto。
3.3、 语言插件
(1) 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=. --go_opt=paths=source_relative foo.proto bar/baz.proto
注意
protoc-gen-go要求pb文件必须指定go包的路径,即
option go_package = \".;streaming\";
----proto_path
这个选项用于指定 protoc 编译器在查找 .proto 文件时应该搜索的根目录。当你在 .proto 文件中使用 import 语句导入其他 .proto 文件时,编译器需要知道去哪里找到这些被导入的文件
–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
(2) 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的产出物,包含- 定义在 
RouteGuideservice中的用来给client调用的接口定义 - 定义在 
RouteGuideservice中的用来给服务端实现的接口定义 
 - 定义在 
 
–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/protobuf是github.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主要的两个参数为plugins 和 paths,分别表示生成Go代码所使用的插件,以及生成的Go代码的位置。
plugins参数有不带grpc和带grpc两种,两者的区别如下,带grpc的会多一些跟gRPC相关的代码,实现gRPC通信
paths参数有两个选项,分别是 import 和 source_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.yamlversion: v1breaking: use: - FILElint: use: - DEFAULT
Lint pb文件
$ buf lintecommerce/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.yamlversion: v1plugins: - 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: v1deps: - remote: buf.build owner: googleapis repository: googleapis commit: 75b4300737fb4efca0831636be94e517
此时执行buf generate pb 即使本地没有依赖,也不会再报错缺少依赖了
参考
- Buf 官方文档
 - Protocol Buffers Documentation
 - https://segmentfault.com/a/1190000043353574
 


