NodeJS编写简单的JSON-RPC协议服务端类库

JSON-RPC(remote protocol call)是一种以json为协议的远程调用服务。我之所以研究JSON-RPC也是因为发现平时常用的下载工具aria2与常用的aria2 GUI管理工具也是使用JSON-RPC进行数据交互的。所以在这里用NodeJS来编写编写简单的JSON-RPC协议服务端类库给大家参考。

JSON-RPC 相关文档:

  1. JSON-RPC介绍:https://baike.baidu.com/item/json%20rpc/13675431?fr=aladdin
  2. JSON-RPC 2.0 规范:https://www.jsonrpc.org/specification
  3. JSON-RPC 2.0 规范(中文版):http://wiki.geekdream.com/Specification/json-rpc_2.0.html

1.简述JSON-RPC协议

关于JSON-RPC协议的一些文档信息网上很多,我就不详细说明了。
这里就简单说明一下请求、响应与错误异常的情况。

1.1.请求对象

发送一个请求对象至服务端代表一个rpc调用, 一个请求对象包含下列成员:

属性 说明
jsonrpc 指定JSON-RPC协议版本的字符串,必须准确写为“2.0”
method 包含所要调用方法名称的字符串,以rpc开头的方法名,用英文句号(U+002E or ASCII 46)连接的为预留给rpc内部的方法名及扩展名,且不能在其他地方使用
params 调用方法所需要的结构化参数值,该成员参数可以被省略
id 已建立客户端的唯一标识id,值必须包含一个字符串、数值或NULL空值。如果不包含该成员则被认定为是一个通知。该值一般不为NULL[1],若为数值则不应该包含小数[2]

简单理解的话,请求的参数类似于:

{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}

1.2.响应对象

当发起一个rpc调用时,除通知之外,服务端都必须回复响应。响应表示为一个JSON对象,使用以下成员:

属性 说明
jsonrpc 指定JSON-RPC协议版本的字符串,必须准确写为“2.0”
result 该成员在成功时必须包含。当调用方法引起错误时必须不包含该成员。服务端中的被调用方法决定了该成员的值。
error 该成员在失败是必须包含。当没有引起错误的时必须不包含该成员。该成员参数值必须为5.1中定义的对象。
id 该成员必须包含。该成员值必须于请求对象中的id成员值一致。若在检查请求对象id时错误(例如参数错误或无效请求),则该值必须为空值。

结果类似于:

{"jsonrpc": "2.0", "result": 19, "id": 1} 

:响应对象必须包含result或error成员,但两个成员必须不能同时包含。

1.3.错误异常

当一个rpc调用遇到错误时,返回的响应对象必须包含错误成员参数,并且为带有下列成员参数的对象:

属性 说明
code 使用数值表示该异常的错误类型。 必须为整数。
message 对该错误的简单描述字符串。 该描述应尽量限定在简短的一句话。
data 包含关于错误附加信息的基本类型或结构化类型。该成员可忽略。 该成员值由服务端定义(例如详细的错误信息,嵌套的错误等)。

-32768至-32000为保留的预定义错误代码。在该范围内的错误代码不能被明确定义,保留下列以供将来使用。

code message meaning
-32700 Parse error语法解析错误 服务端接收到无效的json。该错误发送于服务器尝试解析json文本
-32600 Invalid Request无效请求 发送的json不是一个有效的请求对象。
-32601 Method not found找不到方法 该方法不存在或无效
-32602 Invalid params无效的参数 无效的方法参数。
-32603 Internal error内部错误 JSON-RPC内部错误。
-32000 to -32099 Server error服务端错误 预留用于自定义的服务器错误。

除此之外剩余的错误类型代码可供应用程序作为自定义错误。

2.NodeJS的JSON-RPC协议库编写

2.1.完整代码参考(注意看注释):

