Jigsaw.js 文档
1.1 简介
Jigsaw的中文是拼图的意思,该项目最早来源于一个思考:
将一个服务拆分成多个微服务,最重要的是什么。
服务模块之间的解耦要靠一个消息组件实现,于是Jigsaw就诞生了,专门解决独立服务之间互相调用的问题。
1.1.1 特性
①Jigsaw和一般的RPC框架一样,有生产者和消费者存在,但并不像普通的RPC框架一样繁琐,因为每一个Jigsaw实例既是生产者也是消费者。
②Jigsaw想做到调用另一个进程的函数就像是同个作用域的函数一样简单,于是在用法上做了点有趣的设计。
③Jigsaw的一个设计的要求点是可靠性,于是从Node.js的数据报(即UDP协议)开始重新封装一层Jigsaw专用的通信协议。该协议具有丢包自动重发、大包拆分再发、每包必有回应的特性,是一种可靠的协议。于是无需关注内部实现,直接使用它稳定可靠的通信即可。
④Jigsaw存在一个域名服务器,该域名服务器是Jigsaw自己的内部实现,用于映射Jigsaw的名字到具体的网络地址。使用域名服务器可以在互联网上组成一个虚拟的Jigsaw网络。网络内的任何一个Jigsaw实例都可以通过名字访问另一个Jigsaw实例。
⑤Jigsaw自动管理连接,保证连接之间的健壮性,即使某个计算机发生崩溃导致Jigsaw实例被关闭,也会在下次打开的时候无缝的重新接入Jigsaw网络。
1.2 安装
在npm项目下执行命令npm install jigsaw.js --save
1.3 简单实例
1.3.1
human.js
let {jigsaw}=require("jigsaw.js")("127.0.0.1","127.0.0.1");
let human=new jigsaw("human");//这是一个jigsaw实例
human.send("gun:shoot",{bullets:10});
gun.js
let {jigsaw}=require("jigsaw.js")("127.0.0.1","127.0.0.1");
let gun=new jigsaw("gun");
gun.port("shoot",({bullets})=>{
for(let i=0;i<bullets;i++)
console.log("shoot!");
return {msg:`我已经发射了${bullets}颗子弹`};
})
index.js
let {domainserver,webserver}=require("jigsaw.js")("127.0.0.1","127.0.0.1");
//第一个参数指的是需要绑定到的网卡IP地址,一般默认为127.0.0.1表示绑定到本机IP地址,这样的情况下,所有Jigsaw实例只能在本机内任意通信。
//第二个参数指的是domainserver域名服务器所在主机的IP地址。默认是本机。
//这两个参数决定了Jigsaw实例的环境,一般来说,可以互相访问的Jigsaw实例,这两个参数全部都是一样的。
let {fork}=require("child_process");
domainserver();//一个Jigsaw网络内至少要启动一个域名服务器,所有Jigsaw实例都可以使用该域名服务器
webserver(1793);//本行可以不写,本行可以启动一个访问Jigsaw网络的Web服务器。
fork("human.js");
fork("gun.js");
然后node index.js
就可以运行该实例了。
此处只是一个最简单的用法,对于Web应用的复杂用法可以参考分布式架构的设计,之后会在文档中详细说明。
关于物联网相关的用法,文档之后也会补充。
1.3.2 简单的 Web应用 接口服务器
该文件在examples/simpleaccount.js
let {jigsaw,domainserver,webserver}=require("../index.js")("127.0.0.1","127.0.0.1");
domainserver();
webserver(80);
let accounts=[];
let jg=new jigsaw("account");
jg.port("register",({username,password})=>{
let userid=accounts.length;
accounts[userid]={userid,username,password,token:""};
return {error:false,msg:`注册成功!你的账号是:${userid}`}
});
jg.port("login",({userid,password})=>{
if(!accounts[userid])return {error:true,msg:"系统中不存在该账户"};
if(accounts[userid].password == password){
let token=Math.random()+"";
accounts[userid].token=token;
return {error:false,msg:`登录成功!欢迎你,${accounts[userid].username},你的登录态令牌:${token}`}
}else
return {error:true,msg:"登录失败,密码错误!"};
})
用node simpleaccount.js
启动后。
可以通过
http://127.0.0.1/account/register?username=testuser&password=123
http://127.0.0.1/account/login?userid=0&password=123
进行测试
1.4 API 接口
1.4.1 jigsaw.prototype.constructor(jgname)
jigsaw类的构造器,第一个参数是jigsaw的名字,jigsaw的名字是很重要的,可以用来对实例进行命名空间分配,也可以通过名字访问其它jigsaw实例。
let jg = new jigsaw("myjigsaw");
1.4.2 jigsaw.prototype.port(name,func)
为jigsaw实例声明一个接口,第一个参数是接口名,第二个参数是该接口被调用后触发的函数,该函数可以是一个异步函数(async function)。
jg.port("add",(a,b)=>{
return a+b;
})
jg.port("addasync",async(a,b)=>{
let sleep=(t)=>new Promise((r)=>setTimeout(r,t));
await sleep(1000);
return a+b;
})
1.4.3 jigsaw.prototype.send(path,data) : [Promise]
对指定的路径的jigsaw实例的一个接口进行一次远程调用。
其中第一个参数是路径,第二个参数是传递的数据,该参数必须是一个对象。
若该参数是undefined或者不填,则相当于是传递了空对象{}
。
返回值是一个Promise,会直接返回远程调用的结果。(如果该结果为空,则一定是null)
路径的格式由 jigsaw名 + 冒号 + 接口名 组成。
jg.send("gun:shoot",{bullets:10});
jg.send("app.database:select",{sheet:"users",id:233});
1.4.4 jigsaw.prototype.handle(obj)
该方法可以批量定义接口。就像这样:
jg.handle({
shoot(){
},
async reload(){
}
})
还可以这样:
jg.handle((portname,data)=>{
console.log(`${portname}接口收到了数据`,data);
})
1.4.5 jigsaw.prototype.dighole(targetjigsaw) : [Promise]
向目标的jigsaw打一个"洞",目标jigsaw通过该"洞"可以稳定的访问本jigsaw。
你可以像这样向"洞"发送数据,就和jigsaw的远程调用方法是一样的
对于内网的jigsaw实例访问外网的jigsaw实例,若希望通信可以双向畅通无阻,
那么应当使用该方法打出一个"洞",使用该"洞"作为jigsaw的名字进行远程调用。
若jigsaw实例同在一个内网或者互联网上,则完全不需要使用本方法。
//下面的代码的执行环境可以是可以访问互联网的局域网
let lan = new jigsaw("LAN");
lan.port("call",()=>{
console.log("我被调用了");
});
lan.dighole("INTERNET");
//下面的代码的执行环境可以是互联网
let internet = new jigsaw("INTERNET");
internet.send(`LAN:call`);
1.4.6 jigsaw.setoption(jgname,option) : [Promise]
对特定的jigsaw名进行配置选项,该配置会一直保存在域名服务器上。
该选项对象中有如下属性
①jgcount
若一个jigsaw名被设置了jgcount选项,那么下一次从域名服务器查询该jigsaw实例的网络地址时, 会分别从好几个jigsaw实例中随机选择一个网络地址并返回。
该功能用途主要是负载均衡。
例如一个实例的名字是ticket
,并设置了jgcount属性为4
那么下次任何实例访问该实例的时候,域名服务器都会从以下这4个名字中随机选择一个地址返回
ticket
ticket@1
ticket@2
ticket@3
这样流量被分流到了4个jigsaw实例上,由他们共同分担并承受压力。配合数据库可以实现解决了c10k问题的web应用。
该方法是一个静态方法,用法应当是
await jigsaw.setoption("ticket",{jgcount:4});
1.4.7 jigsaw.prototype.close()
直接关闭 jigsaw 实例,jigsaw内部的套接字实例、保持连接的域名客户端也会因此被关闭。
1.5 测试
本项目可以通过 mocha 框架进行测试,
在项目目录下直接运行
npm test
即可开始测试
另外,你还可以检查测试用例的覆盖率
在项目目录下直接运行
npm run cov
即可得到覆盖率报告
2.1 负载均衡 与 网络IO
一般来说一个分布式系统的负载均衡的实现应当在RPC框架上。所以jigsaw实现了基本的负载均衡。
jigsaw实例可以存在在进程上,那么如果启动多个进程,使用了jigsaw.setoption
方法使得域名服务器开始对请求分流。各个进程就可以等量的处理流量。
因为要保证数据的唯一性,传统的负载均衡应用实现都要依赖数据库。
理想的分布式系统是没有系统之间进行数据交换的成本的,也就是说,RPC框架之间进行通信的需要的时间和空间几乎为0. 那么理论上任何系统都可以无限拓展,不受性能的约束。
但是现实并不是这样的,RPC框架进行一次数据交换,一般靠的是操作系统提供的套接字接口。那么网络IO就需要占用一定的时间。任何分布式系统的设计都应当考虑进去这一点。对于一个方法需要大量远程调用的请求应当重新考虑设计。
由于Jigsaw基于node.js,并且场景为IO密集型,并且是异步IO,所以网络IO的性能是较好的,这也是选择在node.js下开发一款RPC框架的原因。
2.2 推荐的命名规范
一般来说,对Jigsaw实例的命名(即构造器的第一个参数)可以参考这样的规范。
①使用命名空间
可以按照功能从命名上对Jigsaw实例进行分组。
例如这样使用点符号作为命名空间
app.auth //应用的认证层入口,用于鉴权等
app.database //应用的数据库中间件层
app.ticket //应用的订单层
app.account //应用的账户管理层
app.interface.ticket //应用的订单接口
app.interface.register //应用的注册接口
Jigsaw实例的命名并不会影响到Jigsaw的使用,事实上,大部分字符都可以作为名字。
②适当的时候可以匿名
let jg=new jigsaw();
像这样创建一个jigsaw实例,那么该jigsaw实例被称作匿名jigsaw实例。
一般这种实例并不设置接受远程调用的接口,当然要设置也是可以的。
但只能通过打洞的方式进行访问。