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编码,解决数值表示过程中的冗余问题。