gRPC 简介
gRPC是一个高性能、开源的远程过程调用(RPC)框架。它允许客户端和服务器之间进行通信,支持多种编程语言,并且能够在多种环境中部署。
特点:
- 多语言支持:gRPC 支持多种编程语言,包括但不限于:C++, Java, Python, Go, Ruby, C#, Node.js, PHP, 和 Objective-C。这使得 gRPC 可以在跨语言的项目中使用。
- 基于HTTP/2:gRPC 使用 HTTP/2 作为其传输协议。这带来了许多优点,包括双向流、头部压缩、单个TCP连接中的多路复用请求等。
- 协议缓冲(Protocol Buffers):gRPC 使用 Protocol Buffers(protobufs)作为其接口定义语言(IDL)和默认的消息序列化格式。Protobuf 是一种高效的二进制序列化机制,能够显著减少消息的大小和序列化/反序列化的时间。
- 双向流:gRPC 支持四种主要的RPC模式:Unary RPC、Server Streaming RPC、Client Streaming RPC 和 Bidirectional Streaming RPC。这使得它能够处理各种复杂的通信模式。
- 负载均衡、跟踪、健康检查和认证:gRPC 支持许多现代微服务架构所需的特性,包括负载均衡、分布式跟踪、健康检查和多种认证机制。
gRPC 与 IDL
什么是 IDL?
接口定义语言(IDL, Interface Definition Language)是一种用于定义软件组件间接口的描述语言。它允许开发者在不同语言和平台间描述和传递数据结构和服务接口。
在 gRPC 中,接口定义语言(IDL)是用于定义服务和消息结构的关键部分。gRPC 使用 Google 的 Protocol Buffers(protobufs)作为其默认的 IDL 和消息序列化格式。gPRC 的 IDL 是与语言和平台无关的,通过一个编译的过程,生成各个语言的代码。
这里我们将详细解释一下 gRPC 和 IDL 的关系,以及如何使用 Protocol Buffers 来定义 gRPC 服务。
关系:gRPC 先定义了 IDL。而后 gRPC 需要一门编程语言作为 IDL 落地的形式,因此使用了 protobufs。
什么是Protocol Buffers?
Protocol Buffers 是一种由 Google 开发的语言中立、平台无关、可扩展的机制,用于序列化结构化数据。它包括以下几个部分:
- 消息类型定义:用于定义数据结构。
- 服务定义:用于定义远程过程调用(RPC)接口。
- 编译器(protoc):将 protobuf 定义编译成特定语言的代码。
优势:
- 高效的二进制格式
- 紧凑性:Protobuf 使用紧凑的二进制格式进行数据序列化,比 JSON 和 XML 的文本格式占用更少的空间。它可以显著减少数据传输所需的带宽。
- 速度:由于其二进制格式,Protobuf 的序列化和反序列化速度非常快,适合性能要求高的应用。
- 跨语言支持:Protobuf 支持多种编程语言,包括但不限于:C++, Java, Python, Go, Ruby, C#, JavaScript, PHP, 和 Objective-C。这使得 Protobuf 能够在跨语言的项目中使用,确保不同语言之间的数据交换无缝进行。
- 清晰的接口定义:使用
.proto
文件定义数据结构和服务接口,使得接口清晰明了。开发者可以通过读取.proto
文件快速了解接口和数据结构的定义。这种接口定义语言(IDL)不仅定义了数据结构,还定义了数据的序列化和反序列化方式。 - 向后兼容性和扩展性:Protobuf 提供了良好的向后兼容性。开发者可以在不破坏现有代码的情况下对数据结构进行扩展。新版本的消息可以包含额外的字段,而旧版本的消息仍然可以正确解析。通过为每个字段分配唯一的标签(tag),Protobuf 确保了字段的顺序和存在与否不会影响数据的解析。
- 强类型:Protobuf 是强类型的,这意味着在定义数据结构时需要指定每个字段的类型。这种强类型检查可以在编译时捕获许多潜在的错误,提高代码的可靠性。
- 自动代码生成:Protobuf 提供了编译器(protoc),它可以根据
.proto
文件自动生成多种语言的代码。这减少了手工编写序列化和反序列化代码的需求,提高了开发效率。自动生成的代码包括数据类、序列化和反序列化方法,以及对于 gRPC 服务的客户端和服务器代码。 - 支持嵌套和复合类型:Protobuf 支持嵌套消息和复合类型,使得它能够表达复杂的数据结构。开发者可以在一个消息中包含其他消息类型,从而构建复杂的数据模型。
- 灵活的序列化选项:Protobuf 提供了灵活的序列化选项,包括可选字段、重复字段(类似于数组或列表)和映射(键值对)。这些特性使得 Protobuf 可以处理各种复杂的数据结构和需求。
语法入门
Protobuf使用.proto
作为文件后缀,正常来说,在每一个文件中都要生命本文件的语法版本syntax
。一般情况下默认使用syntax=proto3
。
syntax = "proto3";
如果需要生成 Go 的代码,需要在文件里面加上相关的配置。go_package
选项用于指定生成的 Go 代码的包路径。
syntax = "proto3";
package helloworld;
// ";"前表示生成的 Go 代码的包路径,“;”后表示生成的Go代码的包名
option go_package = "github.com/yourusername/yourproject/helloworld;example";
为什么要使用 go_package
呢?
- 避免包名冲突:在大型项目或多个项目中,使用
go_package
可以确保生成的代码放置在特定的包路径下,避免包名冲突。 - 明确代码组织:通过指定包路径,可以使代码的组织更加明确和清晰,有助于维护和管理。
- 兼容性:在 Go 中,包路径通常与项目的文件结构和版本控制系统(如 GitHub)紧密相关。
go_package
选项可以确保生成的代码与 Go 的包路径规则兼容。
文件结构:
yourproject/
├── helloworld/
│ ├── helloworld.proto
│ ├── helloworld.pb.go
│ ├── helloworld_grpc.pb.go
├── main.go
message 定义
message
是一种用于定义数据结构的核心概念。它类似于其他编程语言中的类或结构体,用于描述要序列化和反序列化的数据。每个 message
包含一个或多个字段,每个字段都有一个唯一的编号(标签)和类型。
每个字段都有三要素:类型、名称和标签。标签是一个唯一的整数,用于标识字段。
示例:
syntax = "proto3";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
字段类型:
- int32, int64: 有符号整数
- uint32, uint64: 无符号整数
- sint32, sint64: 有符号整数,采用 ZigZag 编码
- bool: 布尔值
- string: 字符串
- bytes: 字节序列
- float, double: 浮点数
- map:map<keyType, ValueType>
- 数组:repeat
- enum:枚举类型
字段默认值:
- 数字类型(如 int32、float)的默认值是 0
- 字符串的默认值是空字符串 “”
- 布尔类型的默认值是 false
- 枚举类型的默认值是第一个定义的枚举值
service 定义
service
用于定义一组远程过程调用(RPC)方法,这些方法可以在客户端和服务器之间调用。每个 service
包含一个或多个 RPC 方法,每个方法都定义了请求和响应消息类型。
每一个 service
的请求和响应消息类型都是一个 message
。
示例:
// 定义请求消息
message HelloRequest {
string name = 1;
}
// 定义响应消息
message HelloReply {
string message = 1;
}
// 定义 Greeter 服务
service Greeter {
// 定义 SayHello 方法
rpc SayHello (HelloRequest) returns (HelloReply);
}
工具安装
安装 protobuf
在MacOS上,我直接使用 HomeBrew 进行安装。
$ brew install protobuf
安装完成之后,查看 protobuf 的版本。
$ protoc --version
$ libprotoc 25.1 #这是我的版本
接下来,讲解一下 protoc 常用的命令。
- 指定 proto 文件的路径。如果填写
.
表示当前目录下。
--proto_path=IMPORT_PATH, -I=IMPORT_PATH
示例:
protoc --proto_path=src --proto_path=include --go_out=outdir src/foo.proto
- 生成 go 代码存放路径。如果填写
.
表示当前目录下。
--go_out=OUT_DIR
示例:
protoc --go_out=. foo.proto
- 生成 gRPC 代码 (Go)。
--go-grpc_out=OUT_DIR
示例:
将 .proto
文件编译成 Go 的 gRPC 代码并输出到指定目录。通常与 --go_out
一起使用。
protoc --go_out=. --go-grpc_out=. foo.proto
- 代码生成插件
protoc --plugin=protoc-gen-custom=path/to/custom-plugin --custom_out=. foo.proto
参数说明:
- –plugin=protoc-gen-NAME=path/to/plugin:指定一个自定义插件,其中
NAME
是插件的名称,path/to/plugin 是插件的可执行文件路径。 - –NAME_out=OUT_DIR:指定由插件生成的输出代码目录,其中
NAME
是之前指定的插件名称。
示例:
假设你正在使用一个名为 protoc-gen-go
的自定义插件来生成 Go 代码。该插件位于 /usr/local/bin/protoc-gen-go
。
protoc --plugin=protoc-gen-go=/usr/local/bin/protoc-gen-go --go_out=. example.proto
将使用 protoc-gen-go
插件来生成 Go 代码,并将生成的代码输出到当前目录。
安装 go-gen 和 gRPC 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
以上两条命令安装:
- protoc-gen-go:生成 Go 代码的插件。
- protoc-gen-go-grpc:生成 gRPC 代码的插件。
记住,确保你的 GOPATH/bin
目录在系统的 PATH
中,这样你可以直接使用这些插件。
使用工具
- 创建
.proto
文件
syntax = "proto3";
package example;
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
- 使用 protoc --plugin 生成代码
# 生成 Go 代码
protoc --plugin=protoc-gen-go=$(which protoc-gen-go) --go_out=. example.proto
# 生成 gRPC 代码
protoc --plugin=protoc-gen-go-grpc=$(which protoc-gen-go-grpc) --go-grpc_out=. example.proto
在这个命令中:
--plugin=protoc-gen-go=$(which protoc-gen-go)
:指定使用 protoc-gen-go 插件来生成 Go 代码。--go_out=.
:指定生成的 Go 代码输出到当前目录。--plugin=protoc-gen-go-grpc=$(which protoc-gen-go-grpc)
:指定使用 protoc-gen-go-grpc 插件来生成 gRPC 代码。--go-grpc_out=.
:指定生成的 gRPC 代码输出到当前目录。
执行这些命令后,你将在当前目录中看到生成的 .pb.go
和 _grpc.pb.go
文件,这些文件包含了 example.proto
中定义的消息和服务的 Go 代码。
到这里,基本的使用已经介绍完毕了,接下来,将会使用 Buf
这个工具对 protobuf API 进行统一管理。
使用 Buf
我们直接使用 protoc 有如下几个难点:
- 插件难以管理。
- protoc 命令不好记,每次都要输入一大堆参数,烦人。
- 各种文件的 package 的定位简直 要命。
对于以上问题的考虑,我们可以直接使用 Buf 管理。
这是安装地址https://buf.build/docs/installation
。
首先,在项目的顶级目录下使用 buf config init
初始化项目。这时候,你会看到一个buf.yaml
的文件。
buf.yaml
文件主要用于定义项目的整体配置,包括版本控制、模块路径、托管模式、lint 规则和 breaking changes 验证规则等。
以下介绍一下 yaml
文件如何编写:
version: v1
# 定义配置文件的版本。
# 定义输入目录或文件。
# 你可以定义多个目录或文件,Buf 会递归地找到所有 .proto 文件。
# 例如,你可以指定一个单独的目录或多个目录。
build:
roots:
- proto
- third_party
# 定义生成代码的配置。
# 你可以指定多个生成器,每个生成器对应一个输出目录。
# 例如,你可以生成 Go 代码和 gRPC 代码。
# 生成器可以是内置的,也可以是自定义的。
# 例如,你可以使用 protoc-gen-go 和 protoc-gen-go-grpc 插件生成 Go 代码和 gRPC 代码。
generate:
plugins:
- name: go
out: gen/go
- name: go-grpc
out: gen/go
# 定义验证规则。
# 你可以指定多个规则,每个规则对应一个验证器。
# 例如,你可以使用 lint 和 breaking 验证器验证你的 Protobuf 文件。
lint:
use:
- DEFAULT
breaking:
use:
- FILE
详细示例:
文件的目录结构:
my_project/
├── buf.yaml
├── proto/
│ └── example.proto
└── third_party/
└── google/
└── api/
└── annotations.proto
yaml
文件编写:
version: v1beta1
build:
roots:
- proto
- third_party
generate:
plugins:
- name: go
out: gen/go
- name: go-grpc
out: gen/go
lint:
use:
- DEFAULT
breaking:
use:
- FILE
构建项目
buf build
buf.gen.yaml
此文件主要用于定义代码生成的配置,包括使用哪些插件来生成代码、代码生成的输出目录和生成选项等。这些配置项专注于实际的代码生成过程。
新建buf.gen.yaml
文件,并编写相关的配置:
version: v2
managed:
enabled: true
override:
- file_option: go_package_prefix
value: github.com/username/project
plugins:
# 默认使用最新版本
# remote 插件的远程地址
- remote: buf.build/protocolbuffers/go
# protoc-gen-go 插件
out: grpcpro/grpc/proto/gen
opt: paths=source_relative
- remote: buf.build/grpc/go
# proto-gen-go-grpc 插件
out: grpcpro/grpc/proto/gen
opt: paths=source_relative
inputs:
# 定义输入目录。Buf 将处理该目录下的所有 .proto 文件
- directory: grpc
使用命令 buf generate
。
生成对应的go语言的代码。 注意:在与yaml
同级的grpc
文件夹下存着我们的proto文件。
下面是对应的项目文件结构:
my_project/
├── buf.yaml
├── grpc/
│ └── example.proto
└── grpcpro/
└── grpc/
└── proto/
└── gen/
区别
- 用途不同:
buf.yaml
用于定义项目的整体配置和验证规则,而buf.gen.yaml
则专门用于定义代码生成的配置。 - 配置项不同:
buf.yaml
包含版本、模块路径、托管模式、lint 规则和 breaking changes 验证规则等配置项;而buf.gen.yaml
则包含插件配置、输出目录和生成选项等。 - 文件作用范围不同:
buf.yaml
文件的作用范围较广,影响整个项目的配置和验证;buf.gen.yaml
文件则专注于代码生成。
总结
以上就是关于 gRPC 的简单使用,以及相关的工具集合的使用等。文章整体篇幅较大,并且操作难度偏上,希望广大读者耐心看完,并着手实践。