前言
系统分布式已经成为程序员的家常,将大型单体划分为相对简单的小模块,分散系统能力,提升系统扩展性、功能模块复用性等;各功能模块之间肯定会有很多数据共享和交互的应用场景,那就避免不了各模块之间的通信;目前用的比较多的方式是HTTP(Restful API)接口、消息队列等,而HTTP(Restful API)接口应该是目前应用比较广泛的,相对之前的webservice和WCF都显得比较轻量级,而且实用;
随着微服务的盛行,对服务间的通信要求也越来越高,比如传输方式、传输速率、传输内容大小等,而HTTP(Restful API)方式有较重的头信息、无状态、不能复用连接等缺点,所以优化是早晚的事;那么gRPC的出现就显得情有可原啦(gRPC不局限于服务间通信,只要符合Server/Client场景就可以),所以接下来咱们一起来探究学习一下。
正文
1. 认识一下gRPC
gRPC 由 google 开发,最前面的g就代表google。进入gRPC官网,一句话描述了gRPC的主要特色,先上张图:
A high performance, open source universal RPC framework
翻译:一个高性能、开源的通用RPC框架
- 高性能:gRPC遵循HTTP/2协议,解决并优化了HTTP1.1的一些缺陷;默认使用谷歌开源的 protocol buffers(类似于XML、JSON的数据序列化结构协议),传输速率、解析速度都很快、压缩率高,性能整体都比XML和JSON好(后续专门写个程序来比较比较);
- 开源:源码地址请进传送门;
- 通用:各种流行语言(C++、C#、Java、Go、Python等)都能用,轻松实现跨语言通信;本身不限于任何平台。
- RPC:远程过程调用(Remote Procedure Call),通俗一点理解,就是分布式中各服务之间调用的一种技术。
总而言之,gRPC是一个现代的开源高性能远程过程调用(RPC)框架,可以在任何环境中运行。它可以有效地连接数据中心内和跨数据中心的服务,并支持可插拔的负载均衡、跟踪、健康检查和身份认证(后续会一一举例演示)。
算啦,估计到这有些小伙伴还是有点懵圈(语言组织能力还有待提高),那就先将其理解为一个类似于WebAPI的调用框架,只是性能更高,使用更简单,就像调用本地方法一样;它使客户端和服务器应用程序能够透明地通信,隐藏了远程调用的细节,大概过程如下:
上图简析:各语言之间可以互相调用,只要客户端按照约定(Protocol Buffer)传递对应的请求参数,调用服务端对应的方法,最后就会返回约定好(Protocol Buffer)的响应数据。
不说那么多啦,直接开干吧,一边撸码一边说理论。
2. 初体验 gRPC
2.1 从0开始写服务端
-
创建一个空的Web项目(基于.NetCore3.1),引入包Grpc.AspNetCore
-
编写proto文件(重点),因为gRPC是使用protocol Buffer作为接口定义语言,内容包含以下两部分:
传递的消息:请求和响应时的数据信息,类似于现在用的DTO类。
gRPC服务的定义:定义gRPC服务方法,可以理解为现在写的Restful 接口。
这里模拟用户维护的场景,包含增、删、改、查方法,这里新建protos目录专门用来存放proto文件,user.proto内容如下:
// 使用的是proto3版本 syntax = "proto3"; // 定义命名空间,后续生成代码时就会生成对应的命名空间 option csharp_namespace = "gRPC.Demo.Server.protos"; /* 每一句需要用分号结尾 message 用来定义请求和返回数据格式 tag message后面的值数字代表是字段的标识(tag),不是赋值, */ // 新增用户时需要传递数据消息, 可理解为一个类 message AddUserReuqest{ string name=1; int32 age=2; bool isBoy=3; } // 新增时返回的消息格式 message ResultResponse { int32 code=1; string msg =2; } //传递的查询条件信息格式,可理解为平时传入的查询条件对象 message QueryUserReuqest{ string name=1; } //查询返回的用户信息格式,可理解为返回的类 message UserInfoResponse { string name=1; int32 age=2; string gender=3; } // service 用标识定义服务的,里面写对应的方法 service UserService{ // 新增用户 rpc AddUser(AddUserReuqest) returns (ResultResponse); // 查询用户 rpc GetAllUser(QueryUserReuqest) returns (UserInfoResponse); }
proto文件,各种编程语言是通用的,都可以根据定义好的proto文件生成对应编程语言的代码,重写业务即可。对于自动生成代码,其他编程语言需要借助protoc 工具(点这里去下载),而.Net微软已经封装好了,只要引入了Grpc相关的包,直接编译即可。先简单设置一下proto文件属性,大概步骤如下:
注:如果不想按照上面步骤设置文件属性,也可以直接编辑项目文件也行,如下增加红框部分:
最后编译程序就可以自动生成对应的服务端代码了,如下路径:
-
重写生成的方法,增加业务逻辑
proto文件生成的代码只是一个约定,不包含业务逻辑,关于具体的业务需要自己处理,如下:
-
在Startup.cs文件中开启gRPC的功能,如下:
到这服务端就完成了,现在就去写一个客户端连接一下;
2.2 搞一个客户端访问服务
-
创建一个控制台程序
引入Google.Protobuf、Grpc.Net.Client、Grpc.Tools三个包,然后将服务端写好的proto文件拷贝到项目中,按照上面说的步骤设置一下proto文件属性为Client only,编译即可,项目结构如下:
如果不想通过设置属性的方式,也可以直接修改项目文件,然后编译就可自动生成代码:
-
现在可以写业务啦
新增用户调用远程服务逻辑:
查询用户调用远程服务逻辑:
从上面代码来看,使用是不是比较简单,只需要知道远程地址即可,远程调用就像调用本地方法一样,也不用判断状态码,不用自己去解析内容,而是按照约定直接返回结果。
-
先运行服务端,再运行客户端,效果如下:
调试时注意:
由于gRPC使用SSL/TLS保护服务,在调试的时候尽管指定为https连接也会报错,这是因为系统没有信任微软开发证书,执行以下命令即可:
dotnet dev-certs https --trust
然后就可以正常调试了;当然在生产环境中,还是需要配置真实证书的,如果不想用证书也是可以滴,这个后续单独会说到。
2.3 使用.NetCore模板快速生成服务端
.NetCore已经将gRPC服务端封装成一个模板了,可以通过模板快速创建一个gRPC项目,如下:
创建出来的服务端项目基本和刚刚创建的一样,编译运行就能启动;小伙伴后续可以直接用模板创建,在里面添加对应的业务代码即可。
本来打算把gRPC的四种模式接着演示的,但考虑到proto文件的编写是一个重点,所以总结了一些常用的写法,先熟悉熟悉proto文件的编写,后续再通过案例演示加深印象就更好啦。
3. Protocol Buffer 是重点
通过上面示例演示,proto文件用于约定服务接口,各编程语言用其可以生成对应的代码,然后进行业务逻辑的编写,可见proto文件扮演了很重要的角色。在性能方便是以二进制格式进行解析,内容小,传输效率高,适合传递大量数据的场景。
proto文件中每一个message代表了一类结构化的数据,message 里面定义了每一个属性的类型和名字,并指定一个Tag(每个属性后面的数字)。在gRPC传输过程中是通过Tag这个数字进行标识属性的,不是用属性名。
3.1 数据类型
由于protocol Buffer 不限于编程语言,所以在编写proto文件时,指定的类型和不同编程语言的类型是一一对应的,这样在根据proto文件生成对应代码时,约束力就比较强,大概的类型对照表如下(只整理了比较常用的语言,详细请进官网):
每一种类型在没有指定值时都对应有默认值:
- string: 空字符串
- bytes:空byte数组
- bool:false
- 数值型:0
- 枚举enum: 默认第一个枚举值,第一个值必须是0
默认值其实基本上和对应的编程语言默认值差不多一样。
3.2 常用编写方式
-
一般形式
通过message定义一类结构化数据,如下:
使用message定义一类数据时,其实就可以理解为在编写一个类,里面的字段就是类里面的属性。需要注意的是message中指定字段的后面数值不是字段值,而是字段名称对应的一个标识(Tag)。
-
嵌套自定义类型
经常会遇到这种嵌套自定义类型的场景,比如一个班级有多个学生,班级类型就需要嵌套学生,如下:
这里用到repeated来表示一个班级有多个学生,可以理解为List.
-
使用枚举
枚举在编程过程中肯定少不了,这里把上面的性别改为枚举,如下:
枚举类型需要注意的是第一项映射的常量值必须为0,方便设置其作为默认项,同时也是为了兼容proto2。
如果想要一个数字对应多个枚举项,可以开启允许别名的方式进行设置,如下:
-
文件引入
在实际开发场景中,定义的数据类型很多,放在一个文件肯定不太合适,可读性差,所以都会将其放在多个proto文件,然后通过引用多个文件即可;接下来把学生单独出来作为一个proto文件,如下:
注:在创建proto文件时,按照2.1那个步骤设置文件属性,否则不会自动生成类,如果有文件相互引用,还会编译报错。
-
引用第三方proto
在平时开发的时候,有些机构、社区或是大佬将常用的类进行封装,然后打包好, 我们只需要下载引用就可以愉快的使用啦;同样,proto也可以这样,如下在Student中加一个入学时间,如下:
其他类型进传送门,需要哪个,就照着上面方式使用即可。
3.3 参数字段变更注意
需求变动对于开发来说是家常便饭,针对接口增加字段或修改字段都是常有的事,如果打算在原有方法修改字段,那就需要注意啦; 在通过protocol Buffer进行序列化时,是通过类似于字典的形式约定好的,其中字段后面的数字Tag至关重要,用其进行属性字段标识,学生举例如下:
// 在解析时,都是用后面的数字tag进行约定解析的
message Student{
string name=1; // 1 就代表 姓名
int32 age=2; // 2 就代表 年龄
Gender gender=3; // 3 就代表 性别
string addr=4; // 4 就代表 地址
google.protobuf.Timestamp enrollmentDate=5; //5 就代表 入学时间
}
只要已经上线互相调用,如果有参数字段变动,最好是通过新增的方式,因为这样会避免双方调用因为没有同时更改参数字段造成对业务的影响。比如上面学生字段要变动,需求是:新增一个分数字段,删除年龄字段,通常会有以下方式:
-
方式1:将年龄字段直接改成分数字段即可
如果服务端将proto文件改成这样,客户端还没来的及改,就会造成如下情况:
2这个tag标识在服务端代表分数,在传递数据的时候就将分数赋值给该字段;但对于客户端来说,还是认为2这个tag标识为年龄,接收的时候还是以年龄字段进行处理,最终就会影响原有解析逻辑,并影响到业务。
-
方式2:删除年龄字段并将其对应tag标识设置为保留字段,然后新增一个分数字段
通过reserved将原有参数字段和对应的tag标识设为保留值,这样不允许用作其他业务,就会避免业务逻辑处理异常的情况,最多是就接受方没有收到值,按默认值处理即可。
关于字段的更新注意先暂时说这么多,类型之间互相兼容的细节小伙伴去官网好好看看。 这里记住一个重点:tag 标识不能随便进行重用,不然proto文件没有及时同步就有可能导致业务逻辑处理有问题。
总结
.NetCore对于gRPC已经是封装得比较方便啦,引入对应的包,编译自动根据.proto文件生成对应代码,然后直接写业务即可;下一篇说说关于gRPC四种模式及gRPC服务的认证、授权。
一个被程序搞丑的帅小伙,关注"Code综艺圈",和我一起学~~~