迎接关注我的知乎专栏: 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
供应的要领即可,经由过程这个我们能够易如反掌地做到分布式盘算的map
和reduce
:
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);
由于Worker
中add
的是同步的要领,那末明显我们收到返回值的递次是:
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));