const JSONRPC = {
    VERSION: '2.0',
    errorMsg: {},
    methods: {}
};
/*
错误信息 参考:https://www.jsonrpc.org/specification#error_object
| code      |   message          | meaning |
| -32700    |   Parse error      | Invalid JSON was received by the server.An error occurred on the server while parsing the JSON text.  |
| -32600    |   Invalid Request  | The JSON sent is not a valid Request object.  |
| -32601    |   Method not found | The method does not exist / is not available.  |
| -32602    |   Invalid params   | Invalid method parameter(s).  |
| -32603    |   Internal error   | Internal JSON-RPC error.  |
| -32000 to -32099   |   Server error        | Reserved for implementation-defined server-errors.  |
*/
JSONRPC.errorMsg[-32700] = 'Parse Error.';
JSONRPC.errorMsg[-32600] = 'Invalid Request.';
JSONRPC.errorMsg[-32601] = 'Method Not Found.';
JSONRPC.errorMsg[-32602] = 'Invalid Params.';
JSONRPC.errorMsg[-32603] = 'Internal Error.';

/**
 * 检查给定的请求是否有效
 * JSONRPC调用
 * - "jsonrpc" 当前版本== ('2.0')
 * - "id" 请求标识,为数字
 * - "method" 指的是调用的方法名称的字符串
 * @param  {Object} rpc
 * @return {Boolean} 
 */
function validRequest(rpc) {
    return rpc.jsonrpc === JSONRPC.VERSION
        && (typeof rpc.id === 'number' || typeof rpc.id === 'string')
        && typeof rpc.method === 'string';
};

/**
 * 规范化响应对象
 * @param  {Object} rpc
 * @param  {Object} obj 
 * @return {Object} 
 */
function normalize(rpc, obj) {
    obj.id = (rpc && typeof rpc.id === 'number' ? rpc.id : null);
    obj.jsonrpc = JSONRPC.VERSION;
    //如果错误根据错误不存在错误信息的话代码获取错误信息
    if (obj.error && !obj.error.message) obj.error.message = JSONRPC.errorMsg[obj.error.code];
    return obj;
};

/**
 * JSONRPC 请求处理
 * @param  {Object} rpc 
 * @param  {Function} respond 响应回调
 */
JSONRPC.handleRequest = function (rpc, respond) {
    //版本与一些参数验证
    if (!validRequest(rpc)) return respond(normalize(rpc, { error: { code: -32600 } }));//请求协议不规范
    //函数查找
    let method = JSONRPC.methods[rpc.method];
    if (typeof method !== 'function') return respond(normalize(rpc, { error: { code: -32601 } }));// 函数或方法未找到
    //参数解析
    let params = [];
    // 未命名参数
    if (Array.isArray(rpc.params)) {
        params = rpc.params;
    } else if (typeof rpc.params === 'object') {
        //通过正则表达式过滤出参数名
        let names = method.toString().match(/\((.*?)\)/)[1].match(/[\w]+/g);
        if (names) {
            for (let i of names) params.push(rpc.params[i]);
        } else {
            //返回错误 不支持参数
            return respond(normalize(rpc, { error: { code: -32602, message: 'This service does not support named parameters.' } }));
        }
    }
    // 用给定的错误和结果进行响应
    let reply = function (err, result) {
        if (err) {
            if (typeof err === 'number') {
                respond(normalize(rpc, {
                    error: { code: err }
                }));
            } else {
                respond(normalize(rpc, {//解析异常捕获
                    error: {
                        code: err.code || -32603,
                        message: err.message
                    }
                }));
            }
        } else {
            respond(normalize(rpc, { result: result }));
        }
    };
   // 追加 reply 函数作为最后一个参数
    // params.push(reply);
    // 调用方法
    try {
        // reply(false, method.apply(this, params));
        reply(false, method.apply(this));
    } catch (err) {
        reply(err);
    }
}
module.exports = JSONRPC;

2.2.代码讲解

首先我们先定义好基本的JSONRPC对象,对象中包含版本信息(VERSION)、错误信息(errorMsg)、与一些函数。
代码中的handleRequest是核心处理部分。执行逻辑是先对传入的参数数据格式进行校验,然后解析参数中的函数名以及参数,最后执行函数且将执行结果通过回调的方式返回出去。

3.库的使用

