前言
在早前就写过关于JSON-RPC的文章(NodeJS编写简单的JSON-RPC协议服务端类库)。之所以研究JSON-RPC是因为当时使用了aria2下载时,学习部署发现其web界面与服务是分离的,处于了解到了JSON-RPC。也是刚好那段时间,从习惯使用的Python的Flask框架往Nodejs框架学习转变。此后在自己编写的小项目中都会使用到JSON-RPC作为前后端的数据交互方式。
我相信很多小伙伴都接触与使用过REST风格的后端接口规范,这里需要注意的是REST与RPC是两种不同的思维方式。
RPC一般指远程过程调用,RPC是远程过程调用(Remote Procedure Call)的缩写形式,其主要思想是函数映射到API。
我喜欢使用JSON-RPC的原因一方面,平时喜欢用NodeJS的Koa框架快速实现个人小项目或简单的CRUD项目,项目不是前后端分离的,只是静态html,异步请求的方式,js的数据结构前后端是统一的。
而且实现JSON-RPC也非常简单,在编写webSocket项目中也非常实用。所以这里分享一下使用Koa2编写简单的JSON-RPC服务
代码实现
不到50行代码,实现服务端的JSONRPC类
我们根据JSON-RPC 2.0 规范就能非常快速实现JSON-RPC。
首先handle
函数来判断是否是批处理,接着在parse
函数中校验数据,校验成功后调用apply
传参并执行。
util/jsonrpc2.js
:
class JSONRPC {
constructor(methods, debug) {
this.VERSION = '2.0';//版本
this.errorMsg = {//错误字典
'-32700': 'Parse Error.',
'-32600': 'Invalid Request.',
'-32601': 'Method Not Found.',
'-32602': 'Invalid Params.',
'-32603': 'Internal Error.',
};
this.methods = Object.assign({}, methods);
this.debug = !!debug;//是否调试
}
validRequest(rpc) {//数据校验
return rpc.jsonrpc === this.VERSION /*版本校验*/
&& (typeof rpc.id === 'number' || typeof rpc.id === 'string')/*id校验*/
&& typeof rpc.method === 'string';/*函数名校验*/
}
normalize(rpc, obj) {//输出标准化
if (obj.error && !obj.error.message) obj.error.message = this.errorMsg[obj.error.code];
return Object.assign(obj, { jsonrpc: this.VERSION, id: rpc.id });
}
parse(rpc) {//协议解析
if (!this.validRequest(rpc))//请求协议不规范
return this.normalize(rpc, { error: { code: -32600 } });
let method = this.methods[rpc.method];
if (typeof method !== 'function')//函数匹配
return this.normalize(rpc, { error: { code: -32601 } });
let params = Array.isArray(rpc.params) ? rpc.params : [rpc.params];//参数解析
try {
return this.normalize(rpc, { result: method.apply(this.methods, params) });// 函数调用
} catch (err) {
return this.normalize(rpc, {//解析异常捕获
error: {
code: err.code || -32603,
message: this.debug ? "[debug] " + String(err) : this.errorMsg[-32603]//非调试情况不输出错误详情
}
});
}
}
handle(rpc) {//处理入库
return Array.isArray(rpc) ? rpc.map(r => this.parse(r)) : this.parse(rpc);//判断是否批处理
}
}
module.exports = JSONRPC
编写koa主服务
我主要使用koa-body来解析post请求参数
app.js
:
const Koa = require('koa');
const app = new Koa();
const json = require('koa-json');
const onerror = require('koa-onerror');
const staticServer = require('koa-static-server');
const bodyparser = require('koa-body');
const path = require('path');
const logger = require('koa-logger');
const config = require("./config");
const networkInterfaces = require('os').networkInterfaces(); //获取网卡信息
// 客户端异常响应
onerror(app);
// post请求参数解析与上传
app.use(bodyparser({
jsonLimit: '10mb',
multipart: true,
formidable: {
maxFileSize: 2000 * 1024 * 1024 // 设置上传文件大小最大限制,默认2M
},
onerror: function (err, ctx) {
ctx.throw('body parse error', 422);
}
}));
// 自动解析对象返回客户端为json
app.use(json());
// 开发中显示日志
app.use(logger());
//静态文件服务
app.use(staticServer({ rootDir: path.join(__dirname, 'www', 'static'), rootPath: '/static' }));
// 控制台 请求输出
app.use(async (ctx, next) => {
const start = new Date();
await next();
const ms = new Date() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
// 控制台 异常输出
app.on('error', (err, ctx) => {
console.error('server error', err, ctx);
});
app.use(require('./routes'));
app.listen(config.port, () => {
const ens = Object.keys(networkInterfaces)[0];
const address = networkInterfaces[ens][1].address || networkInterfaces[ens][0].address; // 获取内网ip
const notice = `open:
http://localhost:${config.port},
http://${address}:${config.port}
`
console.log(notice);
});
编写Koa路由
routes/index.js
:
const router = require('koa2-router')(); //引入路由函数
router.get('/', async (ctx, next) => {
ctx.redirect('/index')
})
router.get('/index', async (ctx, next) => {
ctx.response.redirect('/static/index.html');
})
router.post('/jsonrpc', require("./jsonrpc"))
module.exports = router
编写jsonrpc测试路由
默认情况下请求头是使用application/json
作为内容格式标识请求头。
在上传文件的时候通过请求头判断是否是文件上传,因为个人习惯使用multipart/form-data
请求头来上传文件,所以这里就用此请求头作为示例。
如果包含文件则将文件数据体放到调用的参数中。
需要注意的是在上传文件的时候是不支持批处理的请求的。
routes/jsonrpc.js
const Jsonrpc = require('../util/jsonrpc2');
const jsonrpc = new Jsonrpc({
test1(a) {
console.log(a);
return a;
},
test2(a, b) {
console.log(a + b);
return a + b;
},
test3(a) {
console.log(this);
return this;
},
test4: (o) => {
//异常模拟
return o / asdsad;
},
upload1() {
return arguments;
},
upload2(o) {
return o;
}
}, true);
module.exports = function (ctx) {
let data = ctx.request.body;
if (ctx.request.header["content-type"].indexOf("multipart/form-data") >= 0) {
params = JSON.parse(data.params);
if (Array.isArray(params)) params.push(ctx.request.files);
else if (params.constructor === Object) params.files = ctx.request.files;
else params = [params, ctx.request.files];
data.params = params;
}
ctx.body = jsonrpc.handle(data);
}
编写测试页面
根据请求协议可自定义封装JSONRPC的web客户端
www/static/iddex.html
:
最终测试效果
参考资料
- NodeJS编写简单的JSON-RPC协议服务端类库
- JSON-RPC 2.0 规范(中文版)