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
),其中字段包含如下元素
类型:类型不仅可以是标量类型(
int
、string
等),也可以是复合类型(enum
等),也可以是其他message
字段名:字段名比较推荐的是使用下划线/分隔名称
字段编号:一个message内每一个字段编号都必须唯一的,在编码后其实传递的是这个编号而不是字段名
字段规则:消息字段可以是以下字段之一
singular
:格式正确的消息可以有零个或一个字段(但不能超过一个)。使用 proto3 语法时,如果未为给定字段指定其他字段规则,则这是默认字段规则optional
:与singular
相同,不过您可以检查该值是否明确设置repeated
:在格式正确的消息中,此字段类型可以重复零次或多次。系统会保留重复值的顺序map
:这是一个成对的键值对字段保留字段:为了避免再次使用到已移除的字段可以设定保留字段。如果任何未来用户尝试使用这些字段标识符,协议缓冲区编译器就会报错
标量值类
标量类型会涉及到不同语言和编码方式,后续有机会深入讲
.proto Type | Go Type | Notes |
---|---|---|
double | float64 | |
float | float32 | |
int32 | int32 | 使用可变长度的编码。对负数的编码效率低下 - 如果您的字段可能包含负值,请改用 sint32。 |
int64 | int64 | 使用可变长度的编码。对负数的编码效率低下 - 如果字段可能有负值,请改用 sint64。 |
uint32 | uint32 | 使用可变长度的编码。 |
uint64 | uint64 | 使用可变长度的编码。 |
sint32 | int32 | 使用可变长度的编码。有符号整数值。与常规 int32 相比,这些函数可以更高效地对负数进行编码。 |
sint64 | int64 | 使用可变长度的编码。有符号整数值。与常规 int64 相比,这些函数可以更高效地对负数进行编码。 |
fixed32 | uint32 | 始终为 4 个字节。如果值通常大于 2^28,则比 uint32 更高效。 |
fixed64 | uint64 | 始终为 8 个字节。如果值通常大于 2^56,则比 uint64 更高效。 |
sfixed32 | int32 | 始终为 4 个字节。 |
sfixed64 | int64 | 始终为 8 个字节。 |
bool | bool | |
string | string | 字符串必须始终包含 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
orapt-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/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.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
即使本地没有依赖,也不会再报错缺少依赖了
参考资料
Buf 官方文档: https://docs.buf.build/introduction
[2]Protocol Buffers Documentation: https://protobuf.dev/
最后在结尾推荐一下我专栏--程序员的全能画图课,课程有针对大型项目的技术评审案例分析和带练演示,无论是小型项目和大型项目都通过实际的案例分析讲解带你掌握下面这些技能。
课程采用理解优于记忆的方式,用平时开发中常见的案例分析,教会大家对他们的使用。甚至还会手把手教你画系统架构图,业务架构图的心法要诀,让你能轻松应对各种研发工作中的非编码需求。
现在可在公众号专栏《程序员的全能画图课》上直接订阅或者扫描上方海报二维码订阅,课程内容已经更新完成,不存在烂尾的风险,后面会不定时加更一些新的总结。