ASP.NET Core 3.x 入门(八)gRPC - Protocol Buffer
此入门教程是记录下方参考资料视频的学习过程
开发工具:Visual Studio 2019
参考资料:https://www.bilibili.com/video/BV1c441167KQ
API文档:https://docs.microsoft.com/zh-cn/dotnet/api/?view=aspnetcore-3.1
目录
基本概念
为什么需要 gRPC ?
- 现如今微服务和流行,而微服务很有可能是使用不同的语言进行构建的。而微服务之间通常需要相互通信,所以微服务之间必须在以下几个方面达成共识
- 需要使用某种 API
- 数据格式
- 错误的模式
- 负载均衡
- 。。。
- 现在最流行的一种 API 风格可能是 REST ,它主要是通过 HTTP 协议来传输 JSON 数据
- 但是现在我们可以看看 gRPC
https://grpc.io/
,它来自 Google ,并且支持众多主流的语言,包括 Go、Dart、C#、C/C++、Nojejs、Python 等等
gRPC 能解决哪些问题?
- 构建(Web)API 是挺麻烦的,因为构建 API 时我们得考虑:
- 数据的格式是 JSON、XML 还是二进制的
- 端点地址以及 GET 还是 POST 等
- 如何调用 API 以及对异常的处理规则
- API 的效率:一次调用读取多少数据?是否太多了或太少了?太少的话可能会导致多次 API 的调用
- 延迟
- 扩展性,是否能支持成上千个客户端
- 负载均衡
- 与其它语言的互操作性
- 如何处理身份认证、监控、日志等等
再次介绍以下 gRPC
- gRPC 来自 Google ,它是一个开源的框架;它同时也是 Cloud Native Computation 基金会(CNCF)的一部分,就像 Docker 和 Kubernetes 一样
- gRPC 允许你为 RPC (Remote Procedure Call)定义请求和响应,然后 gRPC 会帮你处理一切剩余问题
- 它速度快,执行效率高,基于 HTTP/2 构建,低延迟,支持流,与开发语言无关,并且可以很简单的插入身份认证、负载均衡、日志和监控等功能
RPC 是啥?
- RPC 是*(Remote Procedure Call)远程过程调用
- 在客户端代码使用 RPC 调用的时候,就像直接调用了服务端的一个函数
例子
服务器端代码
public class Server
{
public int DoSomething()
{
return 1;
}
}
“遥远”的客户端调用
class Test
{
static void Main()
{
var server = new Server();
var response = server.DoSomething();
}
}
服务器端的 Server 类和客户端的 Server 类不是在同一个项目里的,通过某种方式,调用起来就像在本地代码调用一样
- RPC 它不是一个新的概念,很早它就出现了。但是它存在很多的问题。而 gRPC 是对 RPC 的一种非常简洁的实现,并且解决了很多 RPC 的问题
如何学习 gRPC ?
- 首先,你得学习 Protocol Buffers
https://developers.google.com/protocol-buffers/
,简单的说,它可以用来定义消息和服务 - 然后,你只需要实现服务即可,剩余的 gRPC 代码将会自动为你生成
- .prote 这个文件可以适用于十几种开发语言(包括服务端和客户端),并且它允许你使用同一个框架来支持每秒百万级以上的 RPC 调用
- gRPC 使用的是合约优先的 API 开发模式,它默认使用 Protocol buffers(protobuf)作为接口设计语言(IDL),这 .proto 文件包包括两部分:
- gRPC 服务的定义
- 服务端和客户端之间传递的消息
protobuf 例子
syntax = "proto3";
package Greet;
// The greeting service definition
Service Greeter {
// Sends a gtrrting
rpc SayHello(HelloRequest) returns (HelloReply);
}
// The request message containing the user's name
message HelloRequest {
string name = 1;
}
// The response messgae containing the greetings
message HelloReply {
string message = 1;
}
- 在这里定义了一个 Greeter 服务,它里面定义了一个 SayHello 的 rpc 调用。SayHello 会发送 HelloRequest 这个消息,接受 HelloReply 这个消息
为什么使用 Protocol Buffers
- 它和开发语言无关
- 可以生成所有主流开发语言的代码
- 数据是二进制格式的,串行化的效率高,Payload 比较小
- 也很适合传递大量的数据
- 通过设定某些规则,使得 API 的进化也很简单
Protocol Buffer
- 开发环境
- IDE:Visual Studio Code
- VSCode 的扩展插件:vscode-proto3 和 Clang-Format 这两个扩展
- Windows 还需要安装 Clang
- Mac:brew install clang-format
第一个例子
- 先选择一个文件夹,建立一个名叫 first.proto 文件:
syntax = "proto3";
message FirstMessage {
int32 id = 1;
string name = 2;
bool is_male = 3;
}
syntax = "proto3";
- 这行代码表是我们使用的语法是 proto3 ,之前还有一个 proto2;如果不写这一行,那么 protocol buffer 编译器会认为你采用的是 proto2 。这个必须是文件的第一个非空非注释行
message FirstMessage {}
- 这里定义了一个消息名称为 FirstMessage ,类型是 message。它里面定义了三个字段,它们都是标量类型(Scalar Type),你也可以定义复合类型,这个以后再说
int32
string
bool
- 是指字段(Field)的类型
id
name
is_male
- 字段的名称
1
2
3
- 字段的数值(也叫 Tag ),这个数字是唯一的。它们是用来在信息格式里识别你的字段的,一旦类型被使用了,那么这个数字就不要再改变了
- 因为字段名称可能很长,在传输的时候比较浪费资源,传输的数据更大一些,所以这就相当于起别名,这样传输的效率更高
标量类型
-
数值型
- 数值型有很多种形式:double、float、int32、int64、uint32、uint64、sint32、sint64、fixed32、fixed64、sfixed32、sfixed64。根据需要选择对应的数值类型
-
布尔型
- bool 型可以有 True 和 False 两个值
-
字符串
- string 表是任意长度的文本,但是它必须包含的是 UTF-8 编码或 7 位 ASCII 的文本,长度不可超过 2^32
-
字节型
- bytes 可表示任意的 byte 数组序列,但是长度也不可以超过 2^32 ,最后是由你来决定如何解释这些 bytes 。例如你可以使用这个类型来表示一个图片
做个例子
syntax = "proto3";
message Person {
int32 id = 1;
string name = 2;
float height = 3;
float weight = 4;
bytes avatar = 5;
string email = 6;
bool email_verified = 7;
}
-
可以自己做一个例子,需求是这样的:这个信息表示的是一个人 Person ,使用 proto3 语法
-
字段如下:
- ID
- 姓名
- 身高
- 体重
- 头像
- 电子邮件
- 邮件是否已验证
-
要注意一下别忘记标点符号
字段的数值(Tag)
- 在 Protocol Buffers 里面,字段的名其实没那么重要,但是写 C# 代码的时候,字段名还是很重要的
- 对于 protobuf 来说,这个 tag 是更为重要的
- 可以使用的最小的 tag 数值是 1 ,最大值是 2^29 - 1 ,或者 536,870,911 。但是你不可以使用 19000 到 19999 之间的数,这部分数是保留i的
- 还有一点值得注意的是:
- 从 1 到 15 的 Tag 数只占用 1 个字节的空间,所以它们应该被用在频繁使用的字段上。而从 16 到 2047 ,则占用两个字节,它们可以用在不频繁使用的字段上
字段规则 —— protobuf 的字段必须满足以下两个规则之一:
-
单数字段(Singular)
- 大概意思就是指这个字段只能出现 0 或 1 次(不能超过一次),这也是 proto3 的默认字段规则
-
重复字段(Repeated)
- 与 Singular 相对的就是 Repeated 。如果你想做一个 List 或 数组 的话,你可以使用重复字段这个概念。这个 List 可以有任何数量(包括 0 )的元素。它里面的值的顺序将会得到保留
Repeated Fields 例子
syntax = "proto3";
message Person {
int32 id = 1;
string name = 2;
float height = 3;
float weight = 4;
bytes avatar = 5;
string email = 6;
bool email_verified = 7;
repeated string phone_numbers = 8;
}
- 还是使用前面的 Person 这个例子,我们在里面添加一个 repeated 字段(电话号码)
- 就是在前面加上 repeated 这个关键字即可
- 在 proto3 里面,标量类型的 repeated 字段采用的是 packed 编码
注释
- proto 文件里可以添加注释。它们通常被当作你定义的这些消息的文档
- 注释很简单,还是两种形式
- //
- /* */
保留的字段
syntax = "proto3";
message Person {
int32 id = 1;
string name = 2;
float height = 3;
float weight = 4;
bytes avatar = 5;
string email = 6;
bool email_verified = 7;
repeated string phone_numbers = 8;
reserved 8, 9, 11 to 20;
reserved "foo", "bar";
}
- 如果你对你定义的消息类型进行了更新,例如删除某个字段或者注释掉某个字段,那么其它开发者在以后更新这个消息类型的时候可能会重新使用被你删除/注释掉的字段的数值(tag)。如果以后还需要使用这个消息类型的老版本的 proto 文件,那么这将会引起严重的问题,例如数据损坏、隐私漏洞等等
- 那么一种避免此类事情发生的解决办法就是将你删除/注释掉的这些字段的数值(或/并且包括字段名,因为字段名可引起 JSON 序列化的问题,要另起一行)标记为 reserved,如果其他人再使用这个数值作为字段标识符,那么编译器就会有错误提示:
- 注意不可以把 reserved 数值和字段名放在同一个 reserved 语句里
字段的默认值
- 当消息被解析的时候,如果编码的消息里不含有特定的一个 singular 元素,那么在被解析对象里相应的字段就会被设为默认值
- 常用类型的默认值如下:
- string:空字符串
- bytes:空的 byte 数组
- bool:false
- 数值型:0
- 枚举 enum :枚举里定义的第一个枚举值,值必须是 0
- repeated:通常是相应开发语言里的空 List
- 还有一个消息类型的字段,它的默认值和开发语言有关,这个以后再说
枚举
- 枚举里面定义的第一个值就是这个枚举的默认值
- Enum 的 tag 必须从 0 开始,所以 0 就是枚举的数值默认值
继续上个例子
syntax = "proto3";
message Person {
int32 id = 1;
string name = 2;
float height = 3;
float weight = 4;
bytes avatar = 5;
string email = 6;
bool email_verified = 7;
repeated string phone_numbers = 8;
reserved 8, 9, 11 to 20;
reserved "foo", "bar";
enum Gender {
NOT_SPECIFIED = 0;
FEMALE = 1;
MALE = 2;
}
Gender gender = 10;
}
- 我们对 Person 添加一个枚举类型的字段:性别 Gender
- 首先需要定义枚举类型,这里定义了一个枚举,名称是 Gender ,里面有 3 个值,默认值是 NOT_SPECIFIED ,数值默认值就是 0
- 然后使用这个枚举类型定义了一个字段,名称为 gender ,tag 数为 10
为枚举值起别名
enum Gender {
option allow_alias = true;
NOT_SPECIFIED = 0;
FEMALE = 1;
MALE = 2;
WOMAN = 1;
MAN = 2;
}
- 枚举值是可以起别名的,起别名的作用就是允许两个枚举值拥有同一个数值
- 要想起别名,首先需要设置 allow_alias 这个 option 为 true
- 然后我们为 FEMALE 这个枚举值起了一个别名叫做 WOMAN ,它们的数值是一样的。同样的,MAN 和 MALE 的数值也是一样的
注意
- 枚举里面的常量的值必须不能超过 32 位整型的数值,不建议使用负数
- 枚举可以定义在 message 里面,也可以在外边单独定义以便复用。如果另一个消息想使用 Person 里面这个 Gender 枚举,那么可以使用 Person.Gender 这种形式
enum Gender {
option allow_alias = true;
NOT_SPECIFIED = 0;
FEMALE = 1;
MALE = 2;
WOMAN = 1;
MAN = 2;
reserved 7, 8, 9 to 12, 20 to max;
reserved "BOY", "GIRL";
}
- 针对枚举值被删除/注释掉这种情况,它也可以使用 reserved
- 数值和常量名也必须分开使用两个 reserved 语句
- 其中 max 表示可能的最大的值
使用其它的信息类型
syntax = "proto3";
message Person {
int32 id = 1;
string name = 2;
float height = 3;
float weight = 4;
bytes avatar = 5;
string email = 6;
bool email_verified = 7;
repeated string phone_numbers = 8;
reserved 8, 9, 11 to 20;
reserved "foo", "bar";
enum Gender {
option allow_alias = true;
NOT_SPECIFIED = 0;
FEMALE = 1;
MALE = 2;
WOMAN = 1;
MAN = 2;
reserved 7, 8, 9 to 12, 20 to max;
reserved "BOY", "GIRL";
}
Gender gender = 10;
Date birthday = 22;
}
message Date {
int32 year = 1;
int32 month = 2;
int32 day = 3;
}
- 可以使用其它的信息类型作为字段的类型
- 我们可以在同一个 proto 文件里定义多个信息类型
- 在这个文件里,除了 Person 信息类型外,我还定义了 Date 信息类型
- 若类型不在同一个文件里,我们要在
syntax
底下使用import "文件名.proto";
,这样就能使用了
嵌套类型
Protoco Buffer 允许在信息类型里面定义其它的信息类型
- 如果想在 Person 外边使用 Address 这个类型,那么就需要这样使用:Person.Address
message Person {
int32 id = 1;
string name = 2;
float height = 3;
float weight = 4;
bytes avatar = 5;
string email = 6;
bool email_verified = 7;
repeated string phone_numbers = 8;
reserved 8, 9, 11 to 20;
reserved "foo", "bar";
enum Gender {
option allow_alias = true;
NOT_SPECIFIED = 0;
FEMALE = 1;
MALE = 2;
WOMAN = 1;
MAN = 2;
reserved 7, 8, 9 to 12, 20 to max;
reserved "BOY", "GIRL";
}
Gender gender = 10;
Date birthday = 22;
repeated Address addressed = 9;
message Address {
string province = 1;
string city = 2;
string zip_code = 3;
string street = 4;
string number = 5;
}
}
打包
你可以向 proto 文件添加可选的打包(package)说明符,以避免消息类型间的名称冲突
syntax = "proto3";
package my.project;
message Person {
}
- 打包之后生成的 C# 代码就会使用命名空间来对应 proto 里面的 package ,但是命名方式会改为 PascalCase(每个单词首字母大写)
- 上面的代码在 C# 里面的情况就是:Person 类在 My.Project 这个命名空间下
syntax = "proto3";
package my.project;
option csharp_namespace = "My.WebApis";
message Person {
}
- 但是如果你在 proto 文件里设置了
option csharp_namespace
这个选项,那么在 C# 里的命名空间就是该选项指定的命名空间了 - 这时候,C# 里面 Person 类的命名空间就是 My.WebApis 了,但是在 proto 文件里它的包还是 my.project
设置 Protocol Buffers 编译器
-
protoc 编译器主要就是用来生成代码的,它的下载地址目前是:
https://github.com/protocolbuffers/protobuf/releases/
-
在里面选择你使用的操作系统的版本
-
下载后解压缩到某个路径,然后把解压目录下的 bin 目录添加到系统的环境变量里
-
然后打开命令行,输入 protoc ,如果有类似下面的对象出现,说明安装成功了
-
这里的 --proto_path=PATH 这个参数比较常用,它用来指定到哪个文件夹来查找引入
在我们的 proto 文件所在的文件夹下新建一个 cs 文件夹
控制台打开 proto 文件所在文件夹,protoc --csharp_out=cs first.proto
注意,生成的文件一定不要去改它
全部生成 protoc --csharp_out=cs *.proto