RPC

概念

RPC(Remote Procedure Call)远程过程调用。是一种通过网络向远程计算机请求服务信息,但又不需要了解底层网络技术的通信方式。就是像调用本地服务一样调用远程服务。

RPC采用客户端/服务端的模式,通过request-reponse消息模式实现。

实现步骤

gwt的rpc调用重试机制 grpc重连机制_客户端

RPC的主要实现步骤如下(加粗部分为rpc框架要实现的步骤):

  • 客户端发起本地连接请求连接服务端的服务;
  • 客户端程序句柄(stub)负责将客户端请求的消息序列化成二进制消息;
  • 客户端通过网络模块将消息发送至服务端;
  • 服务端程序句柄(stub)将收到的消息进行反序列化;
  • 服务端程序句柄(stub)根据解析后的方法调用本地服务;
  • 服务端上的服务处理完成后将消息发送至程序句柄(stub);
  • 服务端程序句柄(stub)将消息序列化成二进制消息;
  • 服务端通过网络模块将消息发送至客户端;
  • 客户端程序句柄(stub)将消息反序列化;
  • 客户端程序句柄(stub)根据解析后的方法调用本地服务;

实现原理

gwt的rpc调用重试机制 grpc重连机制_gwt的rpc调用重试机制_02

RPC 架构主要包括三部分:

  • 服务注册中心(Registry),负责将本地服务发布成远程服务,管理远程服务,提供给服务消费者使用,主要用于实现负载均衡和故障切换。
  • RPC Server,服务提供者,提供服务接口定义与服务实现类,服务提供者启动后主动向服务注册中心(Registry)注册机器IP、端口以及提供的服务列表;
  • RPC Client,服务消费者,通过远程代理对象调用远程服务,启动时向服务注册中心(Registry)获取服务提供方地址列表。

优缺点

优点

  • 对于使用者来说,简单高效,无需关心底层逻辑,仅调用接口即可;
  • 主流的rpc框架(如Dubbo、grpc、Hessian等)跨语言支持;

缺点

  • 开发难度大;
  • 异常处理困难

Stub

Stub是一段用于转换参数的代码。

为什么使用存根(Stub)?

rpc调用中,服务端提供方法和接口实现,客户端调用方法。但往往客户端和服务端位于不同的服务器上,主要有以下差异:

  • 编码不同。例如一端是GBK,另一端是UTF-8;
  • 大小端不同;
  • 编译语言不同。例如一段是c++,另一端是java;
  • 内存布局不同;
  • 尾款不同。有32/64位之分;

存根(Stub)选取一种中间形式,让两端的数据都可以正确可逆的与中间数据进行转换。常用的中间数据类型包括:纯字符串、XML、JSON、protobuf。

HTTP协议

从RPC的实现中可以看出,RPC的通信需要底层网络协议的支持,HTTP是一种比较好的实现方式。

概念

HTTP(HyperText Transfer Protocol)超文本传输协议,是一个双向协议,可以用来传输文字、图片、音视频等数据。

HTTP协议在3.0版本之前是基于TCP的,3.0版本底层改成了UDP。

gwt的rpc调用重试机制 grpc重连机制_rpc_03

HTTP/1.0

优点

  • 简单,报文的格式为header+body;
  • 灵活、易于扩展;
  • 应用广泛、跨平台;

缺点

  • 无状态;
  • 明文传输;
  • 不安全(后续HTTPS做了相关方面的修复);

HTTP/1.1

优点

  • 支持tcp长连接,改善了HTTP/1.0短连接造成的开销;
  • 支持管道(pipeline)网络传输;

缺点

  • 请求 / 响应头部(Header)未压缩,首部信息越多延迟越大;
  • 无请求优先级控制;
  • 服务器按请求顺序响应,会造成队头阻塞;
  • 请求只能从客户端开始,服务端被动响应;

HTTP/2.0

优点

  • 头部压缩;
  • 二进制格式。头信息和数据体都是二进制,统称为帧:头信息帧和数据帧;
  • 数据流;
  • 多路复用。一个连接中并发处理多个请求和响应;
  • 服务器推送。服务器可以主动向客户端发送消息

缺点

  • 多个请求复用一个TCP连接,一旦发生丢包,就会阻塞住所有的 HTTP 请求;

HTTP/3.0

