资讯专栏INFORMATION COLUMN

一个浏览器和NodeJS通用的RPC框架

Tony / 735人阅读

摘要:欢迎关注我的知乎专栏这几天写了个小型的框架,最初只是想用写个纯平台的东西,后来无意中开了个脑洞,如果基于把浏览器当做,那岂不是只要是能运行浏览器或者的设备,都可以作为分布式计算中的一个了吗打开一张网页,就能成为分布式计算的一个节点,看起

欢迎关注我的知乎专栏: https://zhuanlan.zhihu.com/starkwang

starkwang/Maus: A Simple JSON-RPC Framework running in NodeJS or Browser, based on websocket.

这几天写了个小型的RPC框架,最初只是想用 TCP-JSON 写个纯 NodeJS 平台的东西,后来无意中开了个脑洞,如果基于 Websocket 把浏览器当做 RPC Server ,那岂不是只要是能运行浏览器(或者nodejs)的设备,都可以作为分布式计算中的一个 Worker 了吗?

打开一张网页,就能成为分布式计算的一个节点,看起来还是挺酷炫的。

一、什么是RPC

可以参考:谁能用通俗的语言解释一下什么是RPC框架? - 知乎

简单地说就是你可以这样注册一个任意数量的worker(姑且叫这个名字好了),它里面声明了具体的方法实现:

var rpcWorker = require("maus").worker;
rpcWorker.create({
    add: (x, y) => x + y
}, "http://192.168.1.100:8124");

然后你可以在另一个node进程里这样调用:

var rpcManager = require("maus").manager;
rpcManager.create(workers => {
    workers.add(1, 2, result => console.log(result));
}, 8124)

这里我们封装了底层的通信细节(可以是tcp、http、websocket等等)和任务分配,只需要用异步的方式去调用worker提供的方法即可,通过这个我们可以轻而易举地做到分布式计算的mapreduce

rpcManager.create(workers => {
    //首先定义一个promise化的add
    var add = function(x, y){
        return new Promise((resolve, reject)=>{
            workers.add(x, y, result => resolve(result));
        })
    }
    //map&reduce
    Promise.all([add(1,2), add(3,4), add(4,5)])
        .then(result => result.reduce((x, y) => x + y))
        .then(sum => console.log(sum)) //19
}, 8124)

如果我们有三个已经注册的Worker(可能是本地的另一个nodejs进程、某个设备上的浏览器、另一个机器上的nodejs),那么我们这里会分别在这三个机器上分别计算三个add,并且将三个结果在本地相加,得到最后的值,这就是分布式计算的基础。

二、Manager的实现 0、通信标准

要实现双向的通信,我们首先要定义这样一个“远程调用”的通信标准,在我的实现中比较简单:

{
    [id]: uuid          //在某些通信中需要唯一标识码
    message: "......"   //消息类别
    body: ......        //携带的数据
}
1、初始化

首先我们要解决的问题是,如何让Manager知道Worker提供了哪些方法可供调用?

这个问题其实很简单,只要在 websocket 建立的时刻发送一个init消息就可以了,init消息大概长这样:

{
    message: "init",
    body: ["add", "multiply"] //body是方法名组成的数组
}

同时,我们要将Manager传入的回调函数,记录到Manager.__workersStaticCallback中,以便延迟调用:

manager.create(callback, port) //记录下这个callback

//一段时间后。。。。。。

manager.start() //任务开始
2、生成workers实例

现在我们的Manager收到了一个远程可调用的方法名组成的数组,我们接下来需要在Manager中生成一个workers实例,它应该包含所有这些方法名,但底层依然是调用一个webpack通信。这里我们可以用类似元编程的奇技淫巧,下面的是部分代码:

