Introduction to RPC
Contents
Reference document:
什么是RPC?
RPC,全称为Remote Procedure Call,即远程过程调用,是一种计算机程序的通信协议,常见于分布式计算、后端服务集群部署等场景。遵循C/S模式,经典实现是一个通过发送请求<->接受回应进行信息交互的系统。随着微服务架构在业界的大规模应用,RPC逐渐被大众所熟知(有关RPC的想法至少可以追溯到1976年)。
常见的RPC协议:
- gRPC,由Google推出,基于HTTP2协议开发,默认采用Protocol Buffers数据序列化协议,支持C#、C++、Dart、GoLang、Java、Kotlin、Node、Objective-C、PHP、Python、Ruby等一系列语言。
- Thrift,由Facebook推出,后捐献给Apache基金会,支持C#、C++、Delphi、ErLang、Haxe、GoLang、Java、Lua、Node、Perl、PHP、Python、Ruby等一些列语言。
- bRPC,由Baidu推出,后捐献给Apache基金会,目前仅支持C++。
- Dubbo,由Alibaba推出,后捐献给Apache基金会,目前仅支持Java。严格意义来讲Dubbo不属于RPC协议,而是一款RPC框架。
- 自定义协议,部分大的公司会选择自行开发一套协议来使用,随着上述协议的不断优化和完善,自定义协议出现的也越来越少。
为什么要使用RPC?
随着互联网的普及,大规模的应用越来越多,大企业对于开发效率和软件质量的需求不断的上升,从而衍生出了越来越多的微服务需求。微服务,简而言之就是将一个大的应用拆分为多个独立的小应用。服务的拆分使得多个需求能进行并行开发、测试,减小了各个需求之前的耦合,从而去提升开发效率和软件的质量。
在RPC之前,我们一般采用HTTP(S)协议进行远端调用,但是由于HTTP(S)为无状态协议,不支持长连接(HTTP1.1版本开始支持长连接),每次通信都要进行连接握手等操作,并且需要携带臃肿的Header参数,导致消耗了很多的时间和带宽。RPC协议一般基于TCP/UDP协议,支持长连接,使用特定的压缩编码格式对数据进行传输,从而极大的提升了通信效率和稳定性。
随着微服务架构的广泛使用,远端调用的需求也越来越多,针对HTTP(S)传输协议的消耗也越来越不能让从业人员接受,随之而来的就是对RPC的广泛使用了。
什么时候应该使用RPC?
目前浏览器端尚不支持RPC请求,所以RPC暂时只能应用于服务端。
对于初创公司来说,由于开发人员数量不会过多,不会存在太多并行需求,而是着重于快速迭代,所以一般不会在项目初期就开始使用微服务架构。相比传统web应用架构而言,微服务架构需要大量的基础设置,如注册中心、配置中心、日志链路追踪、服务监控等。
对于中大型公司,开发人员的数量已经充足,此时的需求一般是复杂并且并行的。对于这种场景,传统web应用架构无论是针对开发还是测试人员来说,都不能很好的进行日常工作,所以此阶段将要开始拆分应用,建立微服务架构的一些基础设施。随着服务数量的增多,服务间调用也越来越频繁,此时便会引入RPC协议,以优化服务间调用的效率和稳定性。
注:对于中大型公司的初创项目来说,因为已经存在很好的基础设施,且开发人员也熟悉了微服务架构开发的流程,所以一般在项目初期便会考虑使用微服务架构。
使用哪种RPC协议?
目前存在多种RPC协议,且大多都支持主流的语言,所以针对协议的选择便会造成一定的困扰。一般来讲主要由以下因素决定:
- 开发语言,变动量越少越好,好的语言生态有较多的组件支持
- 基础设施,RPC协议并不是孤立存在的,需要大量基础设施支持,保持一致很重要
- 接入服务,考虑接入方所使用的协议,不然还要单独提供一套接入转发环境
- 协议效率,常见于Thrift和其它几类协议之间的对比,考虑因素占比较低
基于以上四个因素,考虑语言生态以及不同语言使用者的习惯,所以特定语言选取特定RPC协议的概率较大。如果项目组成员全是Java开发者,则使用Dubbo框架是个不错的选择(使用框架默认支持的协议);如果项目组成员全是GoLang开发者,则大概率会选用gRPC协议。
gRPC的使用简介
以下内容主要参考gRPC Quick Start,需使用Go 1.11及以上版本。
安装对应的环境依赖
-
安装go语言支持,访问GoLang官网下载对应系统安装包安装即可。注意部分系统需手工将
go/bin
路径添加到环境变量中 -
安装gRPC支持,使用
Go Module
时非必需1
$ go get -u -v google.golang.org/grpc
-
安装protoc编译器以生成gRPC服务代码,访问Protobuf Releases页面下载对应
protoc-<version>-<platform>.zip
,解压,将对应路径添加到环境变量中 -
安装protoc的go语言支持插件
1 2
$ go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest $ go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
编写第一个gRPC应用
- 创建项目
1 2
$ mkdir grpc-demo $ go mod init grpc-demo
- 创建protobuf文件
grpc-demo/pb/first-grpc.proto
,定义gRPC消息传递的必需信息,具体可参考protocol buffers1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
syntax = "proto3"; // 声明proto版本 package first_grpc; // 包名,无需显式声明 // 定义一个gRPC服务 service FirstGRPC { // 定义gRPC服务支持调用的方法名、参数、响应信息 rpc TestGRPC (RequestParam) returns (ReplyBody) {} } // 定义一个消息的结构,这里为请求数据 message RequestParam { string name = 1; string value = 2; } // 定义一个消息的结构,这里为响应数据 message ReplyBody { int32 code = 1; string message = 2; }
- 使用
protoc
工具生成上述定义的gRPC服务代码这里将生成1
$ protoc -I pb/ pb/first-grpc.proto --go_out=plugins=grpc:pb
grpc-demo/pb/first-grpc.pb.go
文件 - 编写server端代码
grpc-demo/server/main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
package main import ( "context" "fmt" "google.golang.org/grpc" firstGRPC "grpc-demo/pb" "net" ) type FirstGRPCServer struct { firstGRPC.UnimplementedFirstGRPCServer } func (firstGRPCServer *FirstGRPCServer) TestGRPC(ctx context.Context, requestParam *firstGRPC.RequestParam) ( *firstGRPC.ReplyBody, error) { fmt.Println("requestParam:", requestParam.String()) return &firstGRPC.ReplyBody{Code: 0, Message: "first gRPC call success"}, nil } func main() { listenAddress, err := net.Listen("tcp", "127.0.0.1:80") if err != nil { fmt.Println("failed to listen:", err) return } fmt.Println("listen success") gRPCServer := grpc.NewServer() firstGRPC.RegisterFirstGRPCServer(gRPCServer, &FirstGRPCServer{}) if err := gRPCServer.Serve(listenAddress); err != nil { fmt.Println("failed to server:", err) return } }
- 编写client端代码
grpc-demo/client/main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
package main import ( "context" "fmt" "google.golang.org/grpc" firstGRPC "grpc-demo/pb" "os" "time" ) const ( serverAddress = "127.0.0.1:80" ) func main() { conn, err := grpc.Dial(serverAddress, grpc.WithInsecure(), grpc.WithBlock()) if err != nil { fmt.Println("connect serverAddress failed error:", err) return } defer conn.Close() gRPCClient := firstGRPC.NewFirstGRPCClient(conn) paramName := "defaultName" paramValue := "defaultValue" if len(os.Args) > 2 { paramName = os.Args[1] paramValue = os.Args[2] } ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() gRPCResponse, err := gRPCClient.TestGRPC(ctx, &firstGRPC.RequestParam{Name: paramName, Value: paramValue}) if err != nil { fmt.Println("call TestGRPC failed error:", err) return } fmt.Println("call TestGRPC success res:", gRPCResponse.GetMessage()) }
- 测试
1 2 3 4 5 6 7 8 9 10 11
# 编译server端代码,生成可执行文件 $ go build -o server/server server/main.go # 编译client端代码,生成可执行文件 $ go build -o client/client client/main.go # 启用server端代码 $ ./server/server # 启用client端代码 $ ./client/client
Author Linfeng
LastMod 2023-08-31