前言
现代的软件服务大多数是分布式应用程序,通过暴露自己的 API 对内或对外提供了一系列的功能点。服务与服务之间有时是跨语言、跨平台通信的。
为了解决这些复杂场景,市面上也涌现了有很多解决方案。比如构建 RESTful 服务,将服务能力转化为资源集合;也有面向函数调用的客户端-服务器模式:远程过程调用(Remote Procedure Calls)。今天要介绍的 gRPC 就是后者的演变,一个非常受欢迎分布式进程间通信技术。
认识 gRPC
gRPC 是 Google 在 2015 年推出的 RPC 框架。在认识 gRPC 之前,我们先来了解下 RPC 的相关知识。RPC 主要运用于分布式程序中,它构建了客户端-服务器模型,类似于请求-响应通信方式,只不过这种请求被我们抽象为了函数方法 + 入参信息,底层的网络通信则被屏蔽了起来,到最后就像本地方法调用一样。
Protocol Buffers
上面提到了函数的入参信息,有入参就有对象类型,而端到端的对象类型就势必涉及到
网络通信过程中的字节序列化和反序列化过程。在 gRPC 中,采用了 Protobuf(Protocol Buffers)作为序列化和反序列化协议。它是一种轻便高效的结构化数据存储格式,基于二进制编码构建,能够减少 CPU 的复杂解析,保障了 RPC 调用的高性能。
另外 Protobuf 支持多种编程语言,我们只需要对其进行接口定义描述,便可以根据描述文件自动生成客户端和服务端需要使用到的结构体和方法函数,就像是代码自动生成一样,大大提高了我们的编程效率。
HTTP/2
gRPC 是基于 HTTP/2 设计的,HTTP/2 也是 2015 年发布的,它是下一代的 HTTP 协议,具备很多高级功能,如:
- 基于二进制格式传输,传输速度更快,更紧凑,不易出错。
- 多路复用,单个连接可发送多个请求。
- 对报头压缩,能降低传输开销。
- 允许服务器主动推送。
正是这些 HTTP/2 的特性,使得 gRPC 能够使用较少的资源,获得较快的响应,在移动端设备上更加省电省流量。
gRPC 的使用
接口定义
当我们开发一个 gRPC 应用程序时,要做的第一件事情就是定义一个接口描述文件。在这个文件里,我们会将函数、参数信息都描述出来,就像下面这个 ProductInfo.proto 文件一样:
// ProductInfo.proto
syntax = "proto3";
package ecommerce;
// 服务里的接口列表
service ProductInfo {
rpc addProduct(Product) returns (ProductID);
rpc getProduct(ProductID) returns (Product);
}
// 参数信息
message Product {
string id = 1;
string name = 2;
string description = 3;
}
message ProductID {
string value = 1;
}
gRPC 服务端
当我们拿到定义好的接口描述文件 ProductInfo.proto 后,就可以使用 Protobuf 编译器:protoc 来生成我们的服务端代码了。假设我们的服务端采用的是 Go 语言,则在经过一系列插件的安装后,我们就可以使用下面的命令来编译生成代码了:
protoc --proto_path=IMPORT_PATH --go_out=OUT_DIR --go_opt=paths=source_relative path/to/file.proto
protoc 支持多种语言,具体大伙可以按照官方提供的来生成代码,总之,我们将接口定义文件,转化为了我们需要的服务端代码,我们只需要引用并实现接口函数即可为客户端提供对应的服务了:
import (
...
"context"
pb "github.com/grpc-up-and-running/samples/ch02/productinfo/go/proto"
"google.golang.org/grpc"
...
)
// ProductInfo 接口的实现
// Add product remote method
func (s *server) AddProduct(ctx context.Context, in *pb.Product) (
*pb.ProductID, error) {
// 具体的业务逻辑
}
// Get product remote method
func (s *server) GetProduct(ctx context.Context, in *pb.ProductID) (
*pb.Product, error) {
// 具体的业务逻辑
}
当然,定义完后还得注册下服务:
func main() {
lis, _ := net.Listen("tcp", port)
s := grpc.NewServer()
pb.RegisterProductInfoServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
gRPC 客户端
与服务端类似,我们将根据 ProductInfo.proto 文件生成客户端的代码,即所谓的存根(Stub)。假设我们的客户端使用的是 Java 语言,则我们可以像下面一样使用 Stub 了:
// 根据服务地址创建通道
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080)
.usePlaintext(true)
.build();
// 根据通道创建存根
ProductInfoGrpc.ProductInfoBlockingStub stub =
ProductInfoGrpc.newBlockingStub(channel);
// 调用存根里的方法
StringValue productID = stub.addProduct(
Product.newBuilder()
.setName("Apple iPhone 11")
.setDescription("Meet Apple iPhone 11." +
"All-new dual-camera system with " +
"Ultra Wide and Night mode.")
.build());
整体流程
可以看到,其实接口定义文件 ProductInfo.proto 就是一个粘合剂,它将服务端的逻辑抽象成了语言无关的描述文件,让服务端依次去实现具体逻辑。然后通过它生成的客户端存根(Stub)则屏蔽了底层的通信流程,只需要暴露让上层可以调用的函数即可,就像本地函数调用一样。
其他特性
Metadata(元数据)
gRPC 没有使用 HTTP 所谓的 header 字段,而是使用 Metadata 来构建相关的头部信息。Metadata 是在一次 RPC 调用过程中关于这次调用的信息,以 key-value 的形式存在。
其中 key 是 string 类型, value 也是一组 string。 Metadata 对于 gRPC 本身来说透明,它使得 client 和 server 能为对方提供本次调用的信息。就像一次 http 请求的 RequestHeader 和 ResponseHeader,http header 的生命周期是一次 http 请求, Metadata 的生命周期则是一次 RPC 调用。
Streaming(流)
得意于 HTTP/2 的多路复用能力,使得 gRPC 的客户端和服务端能够进行流式传输,例如我们可以边传输,边处理数据。 gRPC 的流式传输主要分为了下面几种:
- 服务端流式 RPC:客户端发送单个请求,服务器可以发回多个响应。
- 客户端流式 RPC:客户端发送多个请求,而服务器只发回一个响应。
- 双向流式 RPC: 客户端和服务器同时相互发送消息而不等待响应。
Interceptors(拦截器)
gRPC 支持在请求/响应中使用拦截功能,进行消息的拦截并修改它们,这跟平常我们提到的 HTTP 中间件非常的相似。基于拦截器,我们可以实现类似身份认证、链式调用、重试等功能,这些对应微服务是非常的契合的。
负载均衡
gRPC 官方提供了关于负载均衡的方案:Load Balancing in gRPC,有兴趣的同学可以自己研究下。
gRPC 优点
正是前面的 Protobuf 和 HTTP/2 的底层支持,使得 gRPC 在一推出后就受到了许多人的追捧,它主要有以下几个特点:
- 性能:比 REST + JSON 快 10 倍的性能,消息负载小而紧凑,并且通过压缩加快了传输速度。
- 代码生成:通过语言无关的接口定义文件快速的生成各种语言的客户端、服务端代码,提高了开发效率。
- 可拓展性好:提高了一体化的 RPC 解决方案,有许多内置的解决方案,例如数据交换、加密、身份验证、超时取消、拦截器、负载均衡等。
gRPC 的缺点
与其他技术一样,gRPC 有优点也有缺点,下面就是我们在开发应用程序时需要注意的点:
- 有限的浏览器支持:由于 gRPC 大量使用 HTTP/2,因此无法之间在 Web 浏览器调用 gRPC 服务,gRPC 比较适用于手机 APP Client。
- 不友好格式:Protobuf 将 gRPC 消息压缩成非可读格式,需要反序列化才拿到消息格式,不好调试。
总结
现代软件应用程序已经很少孤立存在了,更多是通过网络通信进行服务沟通。gRPC 是一种可拓展、松耦合且类型安全的解决方案.
与传统的基于 REST/HTTP 的通信相比,它能进行更有效的进程间通信,特别是现在流行的微服务架构里,在很多框架里都能看到它的身影存在。所以,是时候开始试试它的魅力所在了!