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

迎接关注我的知乎专栏: 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记录下它是不是忙碌的状况,每次当有挪用需求的时刻,先遍历这张表,

  1. 假如找到有余暇的Worker,那末就将对它发送挪用;

  2. 假如一切Worker都忙碌,那末先把这个挪用暂存在一个行列当中;

  3. 当收到某个Worker的返回值后,会搜检行列中是不是有使命,有的话,那末就对这个Worker发送最前的函数挪用,若没有,就把这个Worker设为余暇状况。

详细使命分配的代码比较冗余,疏散在各个要领内,所以只引见要领,就不贴上来了/w\

悉数的Manager代码在这里(抱歉还没时候补解释):

Maus/manager.js at master · starkwang/Maus

三、Worker的完成

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

Worker的完成远比Manager简朴,由于它只须要对唯一一个Manager通讯,它的逻辑只要:

  1. 吸收Manager发来的数据;

  2. 依据数据做出响应的回响反映(函数挪用、初始化等等);

  3. 发送返回值

所以我们也不放代码了,有兴致的能够看这里:

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://segmentfault.com/a/1190000005102984
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