1、RPC
1.1 什么是RPC
RPC(Remote Procedure Call),即远程过程调用,过程就是方法,简单来说,它就是一种能够像调用本地方法一样调用远程计算机进程中的方法的技术,在这种调用中,我们不需要了解任何网络通信的细节(当然,就使用来说)
最终解决的问题:让分布式或者微服务系统中不同服务之间的调用像本地调用一样简单
1.2 RPC和HTTP
调用远程服务,HTTP 就可以完成的任务,为什么还需要 RPC 呢?需要注意,这两个并不是同一层次的概念,HTTP 是一种传输协议,RPC 应该是比 HTTP 更高层级的概念。完整的 RPC 实现包含有 传输协议 和 序列化协议,其中,传输协议既可以使用 HTTP,也可以使用 TCP 等,不同的选择可以适应不同的场景
RPC 并不是一个崭新的概念,它实际上就是远程通信的一个更高层级的封装,不同传输协议和序列化协议的组合构成了不同的具体 RPC 实现,比如我们熟知的 RESTful,就是 HTTP + JSON + 一些其他细节构成
1.3 RPC技术的演化
早期有一些很流行的 RPC 实现,比如 CORBA(通用对象请求代理体系结构),Java RMI(远程方法调用),它们都用来构建和链接服务或应用程序,但是,大多数传统 RPC 实现极其复杂,因为它们构建在 TCP 之上,并且还有大量的规范限制
鉴于以上 RPC 实现的局限性,SOAP(简单对象访问协议)应运而生,SOAP 是 SOA(面向服务的架构)中的标准通信技术,能够基于任意底层通信协议进行通信,最常用的是 HTTP,序列化协议使用的是 XML
REST(描述性状态转移)是 ROA(面向资源的架构)的基础,在这种架构中,将应用程序建模为各种资源的集合,客户端可以变更这些资源的状态(增删改查)。REST 的通用实现是 HTTP + JSON,通过 HTTP 将应用程序建模为能够通过唯一标识符表示的资源集合,状态变更操作会采用 HTTP 动作(GET,POST,PUT,DELETE等)。实际上,REST 架构风格已经成为了各种服务间通信中非常流行的方法,但是,随着微服务大行其道以及网络交互的激增,REST 已经无法满足现代化的需求了,其主要原因是以下三个主要的局限性:
基于文本的消息协议效率太低。REST 服务建立在基于文本的传输协议 HTTP1.x 上,使用人类可读的文本格式如 JSON,但是,很多时候我们并不需要这种可读性,如果能够直接发送映射服务和客户端业务逻辑的二进制内容,将大大提高效率
缺乏强类型接口。开发 REST 服务时,应用程序之间并不需要共享服务定义和类型定义,我们要么通过网络查看文本格式,要么通过 API 文档,构建这种分散的应用程序时,会遇到很多不兼容、运行时错误和互操作等问题
REST架构风格难以实施。REST 架构风格有很多 “好的实践”,遵循这些实践能构建出真正好用的 REST 服务,但是,它们并没有作为协议的一部分进行强制要求,事实上,大多数 REST 服务不过是通过网络公开的 HTTP 服务,并没有很好地遵循基础的架构风格
由于 REST 的局限性,出现了许多新兴的 RPC 技术,较为流行的有 gRPC、Thrift、GraphQL等
2、gRPC
2.1 gRPC简介
gRPC 是一个现代化的开源 RPC 框架,一开始由 google 开发,是一款语言中立、平台中立、的 RPC 系统,与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个 服务,指定能够被远程调用的 方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个gRPC 服务器来处理客户端调用,在客户端拥有一个 stub 连接服务端上的方法
2.2 gRPC的优势
gRPC 的优势是它被越来越多人采用的关键所在,主要有以下几个方面:
提供高效的进程间通信。使用一个基于 protocol buffers 的二进制协议而不是文本格式与客户端通信,同时在 HTTP2 上实现,拥有更好的性能
具有简单且定义良好的服务接口。契约优先,必须首先定义服务接口,然后才能去处理细节,简单一致,可扩展
强类型。服务契约清晰地定义了应用程序间通信所使用的类型,分布式应用程序的开发更加稳定
支持多语言。基于 protocol buffers 的服务定义是语言中立的,可以选择任意一种语言具体实现
支持双工流。与传统的 REST 相比,gRPC 能够同时构建传统的请求-响应风格的消息以及客户端流和服务端流
具备内置的商业化特性。如认证、加密、弹性时间、元数据交换、压缩、负载均衡以及服务发现等
与云原生生态进行了集成。gRPC 是 CNCF(云原生计算基金会)的一部分,大多数现代框架和技术都对 gRPC 提供了原生支持
业界成熟。通过在谷歌进行的大量实战测试,gRPC 已经发展成熟,被许多公司采用
2.3 gRPC的缺点
gRPC 也存在一定劣势,选择它用来构建应用程序时,需要注意以下三点:
gRPC 不太适合面向外部的服务。gRPC 具有契约驱动、强类型等特点,这会限制向外部暴露服务的灵活性,对客户端有诸多限制,所以更适合用在内部服务器之间通信
避免巨大的服务定义变更。如果出现巨大的服务定义变更,通常需要重新生成客户端代码和服务端代码,会让整个开发生命周期变得复杂,需要小心引入破坏性的变更
生态系统相对较小。与传统 REST 等协议相比,gRPC 仍然处于起步阶段,浏览器和移动应用程序对 gRPC 的支持才刚刚起步
3、一个简单gRPC服务的golang实现
3.1 环境准备
- 下载 protoc 编译器:protobuf,选择合适的平台,解压后将可执行文件加入环境变量,此编译器用来编译服务定义文件 .proto 生成指定语言的目标代码,这些代码用来实现 gRPC 服务以及客户端 stub
- 安装 grpc-go 插件用来生成 go 目标代码
go install google.golang.org/protobuf/cmd/protoc-gen-go
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
- 创建代码目录 product-info,实现一个简单的查看商品信息和添加商品的 rpc 服务,在其中新建三个文件夹 proto、server、client 分别存放服务定义文件和生成的目标代码、服务端程序实现、客户端程序实现,然后执行
go mod init product-info
初始化模块。当然这里只是示例程序,实际场景中服务代码和客户端代码一般都不在同一个机器上,更不可能在同一个模块下了,最终目录结构如下:
3.2 服务定义
开发 gRPC 应用程序时,要首先定义服务接口,然后生成服务端骨架和客户端 stub,客户端通过调用其中定义的方法来访问远程服务器上的方法,服务定义都以 protocol buffers 的形式记录,也就是 gRPC 所使用的服务定义语言
- 在 proto 目录下新建服务定义文件 product-info.proto
// 版本
syntax = "proto3";
// proto文件所属包名
package proto;
// 声明生成的go文件所属的包,路径末尾为包名,相对路径是相对于编译生成目标代码时的工作路径
option go_package = "./proto";
// 包含两个远程方法的 rpc 服务,远程方法只能有一个参数和一个返回值
service ProductInfo {
rpc addProduct(Product) returns (ProductID);
rpc getProduct(ProductID) returns (Product);
}
// 自定义消息类型,用这种方法传递多个参数,必须使用唯一数字标识每个字段
message Product {
string id = 1;
string name = 2;
string description = 3;
float price = 4;
}
message ProductID {
string value = 1;
}
-
编译服务定义文件生成目标源代码,这一步之后在 proto 文件下生成了以下两个文件:
product-info.pb.go,包含用于填充、序列化、检索请求和响应消息类型的所有 protocol buffers 代码
product-info_grpc.pb.go,包含服务端需要继承实现和客户端进行调用的接口定义
# go_out 和 go-grpc-out 目录是相对于服务定义文件中 go_package 指定的目录
protoc proto/product-info.proto --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative
3.3 服务端实现
编译生成服务端骨架的时候,已经得到了建立 gRPC 连接、相关消息类型和接口的基础代码,接下来就是实现得到的接口,在 server 文件夹中新建服务端主程序 main.go:
package main
import (
"context"
"log"
"net"
pb "product-info/proto"
"github.com/gofrs/uuid"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
port = ":50051"
)
// 对服务器的抽象,用来实现服务方法
type server struct {
pb.UnimplementedProductInfoServer
}
// 存放商品,模拟业务逻辑
var productMap map[string]*pb.Product
// 实现 AddProduct 方法
func (s *server) AddProduct(ctx context.Context, in *pb.Product) (*pb.ProductID, error) {
out, err := uuid.NewV4()
if err != nil {
return nil, status.Errorf(codes.Internal, "Error while generating Product ID", err)
}
in.Id = out.String()
if productMap == nil {
productMap = make(map[string]*pb.Product)
}
productMap[in.Id] = in
log.Printf("Product %v : %v - Added.", in.Id, in.Name)
return &pb.ProductID{Value: in.Id}, nil
}
// 实现 GetProduct 方法
func (s *server) GetProduct(ctx context.Context, in *pb.ProductID) (*pb.Product, error) {
product, exists := productMap[in.Value]
if exists && product != nil {
log.Printf("Product %v : %v - Retrieved.", product.Id, product.Name)
return product, nil
}
return nil, status.Errorf(codes.NotFound, "Product does not exist.", in.Value)
}
func main() {
// 创建一个 tcp 监听器
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 创建一个 gRPC 服务器实例
s := grpc.NewServer()
// 将服务注册到 gRPC 服务器上
pb.RegisterProductInfoServer(s, &server{})
// 绑定 gRPC 服务器到指定 tcp
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
3.4 客户端实现
接下来创建客户端程序来与服务器对话,之前编译服务定义文件生成的目标源代码已经包含了访问细节的实现,我们只需要创建客户端实例就可以直接调用远程方法。在 client 文件夹中创建客户端主程序 main.go:
package main
import (
"context"
"log"
"time"
pb "product-info/proto"
"google.golang.org/grpc"
)
const (
// 服务端地址
address = "localhost:50051"
)
func main() {
// 创建 gRPC 连接
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// 创建客户端 stub,利用它调用远程方法
c := pb.NewProductInfoClient(conn)
name := "XiaoMi 11"
description := "XiaoMi 11 with MIUI 12.5"
price := float32(3999.00)
// 调用远程方法
r, err := c.AddProduct(context.Background(), &pb.Product{Name: name, Description: description, Price: price})
if err != nil {
log.Fatalf("Could not add product: %v", err)
}
log.Printf("Product ID: %s added successfully", r.Value)
product, err := c.GetProduct(ctx, &pb.ProductID{Value: r.Value})
if err != nil {
log.Fatalf("Could not get product: %v", err)
}
log.Printf("Product: %v", product.String())
}
3.5 构建和运行
最终工作空间如下:
分别构建运行服务端和客户端程序,go build 或者直接 go run
启动服务端:
go run ./server/main.go
启动客户端:
go run ./client/main.go
- 服务端 log:
到这里就成功构建了一个简单的 gRPC 服务,并在客户端调用成功。当然这只是一个简单的入门程序,更多的细节还需要更加深入的学习,另外,gRPC 是支持多语言的,这里采用 golang 实现了服务端和客户端程序,其他的语言构建 gRPC 服务也都遵循类似的步骤,且客户端和服务端代码无关,也可用不同的语言实现,其他语言的用法可见 官方文档