gRPC是Google基于HTTP/2和protobuf推出的一款也是当下热门的开源RPC(Remote Procedure Call)框架。可在程序或者服务之间进行高性能低带宽的通信,并且支持身份认证、日志系统等等需要用到的功能。在微服务作为主流的时代,各个服务之间的通信也是一个亟需解决的问题。在ASP.NET Core 3.x下,gRPC也是微软传统RPC框架WCF的有效替代。
使用gRPC,可以让客户端像调用本地方法一样地去调用服务端中的方法。gRPC是一种合约优先的API开发模式,就是我们需要先具体地定义好方法和参数后,再进行服务端功能开发和客户端调用。并且客户端和服务端可以是使用不同语言开发的程序,通过gRPC,一旦我们在自己的服务中定义了proto文件,任何其他gRPC支持的语言开发的程序都可以来调用这个通信,通信中涉及到的环境、序列化等gRPC都帮我们完成了。默认情况下,gRPC是使用Protocol Buffers作为其接口定义语言(Interface Definition Language),就是用来定义通信中要用的方法和参数。Protocol Buffers不依赖特定的语言,根据不同需求,编译器就可以将其转换生成C#、Java、Python、Go等十几种语言供我们开发使用,并且在通信中数据是序列化成二进制流的,从而获得更好的传输性能。
本文接下来简单介绍Protocol Buffers和gRPC在.NET Core中的基本用法,主要参考为官方文档和各位大佬的教程(文末有链接)。本文Demo已上传至☞GitHub。
那么先来简单介绍一下Protocol Buffers的语法。
○ 文件名后缀用 ".proto"
○ 别忘记在文首加上一句“syntax = "proto3",来指明使用的是proto3的语法(因为之前还有一个proto2)
○ 通过在proto文件中定义message类型来指明你想序列化传输的对象,可以类比成一个类其中包含你需要的多个字段。比如定义一个叫Person的message,其中包含3个字段。
1 message Person { 2 string name = 1; 3 int32 id = 2; 4 bool has_ponycopter = 3; 5 }
○ 简单介绍下Protocol Buffer中常用的数据类型
§ 数值:double, float, int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64
§ 字符串:string
§ 布尔:bool
§ 字节:bytes ,最大长度232
§ 枚举:enum
□ 定义枚举的方法,另外支持使用别名,别名是用不同的名称表示同一个枚举值,如下面的EnumAllowingAlias.STARTED和EnumAllowingAlias.RUNNING表示的是同一个枚举值。
1 enum EnumAllowingAlias { 2 option allow_alias = true; //表示可以使用别名 3 UNKNOWN = 0; //枚举的编号是从0开始的 4 STARTED = 1; 5 RUNNING = 1; 6 }
○ 定义message时,注意到每个字段都加了一个“唯一标识”的编号,这些编号用来在message转成二进制形式后标识具体字段是啥,在投入使用后尽可能避免修改字段的编号。编号范围是1~229-1其中编号1~15的字段使用一个字节来编码,编号16~2047则使用两个字节。所以将使用频率高的字段用1~15来编号,另外19000~19999是Protocol Buffers的保留字段,最好别用。
○ 字段的规则分为两种,singular(proto3中默认)和repeated,定义字段时二选一
§ singular
大概是表示为单值,这个字段的值最多一个,与repeated相对
§ repeated
大概像是集合类型,比如定义一个字段如“repeated string emails”大概意思可理解为List<string> emails
○ 注释的写法和C#文件中注释的写法基本一致,用“//”或者“/* … */”
○ 保留字段。如果一个已投入使用的proto中移除了某些字段的话,而用户仍然使用这些字段编号就会造成一些较严重的错误。解决办法是将这些想移除的字段名或者字段编号使用reserved关键字修饰。使用了reserved标注的字段在未来使用时,protocol buffer编译器将会抛出错误。
1 message Foo { 2 reserved 2, 15, 9 to 11; 3 reserved "foo", "bar"; 4 }
○ 当使用protocol buffer编译器将proto文件编译成C#语言,会自动生成.cs文件,其中为每个message编译成class。
○ 编译后的类型与C#中类型的对应关系
| proto类型 | double | float | int32 | int64 | string | bool | bytes | enum |
| C#类型 | double | float | int | long | string | bool | ByteString | enum |
| 默认值 | 0 | 0 | 0 | 0 | string.Empty | false | 空字节数组 | 枚举中的第一个值 |
○ 定义完message后,可以将其打包供其他service或者message引用,那么在protocol buffer中打包和引用的方法也很简单
§ 打包语法:
package foo.bar;
message Open { ... }
§ 指定生成自定义的C#命名空间的语法:
option csharp_namespace = "Foo.MyBar";
§ 引用其他proto文件中定义的message类型的语法:
import "myproject/other_protos.proto";
○ 有了message的定义,要在RPC中应用的话就需要定义“方法”,在protocol buffer中即是service。在.proto文件中定义service的语法是:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
○ 其中service和rpc都是关键字,Search是“方法”名,SearchRequest与SearchResponse都是定义的message类型。
○ 定义好service后,proto编译器就会将service使用我们选择的语言编译成的服务接口的代码。
○ Protocol buffers的一些其他关键字如any,oneof等暂时没用上,就先不列出了,可参考官方文档。
gRPC Demo实践
IDE使用的VS2019,首先使用ASP.NET Core建立一个gRPC的服务端,使用一个WPF程序作为客户端实现最基本的gRPC通信Demo,以一个员工信息的增查为例来演示gRPC中的常用场景。
gRPC 通常有四种模式的通信,分别是“一元(unary)”,“客户端流(client streaming)”,“服务端流(server streaming)” 以及“双向流模式( bidirectional streaming)”,对于 HTTP 2 来说其实都是用流的模式,以下实验是参考杨旭大佬的教程。
首先创建gRPC服务端,新建一个空白的ASP.NET Core Web应用程序命名为gRPC.Server。用NuGet安装“Grpc.AspNetCore”。新建一个Protos文件夹来存放.proto文件,新建一个名为Message.proto的文件来定义通信过程中需要的message。
1 syntax = "proto3"; //给编译器指明语法为proto3 2 3 //员工 4 message Employee{ 5 int32 Id = 1; //Id 6 string Name = 2; //姓名 7 int32 EmployeeNo = 3; //工号 8 Gender Gender = 4; //性别 9 Date BirthDay = 5; //生日 10 string Department = 6; //部门 11 bool IsValid = 7; //有效性 12 bytes Photo = 8; //照片 13 } 14 //性别(枚举) 15 enum Gender{ 16 NOT_SPESIFICED = 0; 17 FEMALE = 1; 18 MALE = 2; 19 } 20 //日期 21 message Date{ 22 int32 Year = 1; 23 int32 Month = 2; 24 int32 Day = 3; 25 } 26 27 //Service 用的参数: 28 //根据Id查询员工信息 29 message GetEmployeeByIdRequest{ 30 int32 Id = 1; 31 } 32 //上传的员工信息请求 33 message EmployeeRequest{ 34 Employee Employee = 1; 35 } 36 //返回员工信息 37 message EmployeeResponse{ 38 Employee Employee = 1; 39 } 40 //根据条件查询员工 41 message GetEmployeeCollectionRequest{ 42 string SearchTerm = 1; 43 bool IsValid = 2; 44 } 45 //返回员工信息集合 46 message GetEmployeeCollectionReponse{ 47 Employee Employee = 1; 48 } 49 //上传员工照片 50 message AddPhotoRequest{ 51 bytes Photo = 1; 52 } 53 //上传员工照片响应 54 message AddPhotoReponse{ 55 bool IsOK = 1; 56 }