//收到worker发来的init消息之后
var workers = {
    __send: this.__send.bind(this), //这个this指向Manager,而不是自己
    __functionCall: this.__functionCall.bind(this) //同上
};
var funcNames = data.body; //比如["add", "multiply"]
funcNames.forEach(funcName => {
    //使用new Function的奇技淫巧
    rpc[funcName] = new Function(`
        //截取参数
        var params = Array.prototype.slice.call(arguments,0,arguments.length-1);
        var callback = arguments[arguments.length-1];
        
        //这个__functionCall调用了Manager底层的通信,具体在后面解释
        this.__functionCall("${funcName}",params,callback);
    `)
})
//将workers注册到Manager内部
this.__workers = workers;
//如果此时Manager已经在等待开始了,那么开始任务
if (this.__waitingForInit) {
    this.start();
}

还记得上面我们有个start方法么?它是这样写的:

start: function() {
    if (this.__workers != undefined) {
        //如果初始化完毕,workers实例存在
        this.__workersStaticCallback(this.__workers);
        this.__waitingForInit = false;
    } else {
        //否则将等待初始化完毕
        this.__waitingForInit = true;
    }
},
3、序列化

如果只是单个Worker和单个Manager,并且远程方法都是同步而非异步的,那么我们显然不需要考虑返回值顺序的问题:

比如我们的Manager调用了下面一堆方法:

workers.add(1, 1, callback);
workers.add(2, 2, callback);
workers.add(3, 3, callback);

由于Workeradd的是同步的方法,那么显然我们收到返回值的顺序是:

2
4
6

但如果Worker中存在一个异步调用,那么这个顺序就会被打乱:

workers.readFile("xxx", callback);
workers.add(1, 1, callback);
workers.add(2, 2, callback);

显然我们收到的返回值顺序是:

2
4
content of xxx

所以这里就需要对发出的函数调用做一个序列化,具体的方法就是对于每一个调用都给一个uuid(唯一标识码)。

比如我们调用了:

workers.add(1, 1, stupid_callback);

那么首先Manager会对这个调用生成一个 uuid :

9557881b-25d7-4c94-84c8-2463c53b67f4

然后在__callbackStore中将这个 uuid 和stupid_callback 绑定,然后向选中的某个Worker发送函数调用信息(具体怎么选Worker我们后面再说):

{
    id: "9557881b-25d7-4c94-84c8-2463c53b67f4",
    message: "function call",
    body: { 
        funcName: "add", 
        params: [1, 1] 
    }
}

Worker执行这个函数之后,发送回来一个函数返回值的信息体,大概是这样:

{
    id: "9557881b-25d7-4c94-84c8-2463c53b67f4",
    message: "function call",
    body: { 
        result: 2 
    }
}

然后我们就可以在__callbackStore中找到这个 uuid 对应的 callback ,并且执行它:

this.__callbackStore[id](result);

这就是workers.add(1, 1, stupid_callback)这行代码背后的原理。

4、任务分配

如果存在多个Worker,显然我们不能把所有的调用都傻傻地发送到第一个Worker身上,所以这里就需要有一个任务分配机制,我的机制比较简单,大概说就是在一张表里对每个Worker记录下它是否繁忙的状态,每次当有调用需求的时候,先遍历这张表,

如果找到有空闲的Worker,那么就将对它发送调用;

如果所有Worker都繁忙,那么先把这个调用暂存在一个队列之中;

当收到某个Worker的返回值后,会检查队列中是否有任务,有的话,那么就对这个Worker发送最前的函数调用,若没有,就把这个Worker设为空闲状态。

具体任务分配的代码比较冗余,分散在各个方法内,所以只介绍方法,就不贴上来了/w

全部的Manager代码在这里(抱歉还没时间补注释):

Maus/manager.js at master · starkwang/Maus

三、Worker的实现

这里要再说一遍,我们的RPC框架是基于websocket的,所以Worker可以是一个PC浏览器!!!可以是一个手机浏览器!!!可以是一个平板浏览器!!!

Worker的实现远比Manager简单,因为它只需要对唯一一个Manager通信,它的逻辑只有:

接收Manager发来的数据;

根据数据做出相应的反应(函数调用、初始化等等);

发送返回值

所以我们也不放代码了,有兴趣的可以看这里:

Maus/worker.js at master · starkwang/Maus

四、写一个分布式算法

假设我们的加法是通过这个框架异步调用的,那么我们该怎么写算法呢?