优点

  • 底层协议改为UDP。基于UDP的QUIC协议可以实现类似TCP的可靠性传输,QUIC是一个在UDP之上的伪TCP+TLS+HTTP/2的多路复用协议;

GRPC-WEB

gRPC-Web是一个JavaScript客户端库,使Web应用程序能够直接与后端gRPC服务通信,而不需要HTTP服务器充当中介。

gwt的rpc调用重试机制 grpc重连机制_服务端_04

关于详细的grpc-web可以查看:

ProtoBuf

概念

protobuf(protocol buffer)一种用于序列化结构数据的工具,用于数据的存储和交换。是开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因为protobuf是二进制数据格式,需要编码和解码。

序列化和反序列化

在前面介绍rpc实现步骤时讲到,客户端或服务端的程序句柄(stub)在接收自身服务的消息后,会将消息进行序列化,在接收网络模块发送的消息后,会将数据进行反序列化。

  • 序列化:将结构数据或者对象转换成能够用于存储和传输的格式;
  • 反序列化:将序列化后的数据还原为结构数据和对象;
为什么要进行序列化和反序列化?
  • 减少存储空间;
  • 方便网络传输;
  • 可以在进程间传递对象;

特点

  • 使用二进制数据交换格式;
  • 使用扩展名为.proto的文件定义存储类的内容;
  • 使用自己的编译器protoc;

优点

  • 数据压缩性高;
  • 传输速度快;
  • 序列化速度快;
  • 简单、可扩展性高、加密性好;
  • 跨平台、跨语言、可扩展性强;

安装protoc

1)下载通用编译器

下载地址:https://github.com/protocolbuffers/protobuf/releases

下载21.7-win64版本即可。

gwt的rpc调用重试机制 grpc重连机制_gwt的rpc调用重试机制_05

2)添加至环境变量

gwt的rpc调用重试机制 grpc重连机制_服务端_06

3)安装go专用的protoc的生成器

go get github.com/golang/protobuf/protoc-gen-go

示例

1)创建.proto文件

// 当前proto的版本
syntax = "proto3";

// 格式: option go_package= "path;name";
// path: 指定生成文件的存放地址
// name: 生成的go文件所属的文件名
option go_package = "../service";

// 指定文件从生成出来的package
package service;

// 传输的对象
message User {
	string username = 1;
	int32 age = 2;
}

2)编译生成user.pb.go文件

执行命令

protoc --go_out=./ user.proto

如图所示,成功生成user.pb.go文件。

gwt的rpc调用重试机制 grpc重连机制_服务端_07

3)测试

测试程序main.go如下:

package main

import (
	"fmt"
	"service"
)

func main() {

	user := &service.User{
		Username: "dong",
		Age:      18,
	}

	// 序列化
	marshal, err := proto.Marshal(user)
	if err != nil {
		panic(err)
	}

	// 反序列化
	newUser := &service.User{}
	err := proto.Unmarshal(marshal, newUser)
	if err != nil {
		panic(err)
	}

	fmt.Println(newUser.String())
}

程序执行结果如下:

gwt的rpc调用重试机制 grpc重连机制_rpc_08

proto文件

message

message是定义一个消息类型的关键字。

字段规则

requried: 消息体中必填字段,不设置会导致解码异常;

optional: 消息体中的可选字段;

repeated: 消息体中可重复的字段,重复的值的顺序会被保留;

示例:

.proto中定义类型如下:

message User {
	optional string passwd = 3;
	repeated string address = 4;
}

生成的go文件中的定义如下:

// 传输的对象
type User struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	Passwd   *string  `protobuf:"bytes,3,opt,name=passwd,proto3,oneof" json:"passwd,omitempty"`
	Address  []string `protobuf:"bytes,4,rep,name=address,proto3" json:"address,omitempty"`
}

字段映射

gwt的rpc调用重试机制 grpc重连机制_服务端_09

默认值

类型

默认值

bool

false

整型

0

string

“”

enum

第一个枚举元素的值,默认值为0

message

DEFAULT_INSTANCE

标识号

在消息体的定义中,每个字段都必须要有一个唯一的标识号,标识号的范围是[0, 2^29-1]。

// 传输的对象
message User {
	string username = 1;
	int32 age = 2;
	optional string passwd = 3;
	repeated string address = 4;
}

嵌套消息

message Student
{
	message BoyStudent 
	{
		string name = 1;
		int32 age = 2;
		repeated int32 height = 3;
	}
	
	repeated BoyStudent boy = 1;
}
message StudentMsg
{
	Student.BoyStudent boy = 1;
}

