Rpc 基本概念
RPC(Remote Procedure Call)远程过程调用是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议,简单的理解是一个节点请求另一个节点提供的服务。RPC 只是一套协议,基于这套协议规范来实现的框架都可以称为 RPC 框架,比较典型的有 Dubbo、Thrift 和 gRPC。
RPC 是远程过程调用的方式之一,涉及调用方和被调用方两个进程的交互。因为 RPC 提供类似于本地方法调用的形式,所以对于调用方来说,调用 RPC 方法和调用本地方法并没有明显区别。
一个完整的 RPC 框架包含了服务注册发现、负载、容错、序列化、协议编码和网络传输等组件。不同的 RPC 框架包含的组件可能会有所不同,但是一定都包含 RPC 协议相关的组件,RPC 协议包括序列化、协议编解码器和网络传输栈,如下图所示:
RPC 协议一般分为公有协议和私有协议。例如,HTTP、SMPP、WebService 等都是公有协议。如果是某个公司或者组织内部自定义、自己使用的,没有被国际标准化组织接纳和认可的协议,往往划为私有协议,例如 Thrift 协议和蚂蚁金服的 Bolt 协议。
RPC 和 HTTP 区别
RPC 和 HTTP 都是微服务间通信较为常用的方案之一,其实 RPC 和 HTTP 并不完全是同一个层次的概念,它们之间还是有所区别的。
- RPC 是远程过程调用,其调用协议通常包括序列化协议和传输协议。序列化协议有基于纯文本的 XML 和 JSON、二进制编码的 Protobuf 和 Hessian。传输协议是指其底层网络传输所使用的协议,比如 TCP、HTTP。
- 可以看出 HTTP 是 RPC 的传输协议的一个可选方案,比如说 gRPC 的网络传输协议就是 HTTP。HTTP 既可以和 RPC 一样作为服务间通信的解决方案,也可以作为 RPC 中通信层的传输协议(此时与之对比的是 TCP 协议)。
Go 语言原生有 RPC 包,RPC 过程调用实现起来非常简单。服务端只需实现对外提供的远程过程方法和结构体,然后将其注册到 RPC 服务中,客户端就可以通过其服务名称和方法名称进行 RPC 方法调用。
gRPC 特点
在 gRPC 的客户端应用可以想调用本地对象一样直接调用另一台不同的机器上的服务端的应用的对象或者方法,这样在创建分布式应用的时候更容易。
- 语言无关,支持多种语言;
- 基于 IDL 文件定义服务,gRPC 使用 protocol buffer 作为接口定义语言(IDL)来描述服务接口和有效负载消息的结构。通过 proto3 工具生成指定语言的数据结构、服务端接口以及客户端 Stub。
- 通信协议基于标准的 HTTP/2 设计,支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量;
- 序列化支持 PB(Protocol Buffer)和 JSON,PB 是一种语言无关的高性能序列化框架,基于 HTTP/2 + PB, 保障了 RPC 调用的高性能。
gRPC 使用和上面 RPC 使用方法类似,首先定义服务,指定其能够被远程调用的方法,包括参数和返回类型,这里使用 protobuf 来定义服务。在服务端实现定义的服务接口,并运行一个 gRPC 服务器来处理客户端调用。
gRPC 代码结构
- 先使用 protobuf 定义服务。
syntax = "proto3" ;
//package myproto ;
option go_package = ".;protoes";
//定义服务
service HelloServer {
rpc SayHello (HelloReq) returns (HelloRsp){}
rpc SayName (NameReq) returns (NameRsp){}
}
//客户端发送给服务端
message HelloReq {
string name = 1 ;
}
//服务端返回给客户端
message HelloRsp {
string msg = 1 ;
}
//客户端发送给服务端
message NameReq {
string name = 1 ;
}
//服务端返回给客户端
message NameRsp {
string msg = 1 ;
}
定义了两个服务 SayHello,SayName 及对应的四个消息(message)。然后在执行命令生成 pd.go 文件
protoc --go_out=plugins=grpc:./ *.proto #添加grpc插件
- 编写服务端 server.go
package main
import (
"fmt"
"net"
"google.golang.org/grpc"
pd "demo/myproto" //导入proto
"context"
)
type server struct {}
func (this *server) SayHello(ctx context.Context, in *pd.HelloReq) (out *pd.HelloRsp,err error) {
return &pd.HelloRsp{Msg:"hello"}, nil
}
func (this *server) SayName(ctx context.Context, in *pd.NameReq) (out *pd.NameRsp,err error){
return &pd.NameRsp{Msg:in.Name + "it is name"}, nil
}
func main() {
ln, err := net.Listen("tcp", ":10088")
if err != nil {
fmt.Println("network error", err)
}
//创建grpc服务
srv := grpc.NewServer()
//注册服务
pd.RegisterHelloServerServer(srv, &server{})
err = srv.Serve(ln)
if err != nil {
fmt.Println("Serve error", err)
}
}
- 编写客户端 client.go
package main
import (
"fmt"
"google.golang.org/grpc"
pd "demo/myproto" //导入proto
"context"
)
func main() {
//客户端连接服务端
conn, err := grpc.Dial("127.0.0.1:10088", grpc.WithInsecure())
if err != nil {
fmt.Println("network error", err)
}
//网络延迟关闭
defer conn.Close()
//获得grpc句柄
c := pd.NewHelloServerClient(conn)
//通过句柄进行调用服务端函数SayHello
re1, err := c.SayHello(context.Background(),&pd.HelloReq{Name:"zhangsan"})
if err != nil {
fmt.Println("calling SayHello() error", err)
}
fmt.Println(re1.Msg)
//通过句柄进行调用服务端函数SayName
re2, err := c.SayName(context.Background(),&pd.NameReq{Name:"zhangsan"})
if err != nil {
fmt.Println("calling SayName() error", err)
}
fmt.Println(re2.Msg)
}
执行代码:
go run server.go
go run client.go
gRPC 四种通信方式
gRPC 允许你定义四类服务方法:
- 简单 RPC(Simple RPC):即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。
rpc SayHello(HelloRequest) returns (HelloResponse){
}
- 服务端流式 RPC(Server-side streaming RPC):一个请求对象,服务端可以传回多个结果对象。即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
- 客户端流式 RPC(Client-side streaming RPC):客户端传入多个请求对象,服务端返回一个响应结果。即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
- 双向流式 RPC(Bidirectional streaming RPC):结合客户端流式 rpc 和服务端流式 rpc,可以传入多个对象,返回多个响应对象。即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}
protobuf 简介
protocol buffers 是 google 推出的一种数据序列化格式,简称 protobuf。
优点:
- 支持多种编程语言
- 序列化数据体积小
- 反序列化速度快
- 序列化和反序列化代码自动生成
缺点:
- 可读性差,缺乏自描述性
下图(左 JSON,右 Protobuf)是同样的一段数据,用 json 和 protobuf 分别描述(仅表示描述方式,并不是最终生成的序列化数据)。可以看出,protobuf 是把 json 中的 key 去掉了,用数字代替 key,从而实现了减小了序列化后的数据大小。而 protobuf 反序列化过程中,无需做字符串解析,所以速度也很快,综合性能优于 json 很多。
protobuf 使用,主要分为以下步骤:
-
定义消息格式,编写.proto 文件
-
选择合适的 protobuf 实现框架,对.proto 文件进行编译,生成对应的源代码文件
-
在代码中调用生成的源代码文件,完成序列化和反序列化功能
消息格式:
protobuf 数据是连续的 key-value 组成,每个 key-value 对表示一个字段,value 可以是基础类型,也可以是一个子消息。
其中,key 表示了该字段数据类型,字段 id 信息,而 value 则是该字段的原始数据。若字段类型是 string/bytes/ 子 message(长度不固定),则在 key 和 value 之间还有一个值表示该字段的数据长度,如下图所示:
key 值的计算方式为:key=(id<<3)|type,其中 id 是在消息定义时的字段 id,而 type 表示数据类型,取值范围 0-5,如下表所示:
例:假如 proto 文件定义如下,其中消息取值为 code=10,msg=“abc”,尝试计算序列化后的消息数据。
message Response{
required int32 code = 1;
required string msg = 2;
}
a. 消息的最终结构为 key1-value1-key2-length2-value2;
b. 其中,key1=(id<<3)|type = (1<<3)|0=0x08,value1=0x0a,key2=(id<<3)|type=(2<<3)|2=0x12,length2=0x03,value2=0x616263;
c. 因此,最终的消息数据为:0x080a1203616263.
数据压缩:
protobuf 引入了 varint 编码和 zigzag 编码,解决数值表示过程中的冗余问题。