这里的话我就以简单的post请求对前后端进行交互。

简单的服务端示例代码:

const http = require('http');
const fs = require('fs');
const url = require('url');
const RPC = require('./jsonrpc');
RPC.methods = {
    demo(t) {
        return "hello" + (t || "word");
    },
    asd(a) {
        console.log(a);
        return 999;
    },
    aaa(b, c) {
        return b + c;
    },
    eee: (a, v) => {
        return a * v;
    },
    ccc: (o) => {
        //异常模拟
        return o / asdsad;
    },
    //几种写法
    test(a, b, c, d, e) {
        return { a, b, c, d, e };
    },
    test2:function(a, b, c, d, e) {
        return { a, b, c, d, e };
    },
    test3:(a, b, c, d, e)=>{
        return { a, b, c, d, e };
    }
};
const jsonrpc = RPC.handleRequest;
const routes = {//<====路由
    "/jsonrpc"(req, res) {
        var contentType = req.headers['content-type'] || '';
        //接收post请求
        if (req.method === 'POST' && contentType.indexOf('application/json') >= 0) {
            var data = '';
            req.setEncoding('utf8');
            req.addListener('data', function (chunk) { data += chunk; });
            req.addListener('end', function () {
                //响应函数
                let respond = function (obj) {
                    var body = JSON.stringify(obj);
                    res.writeHead(200, {
                        'Content-Type': 'application/json',
                        'Content-Length': Buffer.byteLength(body)
                    });
                    res.end(body);
                };
                try {
                    var rpc = JSON.parse(data),//获取json数据
                        batch = Array.isArray(rpc);//<===是否批处理
                    if (batch) {
                        var responses = [],
                            len = rpc.length,
                            pending = len;
                        for (var i = 0; i < len; ++i) {
                            (function (rpc) {
                                jsonrpc(rpc, function (obj) {
                                    responses.push(obj);
                                    if (!--pending) {
                                        respond(responses);
                                    }
                                });
                            })(rpc[i]);
                        }
                    } else {
                        jsonrpc(rpc, respond);
                    }
                } catch (err) {
                    console.log(err);
                    return respond({ err: 1110 });
                }
            });
        } else {
            res.end();
        }
    },
    "/"(request, response) {
        response.writeHead(200, { 'Content-Type': 'text/html' });
        response.write(`服务已开启`);
        response.end();
    },
    "/404"(request, response) {
        response.writeHead(404, { 'Content-Type': 'text/html' });
        response.write("404");
        response.end();
    }
}
// 创建服务器
http.createServer(function (request, response) {
    // 解析请求,包括文件名
    var pathname = url.parse(request.url).pathname;
    // 输出请求的文件名
    console.log("Request for " + pathname + " received.");
    route = routes[pathname]
    if (route) {
        route(request, response);
    } else {
        routes["/404"](request, response);
    }
}).listen(8889); 

浏览器控制台测试脚本:

fetch('/jsonrpc', {
    headers: {
        "Content-Type": "application/json"
    },
    method: 'POST',
    body: JSON.stringify([
         { "jsonrpc": "2.0", "method": "test", "id": 1, "params": [1,2,3,4,5] },
        { "jsonrpc": "2.0", "method": "test2", "id": 2, "params": [1,2,3,4,5] },
        { "jsonrpc": "2.0", "method": "test3", "id": 3, "params":  [1,2,3,4,5] },
        { "jsonrpc": "2.0", "method": "test", "id": 4, "params": {a:1,b:2,c:3,d:4,e:5} },
        { "jsonrpc": "2.0", "method": "test2", "id": 5, "params": {a:1,b:2,c:3,d:4,e:5} },
        { "jsonrpc": "2.0", "method": "test3", "id": 6, "params": {a:1,b:2,c:3,d:4,e:5} },
    ])
}).then(v => {
    return v.json();
}).then(v => {
    console.log(v)
});

4.测试

params为数组
params为对象
请求结果返回错误示例
批量请求

本文代码放在:https://gitee.com/baojuhua/JS-snippets/tree/master/NodeJS/JOSNRPC

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249