定义接口

service AccessService {
	// rpc 服务的函数名 (传入参数) 返回 (返回参数)
	rpc ListDevices(ListRequest) returns (ListResponse);
}

gRPC

概念

概念

gRPC是由开发的一个高性能、通用的开源RPC框架,主要面向移动应用开发且基于HTTP/2协议标准而设计,同时支持大多数流行的编程语言。

理念

定义一个服务,指定其能被远程调用的方法(包括参数和返回类型),在服务端实现这个接口,并运行一个gRPC服务器来处理客户端调用。客户端拥有一个存根能够保存像服务端一样的方法。

框架

gwt的rpc调用重试机制 grpc重连机制_gwt的rpc调用重试机制_10

安装

1)查看GOPATH的路径

go env

2)下载grpc源码

cd $GOPATH

mkdir -p src/google.golang.org

cd src/google.golang.org/

git clone https://github.com/grpc/grpc-go

mv grpc-go grpc

3)下载grpc依赖1

cd $GOPATH

mkdir -p src/golang.org/x

cd src/golang.org/x/

git clone https://github.com/golang/net
git clone https://github.com/golang/text

4)下载grpc依赖2

cd $GOPATH

cd src/google.golang.org/

git clone https://github.com/google/go-genproto

mv go-genproto genproto

依赖需要下载完整,否则使用时报错!!!

实例

1)创建.proto文件

syntax = "proto3";

option go_package="../service";

package service;

message ProductRequest {
	int32 prod_id = 1;
}

message ProductReponse {
	int32 prod_stock = 1;
}

// 定义接口
service ProdService {
	rpc GetProductStock(ProductRequest) returns(ProductReponse);
}

2)编译.proto文件

// 指定grpc插件
protoc --go_out=plugins=grpc:./ Product.proto

3)创建接口实现文件

// product.go

package service

import "context"

var ProductService = &productService{}

type productService struct {
}

// 接口实现
func (p *productService) GetProductStock(context context.Context, request *ProductRequest) (*ProductReponse, error) {
	stock := p.GetStockById(request.ProdId)
	return &ProductReponse{ProdStock: stock}, nil
}

func (p *productService) GetStockById(id int32) int32 {
	return 100
}

4)创建服务端

// grpc_server.go

package main

import (
	"service"

	"google.golang.org/grpc"

	"net"

	"log"

	"fmt"
)

func main() {
	// 创建rpc实例
	rpcServer := grpc.NewServer()

	// 服务注册
	service.RegisterProdServiceServer(rpcServer, service.ProductService)

	// 启动监听
	listener, err := net.Listen("tcp", ":8800")
	if err != nil {
		log.Fatal("启动监听失败", err)
	}

	// 启动服务
	err = rpcServer.Serve(listener)
	if err != nil {
		log.Fatal("启动服务失败", err)
	}

	fmt.Println("启动服务成功")
}

5)创建客户端

// grpc_client.go

package main

import (
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	
	"log"
	
	"service"
	
	"context"
	
	"fmt"
)

func main() {
	// 创建连接
	conn, err := grpc.Dial(target:":8800", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatal("服务端连接失败: ", err)
	}

	// 退出时关闭连接
	defer conn.Close()
	
	// 创建客户端实例
	productServiceClient := service.NewProdServiceClient(conn)
	
	// 方法请求
	resq, err := productServiceClient.GetProductStock(context.Background(), &service.ProductRequest{ProdId: 233})
	if err != nil {
		log.Fatal("调用gRPC方法失败: ", err)
	}	
	
	fmt.Println("调用gRPC方法成功, ProdStock = ", resq.ProdStock)
}

6)测试

启动grpc_server,开始监听。

go run grpc_server.go

启动grpc_client,连接成功。

go run grpc_client.go

打印如下:

gwt的rpc调用重试机制 grpc重连机制_网络_11

7)工程目录

gwt的rpc调用重试机制 grpc重连机制_gwt的rpc调用重试机制_12

部分参考:

rpc介绍:http://wjhsh.net/GreenForestQuan-p-11543779.html

grpc-web:https://www.cncf.io/blog/2018/10/24/grpc-web-is-going-ga/

Protobuf官网:https://developers.google.com/protocol-buffers/docs/proto3

gRPC官网:https://grpc.io