想学分布式?来看看gRPC!

最近接触到了微前端的项目,其中用到了没接触过的gRPC,这里进行一下总结。

接触gRPC前我们先了解一下RPC

1.什么是RPC?

RPC( Remote Procedure Call )意为本地过程调用。

我们先来看看本地函数调用

function add(a, b){
	return a+b;
}
console.log(add(1, 2));
这其实就是非常普通的本地函数调用,因为在同一个地址空间。但是现在我们希望将系统改成分布式,把能共享的功能全部抽离出来,比如将上面的add函数放到一个单独的服务中, 在远程调用时,我们在服务A上需要执行的函数体是在服务B上的,而 RPC就是要像调用本地的函数一样去调远程函数。 

2.远程调用带来的问题:

​ 现在服务A上需要执行的函数体是在服务B上的,那么我们怎么告诉服务B我们要调用add函数而不是其他函数例如mutiple函数呢,所以RPC就是要解决分布式系统中,服务之间调用的问题,那么如何解决这个问题呢?

Call ID映射:

​ 在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 的对应表。相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。

但是现在又出现了新的问题:客户端如何把参数ID传给远程的函数呢?在本地我们只需要把参数压栈然后读取就可以了,但是远程调用的时候不能通过内存来传参。也有可能客户端和服务端使用的都是不同的语言

序列化和反序列化:

​ 这时候客户端就需要把数据序列化之后传给服务端,服务端接收到数据之后再反序列化转换成自己能读取的格式。 同理,从服务端返回的值也需要序列化反序列化的过程。

​ 具体过程如下:

// Client端
1. 将这个调用映射为Call ID。
2. 将Call ID,和参数a,b序列化。可以直接将它们的值以二进制形式打包
3. 把2中得到的数据包发送给ServerAddr,这需要使用网络传输层
4. 等待服务器返回结果
5. 如果服务器调用成功,那么就将结果反序列化,然后console.log进行打印

// Server端
1. 在本地维护一个Call ID到函数指针的映射call_id_map
2. 等待请求
3. 得到一个请求后,将其数据包反序列化,得到Call ID
4. 通过在call_id_map中查找,得到相应的函数指针
5. 将a和b反序列化后,在本地调用add函数,得到结果
6. 将结果序列化后通过网络返回给Client

3.什么是gRPC?

gRPC是一款RPC框架,其优点在于:

  • 使用Protobuf进行数据编码,提高数据压缩率。

  • 使用HTTP2.0弥补了HTTP1.1的不足。

  • 同样在调用方和服务方使用协议约定文件,提供参数可选,为版本兼容留下缓冲空间。

简单介绍一下Protobuf,它是Google开发的一种跨语言、跨平台、可扩展的用于序列化数据协议。 举个例子:

// XXXX.proto
service Test {
    rpc HowRpcDefine (Request) returns (Response) ; // 定义一个RPC方法
}
message Request {
    //类型 | 字段名字|  标号
    int64    user_id  = 1;
    string   name     = 2;
}
message Response {
    repeated int64 ids = 1; // repeated 表示数组
    Value info = 2;         // 可嵌套对象
    map<int, Value> values = 3;    // 可输出map映射
}
message Value {
    bool is_man = 1;
    int age = 2;
}

可以看出有几个明确的特点:

  • 有明确的类型,支持的类型有多种。

  • 每个field会有名字。

  • 每个field有一个数字标号,一般按顺序排列。

  • 能表达数组、map映射等类型。

  • 通过嵌套message可以表达复杂的对象。

  • 方法、参数的定义落到一个.proto 文件中,依赖双方需要同时持有这个文件,并依此进行编解码

​ 为什么选择protobuf,而不是普及最广的json作为编码方案? 可以做一个直观对比,以上文proto中的Response为例,一次输出json的结果是:

"{\"ids\":[123,456],\"info\":{\"is_man\":true,\"age\":20},\"values\":{\"110\":{\"is_man\":false,\"age\":18}}}"

​ 所有内容被打包成了一个字符串,里面包含字段名、value,当Reponse很大时,体积消耗很大,浪费主要在三个方面:

  • 字段名,例如上面的“ids”、“info”等,如果json体大,则重复会更多。
  • 数字用字符串表达了,例如123数字变成了“123”,这在编码后体积由一个字节变成三字节。
  • 类型字符,如[ 、 ]、{ 、}。

但如果是protobuf呢? 输出是一段人眼无法理解的二进制串,里面:

  • 去掉了字段名,转而以字段标号替代,通过标号可以在proto中找到字段名。
  • 没有类型字符等。
  • 用二进制表达内容,不会将数字转成字符串。
  • 字段值按顺序依次排列。

​ 这使得protobuf的编码结果体积,通常是json编码后的十分之一以下。同时由于排列简单,其解析算法的时空复杂度远小于json,对cpu消耗也小很多。这使得protobuf在大数据量、高频率的数据交互场景下,远胜于json,被大规模分布式RPC场景广泛使用。

​ 了解完Protobuf之后gRPC其他不作赘述,我们直接进入最近正在修改的微前端项目中看一下,下面会用到getDevices方法

getDevices方法需要传递的protobuf数据 :

message GetDevices {
    //message Request中为发送请求时携带的数据
    message Request {
    	//类型 | 字段名字|  标号
        int64 offset = 1;
        int64 limit = 2;
        filterFilter = 3;
    }
    
    //message Response中为服务端返回的数据
    message Response {
    	//类型 | 字段名字|  标号
        repeated devices = 101;
    }
}

调用getDevices方法

import { asset } from 'xxx';

let params = {
    limit,
    offset,
    filterFilter: {
       ...filters
    }
};
const res = await asset.getDevices(params);

​ 这里我们调用asset.getDevices方法的时候是不是就像本地函数调用一样?这就是gRPC的功劳。

​ 我们再进到引入asset对象的xxx目录中查看:

export const asset = get(window, 'yyy.client.asset');

​ 这里的get就是获取window中的yyy.asset对象,那么window中的yyy.asset对象从何而来呢?我们进入zzz目录中查看:

asset = new Asset(HOSTNAME); 
//HOSTNAME是我们asset.getDevices时需要发送请求的网关地址
window.yyy = {
  client: asset,
};

​ 至此我们找到了asset从何而来以及为什么能像使用本地函数一样来进行远程调用asset.getDevices方法。

​ 参考资料:

  • https://www.zhihu.com/question/25536695/answer/221638079
  • https://www.jianshu.com/p/2accc2840a1b
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值