在单机情况下,写个斐波拉契数列简直跟喝水一样简单(事实上这种暴力递归的写法非常非常傻逼且性能低下,只是作为范例演示用):

var fib = x => x>1 ? fib(x-1)+fib(x-2) : x

但是在分布式环境下,我们要将workers.add方法封装成一个Promise化的add

//这里的x, y可能是数字,也可能是个Promise,所以要先调用Promise.all
var add = function(x, y){
    return Promise.all([x, y])
        .then(arr => new Promise((resolve, reject) => {
            workers.add(arr[0], arr[1], result => resolve(result));
        }))
}

然后我们就可以用类似同步的递归方法这样写一个分布式的fib算法:

var fib = x => x>1 ? add(fib(x-1), fib(x-2)) : x;

然后你可以尝试用你的电脑里、树莓派里、服务器里的nodejs、手机平板上的浏览器作为一个Worker,总之集合所有的计算能力,一起来计算这个傻傻的算法(事实上相比于单机算法会慢很多很多,因为通信上的延迟远大于单机的加法计算,但只是为了演示啦):

//分布式计算fib(40)
fib(40).then(result => console.log(result));

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79411.html

相关文章

  • 精读《REST, GraphQL, Webhooks, & gRPC 如何选型》

    摘要:而利用进一步提高了序列化速度,降低了数据包大小。带来的最大好处是精简请求响应内容,不会出现冗余字段,前端可以决定后端返回什么数据。再次强调,相比和,是由前端决定返回结果的反模式。请求者可以自定义返回格式,某些程度上可以减少前后端联调成本。 1 引言 每当项目进入联调阶段,或者提前约定接口时,前后端就会聚在一起热火朝天的讨论起来。可能 99% 的场景都在约定 Http 接口,讨论 URL...

    DevWiki 评论0 收藏0
  • gRPC实现跨语言微服务间通信 -- 精通外语电报员与煲电报粥小怪兽

    摘要:插画牛肉框架小怪兽的电报员一旦系统怪物被拆分成了多个服务小怪兽,小怪兽们如何沟通协作就成了我们最关心的问题。插画牛肉实现客户端小怪兽发送今晚的月色真美,服务端小怪兽收到电报内容,并回复。 作者:亚瑟、文远 1. 微服务框架 -- 从系统怪物到服务小怪兽 一个小巧的单体应用会随着公司业务的扩张而慢慢成长,逐渐演化成一个庞大且复杂的系统怪物,系统任何一处的问题都将影响整个怪物的表现,很少有...

    waltr 评论0 收藏0
  • RPC框架实践之:Google gRPC

    摘要:与文章框架实践之一文中实践的另一种通用框架能通过自动生成对应语言的接口类似,也能自动地生成和的存根,我们只需要一个命令就能快速搭建起运行环境。类似于之前对于框架的实践步骤,下面一一阐述。 showImg(https://segmentfault.com/img/remote/1460000014946557); 概述 gRPC是Google开源的通用高性能RPC框架,它支持的是使用P...

    malakashi 评论0 收藏0
  • RPC框架实践之:Google gRPC

    摘要:与文章框架实践之一文中实践的另一种通用框架能通过自动生成对应语言的接口类似,也能自动地生成和的存根,我们只需要一个命令就能快速搭建起运行环境。类似于之前对于框架的实践步骤,下面一一阐述。 showImg(https://segmentfault.com/img/remote/1460000014946557); 概述 gRPC是Google开源的通用高性能RPC框架,它支持的是使用P...

    vibiu 评论0 收藏0
  • 带入gRPC:gRPC及相关介绍

    摘要:原文地址带入及相关介绍项目地址作为开篇章,将会介绍相关的一些知识。 原文地址:带入gRPC:gRPC及相关介绍 项目地址:go-grpc-example 作为开篇章,将会介绍 gRPC 相关的一些知识。简单来讲 gRPC 是一个 基于 HTTP/2 协议设计的 RPC 框架,它采用了 Protobuf 作为 IDL 你是否有过疑惑,它们都是些什么?本文将会介绍一些常用的知识和概念,更详...

    y1chuan 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<