RPC协议
- RPC简介
- 为啥需要RPC
- RPC的调用过程
- gRPC
- ProtoBuffer
- gRPC实战
RPC简介
RPC(Remote Procedure Call Protocol)远程过程调用协议,目标就是让远程服务调用更加简单、透明。RPC 框架负责屏蔽底层的传输方式(TCP 或者 UDP)、序列化方式(XML/Json/ 二进制)和通信细节,服务调用者可以像调用本地接口一样调用远程的服务提供的接口,而不需要关心底层通信细节和调用过程。
其大致过程如上图所示。
为啥需要RPC
当我们的业务越来越多、应用也越来越多时,自然的,我们会发现有些功能已经不能简单划分开来或者划分不出来。我们平时写的项目大多数所有的模块都在一起,部署一台服务器上来,但是这也有几个问题:
- 单台服务器的硬件资源有限,能够承受的并发量并不高
- 任意模块的修改,都会导致整个项目代码重写编译,有可能你你只修改了只行代码并且至少一个模块的,当然你肯定只想编译这一个模块的代码就行,但是事实上整个项目需要重新编译。
- 系统当中有些模块属于CPU密集型,有些模块是I/O密集型的,造成各模块对硬件资源的需求不一样的.当我们把这些模块打包发在同一台机器上,我们就只能综合整个模块选择内存和CPU以及网路带宽,没有办法针对每个模块选择其合适的硬件资源。
此时可以将公共业务逻辑抽离出来以及将服务进行模块拆分,将之组成独立的服务 Service 应用,而原有的、新增的应用都可以与那些独立的 Service 应用 交互,以此来完成完整的业务功能,所以我们急需一种高效的应用程序之间的通讯手段来完成这种需求,RPC 大显身手的时候来了。
但是这也有一些我们需要思考的点:
- 大软件的模块怎么进行划分,各个模块之间可能会实现大量重复的代码
- 各个模块之间改怎么访问?这使用的就是RPC来实现。
RPC的调用过程
要让网络通信细节对使用者透明,我们需要对通信细节进行封装,我们先看下一个 RPC 调用的流程涉及到哪些通信细节:
- 服务消费方(client)调用以本地调用方式调用服务;
- client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
- client stub找到服务地址,并将消息发送到服务端;
- server stub收到消息后进行解码;
- server stub根据解码结果调用本地的服务;
- 本地服务执行并将结果返回给 server stub;
- server stub将返回结果打包成消息并发送至消费方;
- client stub接收到消息,并进行解码;
- 服务消费方得到最终结果。
而RPC的目标就是要 2~8(上面的2-8) 这些步骤都封装起来,让用户对这些细节透:
gRPC
1.grpc 的相关概念
什么是gRPC? gRPC 是一个高性能、通用的开源 RPC 框架,其由 Google 2015 年主要面向移动应用开发并基于 HTTP/2 协议标准而设计,基于 ProtoBuf 序列化协议开发,且支持众多开发语言。
由于是开源框架,通信的双方可以进行二次开发,所以客户端和服务器端之间的通信会更加专注于业务层面的内容,减少了对由 gRPC 框架实现的底层通信的关注。
2.gRPC的特点
- 跨语言使用,支持 C++、Java、Go、Python、Ruby、C#、Node.js、Android Java、Objective-C、PHP 等编程语言;
- 基于 IDL 文件定义服务,通过 proto3 工具生成指定语言的数据结构、服务端接口以及客户端 Stub;
- 通信协议基于标准的 HTTP/2 设计,支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量;
- 序列化支持 PB(Protocol Buffer)和 JSON,PB 是一种语言无关的高性 能序列化框架,基于 HTTP/2 + PB, 保障了 RPC 调用的高性能;
其交互过程大概如下:
1.交换机在开启 gRPC 功能后充当 gRPC 客户端的角色,采集服务器充当 gRPC 服务器角色;
2.交换机会根据订阅的事件构建对应数据的格式(GPB/JSON),通过 Protocol Buffers 进行编写 proto 文件,交换机与服务器建立 gRPC 通道,通过 gRPC 协议向服务器发送请求消息;
3.服务器收到请求消息后,服务器会通过 Protocol Buffers 解译 proto 文件,还原出最先定义好格式的数据结构,进行业务处理;
4.数据处理完后,服务器需要使用 Protocol Buffers 重编译应答数据,通过 gRPC 协议向交换机发送应答消息;
5.交换机收到应答消息后,结束本次的 gRPC 交互。
ProtoBuffer
ProtoBuffer 是一种更加灵活、高效的数据格式,与 XML、JSON 类似,在一些高性能且对响应速度有要求的数据传输场景非常适用。
ProtoBuffer 在 gRPC 的框架中主要有三个作用:
- 定义数据结构和字段
- 定义服务的接口以及生成客户端的Stub(个人理解就是一个客户端类)
- 通过序列化和反序列化方式提升传输效率。
为什么 ProtoBuf 会提高传输效率呢?
我们知道使用 XML、JSON 进行数据编译时,数据文本格式更容易阅读,但进行数据交换时,设备就需要耗费大量的 CPU 在 I/O 动作上,自然会影响整个传输速率。
Protocol Buffers 不像前者,它会将字符串进行序列化后再进行传输,即二进制数据。在这里不详细说明,有兴趣的可以看博客的另外一篇博客.
为什么 ProtoBuf 会提高传输效率呢?
我们知道使用 XML、JSON 进行数据编译时,数据文本格式更容易阅读,但进行数据交换时,设备就需要耗费大量的 CPU 在 I/O 动作上,自然会影响整个传输速率。
Protocol Buffers 不像前者,它会将字符串进行序列化后再进行传输,即二进制数据
gRPC 能够做到跨平台,多语言,很大一部分得益于Protobuffer自动的编译器。前面提到的 proto 文件就是通过编译器进行编译的,proto 文件需要编译生成一个类似库文件,基于库文件才能真正开发数据应用。
具体用什么编程语言编译生成这个库文件呢?由于现网中负责网络设备和服务器设备的运维人员往往不是同一组人,运维人员可能会习惯使用不同的编程语言进行运维开发,那么 Protocol Buffers 其中一个优势就能发挥出来——跨语言。
gRPC实战
在这里博主选择go语言进行演示grpc的使用,其他铁子也可以自行选择提前语言进行使用。在使用grpc之前各位铁子要安装好这个protobuf。
安装grpc最核心的库
go get google.golang.org/grpc
上面安装是安装了这个protocol的编译器,它可以生成各种语言因此除了这个编译器我们还需要配合各个语言的代码生成工具,对于golang来说我们叫做protoc-gen-go
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
安装好之后我们就可以开始编写proto文件
syntax="proto3";
option go_package=".;service";//这部分内容是关于生成的go代码,在那个目录的包中,service代表生成的go文件的package是service
message HelloRequest{
bytes requestName=1;
}
message HelloResponse{
bytes responseMsg=1;
}
service SayHello{
rpc SayHello(HelloRequest)returns(HelloResponse);
}
编写完成proto文件之后我们开始使用这个命令生成代码
protoc --go_out=. hello.proto
protoc --go-grpc_out=. hello.proto
下面我们开始编写客户端的代码
package main
import (
"context"
"errors"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
service "http/gRpc/server/proto"
"net"
)
type server struct {
*service.UnimplementedSayHelloServer //包含定义的sayHello方法的结构体
}
func (s *server) SayHello(ctx context.Context, req *service.HelloRequest) (*service.HelloResponse, error) {
//获取源数据信息
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, errors.New("未传输token")
}
var appId string
var appKey string
if v, ok := md["appid"]; ok {
appId = v[0]
}
if v, ok := md["appkey"]; ok {
appKey = v[0]
}
fmt.Println("the appkey is" + appKey)
//这里应该要去数据库里面查询,用户id appid
if appId != "ksy" || appKey != "122" {
return nil, errors.New("错误" + appKey + appId)
}
return &service.HelloResponse{
ResponseMsg: []byte(req.RequestName),
}, nil
}
func Test() {
//开启监听
listen, err := net.Listen("tcp", "127.0.0.1:8909")
if err != nil {
panic(err)
}
//创建grpc服务器
grpcServer := grpc.NewServer()
//注册进来,在grpc 服务端当中注册我们编写的服务
service.RegisterSayHelloServer(grpcServer, &server{})
//启动服务
if err := grpcServer.Serve(listen); err != nil {
panic(err)
}
}
func main() {
Test()
}
这个客户端对应代码
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
service "http/gRpc/server/proto"
)
func Test() {
//链接server端,此处禁用安全链接
conn, err := grpc.Dial(":8909", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
defer conn.Close()
//建立链接
client := service.NewSayHelloClient(conn)
//发起远程过程调用
resp, err := client.SayHello(context.Background(), &service.HelloRequest{RequestName: []byte("ksy")})
fmt.Println(resp)
}
type ClientTokenAuth struct {
}
// GetRequestMetadata 重写接口
func (c *ClientTokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appid": "ksy",
"appKey": "122",
}, nil
}
func (c *ClientTokenAuth) RequireTransportSecurity() bool {
//不开启安全传输
return false
}
// TestToke token认证
func TestToke() {
var opts []grpc.DialOption//选项
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
opts = append(opts, grpc.WithPerRPCCredentials(new(ClientTokenAuth)))
conn, err := grpc.Dial(":8909", opts...)
if err != nil {
panic(err)
}
defer conn.Close()
//建立链接
client := service.NewSayHelloClient(conn)
//发起远程过程调用
resp, err := client.SayHello(context.Background(), &service.HelloRequest{RequestName: []byte("ksy")})
fmt.Println(err)
fmt.Println(resp)
}
func main() {
TestToke()
}
下面我们可以把客户端和服务端启动起来,就可以进行通信了。