rapid-webrpc
Version:
Rapid-webrpc toools
438 lines (362 loc) • 11.7 kB
JavaScript
/**
* Web RPC Services.
* publish the functional end-point for Web App.
*
*
* 关于api上发布的方法,做如下约定,
* 1. 需要返回数据或确认调用状态的, 统一使用异步风格,并约定最后一个参数为 callback 用于回传数据.
* 2. 无需返回及确认状态的方法填接, 无回调参数, 无返回值.
* 3. 参数必须保证可以直接进行完整的JSON序列化.
*
*
* Protocol :
*
* base : 基本结构,一个json对像.
*
* {
* type:"", // 消息类型
* body:"", // 负载内容
*
* // --- 其它附加信息 ---
* "key1":"value1",
* "key2":"value2",
* ...
* "keyn":"valuen",
* }
*
* echo : 客户端连接上来之后的第一个消息.用于请求info信息,并通知server端自己是谁.
*
* {
* type:"echo",
* clientId:"",
* }
*
* Info : 一段JSON信息,用来描述当前服务结点的状态,提供的服务等情况.
*
* {
* type:"info",
* body:{
* points : {
* ifName:{
* methods:{
* methodName1:{
* input:["string","number","object"],
* output:["error","array","string"]
* }
* }
* }
* },
* publish : {
* name:{
* struct:{},
* opts:{
* find:true,
* remove:true,
* update:true,
* save:true
* }
* }
* }
* }
* }
*
* GlogalError : 一段JSON,用来描述一段非调用方法产生的错误信息. 如客户端消息解析错误等.
* {
* type:"error",
* body:"" // 错误消息
* }
*
* Message用于描述通迅内容,固定字段名,结构如下:
*
* {
* type:"call"|"callback"|"notify" // 调用类型 Call 表示调用, Notify 表示单向通知调用,不需要返回. Callback 表示方法返回结果.
* sn:"", // 调用序号,用于标识通信顺序, Call,Notify累加值,每次加1, Callback为对应请求的序号
* name:"", // 接口名称
* method:"MethodName", // 方法名称
* body:[], // 输入参数,使用一个array,注意内容与Methods定义的Input结构相同.
* cid:"", // clientid,用于在标识唯一client,由server侧解析消息时附加
* }
*
*/
var format = require("util").format;
var Domain = require("domain");
var randomStr =function(_len){
for(var str = "" , len = _len || 10 ; str.length < len ; str += (~~(Math.random() * 36)).toString(36));
return str;
};
var defaultInput = function(len){
for(var arr = [], i = len -1; arr.length < i;arr.push("object"));
return arr;
};
var SPoint = function(origin, _description,_name){
var me = this , description = _description , name = _name;
if(!origin){
throw new Error("origin is undefined");
}
if(!(me instanceof SPoint)){
return new SPoint(origin);
}
if(!description){
description = {};
}
name = me.name = description.name = description.name || name || randomStr(10);
//不相等表示未提供接口描述, 扫描生成
if(description != _description){
var methods = description.methods = {};
var method = null , parlen = 0;
for(var key in origin){
// 以下划线开头的,认为是内部方法或对像,直接忽略
if(key[0] == "_"){
continue;
}
/*
* 由于需要在线程间通导,所以生成的方法,全为异步方法.
*/
if((method = origin[key]) instanceof Function){
/*
* function生成代理方法.并创建方法描述,
* 这里根据n = function.length做为判断依据, 规则如下:
*
* n = 0 无参数也无传出的通知方法.
* n = 1 无参数但有返回值.
* n > 1 有n - 1个对像参数, 最后一个为callback. 由于这个callback无法检则参数列表,
* 所以认为有两个参数,第一个为错误对像,第二个为数据对像.
*
* 由于以上判断方式,所以可见,这里不支持可变参数.其目的为在前端
* 能完成一部份参数检查工作,减小server端带宽及压力.
*
*/
switch(parlen = method.length || 0){
case 0 :
methods[key] = {
input:[]
};
break;
case 1 :
methods[key] = {
input:[],
output:['error','object']
};
break;
default:
methods[key] = {
input:defaultInput(parlen),
output:['error','object']
};
}
me[key] = (function(origin,method){
// 原始方法的代理方法
return function(args){
var len = arguments.length;
if(len == 1 && Array.isArray(args)){
// 由于约定好最后个参数应该是一个function.
// 所以如果只有一个参数,并且是array,则表示在外层已经拼接了参数列表.
method.apply(origin,args);
}else if(len >= 1 && typeof(arguments[len - 1]) == "function"){
// 如果有大于1个参数,并且最后一个参数是一个function,表示没有拼接,只是正常的调用
method.apply(origin,arguments);
}else if(len == 0 && method.length == 0){
// 没有调用参数,原始方法也没有参数,确认为通知方法.
method.apply(origin,arguments);
}else{
// 错误调用
throw new Error("error arguments");
}
};
})(origin,method);
}else{
/*
* 非function,生成get方法. set方法建议由原始对像自己实现.例如有些属性不应该被改变
*/
me[getterName(key)] = (function(origin,key){
return function(cb){
cb && cb(origin[key]);
};
})(origin,key);
}
}
}
me.toString = function(){
return JSON.stringify(description);
};
me.valueOf = function(){
return description;
};
}
// Client
var ClientPrototype = {
parseMsg : function(msg){
var spliceStr = "\n\n", index;
if((index = this.__receiveStr.lastIndexOf(spliceStr)) == -1){
// waiting next
return;
}
// 能完整解析的内容
var parseStr = this.__receiveStr.substring(0,index);
// 剩余的
this.__receiveStr = this.__receiveStr.substring(index + 2);
var parseArr = parseStr.split(spliceStr);
var msgs = [];
parseArr.forEach(function(str){
var me = this;
var decodeStr = decodeURI(str);
var msg = JSON.parse(decodeStr);
log.dev("dispatch msg : type:%s, method:%s", msg.type , msg.method);
this.dispatch(msg,function(err,msg){
if(err){
me.emit("error",err);
return;
}
me.write(msg);
});
},this);
},
receive:function(msg){
log.dev("receive message : ",msg);
this.__receiveStr += msg;
this.parseMsg();
},
setWritable:function(conn){
this.__conn = conn;
},
setServerPoint:function(sp){
this.__sp = sp;
},
write:function(msg){
if("string" == typeof msg){
this.__conn.write(msg);
}else{
this.__conn.write(JSON.stringify(msg));
}
},
dispatch:function(msg,cb){
if(msg.type == "echo"){
if(this.name != msg.body){
log.info("WebRPC:client change name from [%s] to [%s]", this.name, msg.body);
}
this.name = msg.body;
}
this.__sp.__dispatch(msg,cb);
}
};
var Client = function(conn,sp){
var me = Domain.create();
me.__receiveStr = "";
me.name = "unknow client [" + conn.remoteAddress +"]";
for(var key in ClientPrototype){
if("function" == typeof ClientPrototype[key]){
me[key] = me.bind(ClientPrototype[key]);
}else{
me[key] = ClientPrototype[key]
}
}
me.on("dispose",function(){
me.__receiveStr = null;
me.__conn = null;
me.__sp = null;
});
me.setWritable(conn);
me.setServerPoint(sp);
return me;
}
// service point manager
var SPM = function(){
this.sps = {};
}
SPM.prototype = {
// 获得已有服务接口的描述.
info:function(){
var iobj = {};
var pointMgr = this.sps;
for(var key in pointMgr){
iobj[key] = pointMgr[key].valueOf();
}
return iobj;
},
// 发布一个服务接口.
addPoint:function(origin,description,iname){
var p = new SPoint(origin,description,iname);
this.sps[p.name] = p;
return p;
},
/**
* 接收并处理一条消息
*/
__dispatch:function(msg,cb){
var method = msg.method;
var body = msg.body;
var sp = null;
switch(msg.type){
case "echo":
log.dev("WebRPC: register client , %s", msg.body);
cb(null,{type:"info", body:this.info()});
return;
case "notify" :
cb = "notify";
case "call" :
// 两段式方法名,前一级为SP名称,后一级为调用方法名
if(method && (method = method.split(".")).length ==2){
if(!Array.isArray(body)){
body = [body];
}
if(cb != "notify"){
if(cb instanceof Function){
body.push((function(sn,cb){
return function(){
var msg = {type:"callback",sn:sn};
var args = Array.prototype.slice.call(arguments,0);
msg.body = args;
cb(null,msg);
}
})(msg.sn,cb));
}else{
// break and send Error;
break;
}
}
sp = this.sps[method[0]];
if(sp){
sp[method[1]].apply(sp,body);
}
return;
}
//break and send error;
break;
}
cb(new Error(format("ignoer the message, [type:%s ]",msg.type)),null);
},
/**
* 添加子服务节点. 将代理一个节点的发布方法.
* @param url : {string|function}
* 远端节点的访问位置或访问方法.
* [http | https | tcp | ws]://host[:port][/path/]
* @param opts : {map}
* 连接配置信息
*/
addSubnode : function(url,opts){
},
// ------------ 以下统统为接入方法 ------------ //
joint:function(conn){
var me = this;
/**
* 这个方法将用于监听onconnect事件
*/
var client = new Client(conn,me);
// client.setWritable(conn);
// client.setServerPoint(me);
client.on("error",function(err){
// 致命错误..关闭连接.
log.error("error...\n %s" + err.stack);
conn.end(JSON.stringify({type:"error",body:err.stack}));
});
// websocket, xhr-stream and so on by sockjs
conn.on("data",function(originMsg){
client.receive(originMsg);
});
conn.on("close",function(){
log.info("dispost domain");
client.dispose();
});
}
}
module.exports = SPM;