├── README.md ├── examples ├── echo.js ├── echo_client.js └── notify_client.js ├── lib └── msgpack-rpc.js ├── package.json └── test ├── errors.test.js ├── session.test.js ├── simple_server_client.test.js ├── timeout.test.js └── unix_socket.test.js /README.md: -------------------------------------------------------------------------------- 1 | node-msgpack-rpc 2 | ================ 3 | 4 | node-msgpack-rpc is an implementation of the [Msgpack-RPC](http://redmine.msgpack.org/projects/msgpack/wiki/RPCDesign) protocol specification for node.js. Msgpack-RPC is built ontop of the very fast [MessagePack](http://msgpack.org) serialization format. This implementation supports tcp and unix socket transports (it may one day support UDP). 5 | 6 | Simple Usage 7 | ------------ 8 | 9 | The easiest way to create a server is with a handler object. All incoming calls will be invoked on the handler object: 10 | 11 | var handler = { 12 | 'add' : function(a, b, response) { 13 | response.result( a + b ); 14 | } 15 | } 16 | 17 | var rpc = require('msgpack-rpc'); 18 | rpc.createServer(); 19 | rpc.setHandler(handler); 20 | rpc.listen(8000); 21 | 22 | a corresponding client might look like: 23 | 24 | var c = rpc.createClient(8000, '127.0.0.1', function() { 25 | c.invoke('add', 5, 4, function(err, response) { 26 | assert.equal(9, response); 27 | c.close(); 28 | } 29 | }); 30 | 31 | 32 | Without a handler 33 | ----------------- 34 | 35 | rpc.createServer(function(rpc_stream) { 36 | rpc_stream.on('request', function(method, params, response) { 37 | if(method == 'add') { 38 | response.result( params[0] + params[1] ); 39 | } else { 40 | response.error("unknown method!"); 41 | } 42 | } 43 | 44 | rpc_stream.on('notify', function(method, params) { 45 | console.log("recieved notification: " + method); 46 | }); 47 | }); 48 | rpc.listen(8000); 49 | 50 | Session Pool 51 | ------------ 52 | 53 | This module also provides a session pool which allows you to re-use client connections: 54 | 55 | var sp = new SesssionPool(); 56 | sp.getClient(8000, '127.0.0.1').invoke('hello','world', function(err, response) { ... });; 57 | sp.getClient(8001, '127.0.0.1').invoke('hello','world', function(err, response) { ... });; 58 | 59 | // Uses same tcp connection as above 60 | sp.getClient(8000, '127.0.0.1').invoke('goodbye','world', function(err, response) { ... });; 61 | 62 | sp.closeClients(); 63 | 64 | Installation 65 | ------------ 66 | 67 | First you will need to install the [node-msgpack](http://github.com/pgriess/node-msgpack) add-on 68 | 69 | To install node-msgpack-rpc with npm: 70 | 71 | git clone http://github.com/bpot/node-msgpack-rpc/ 72 | cd node-msgpack-rpc 73 | npm link . 74 | 75 | 76 | RPC Stream API 77 | -------------- 78 | 79 | Clients and the streams passed to servers for incoming connections are both instances of MsgpackRPCStream. 80 | 81 | Methods 82 | 83 | c.createClient(port, [hostname], [ready_cb]); 84 | c.invoke(method, [param1, param2, ...], cb); 85 | c.notify(method, [param1, param2, ...]); 86 | c.setTimeout(milliseconds); // Setting this will cause requests to fail with err "timeout" if they don't recieve a response for the specified period 87 | c.close(); // Close the socket for this client 88 | c.stream // underlying net.Stream object 89 | 90 | Events 91 | 92 | 'ready' // emitted when we've connected to the server 93 | 'request' // recieved request 94 | 'notify' // recieved notification 95 | 96 | 97 | TODO 98 | ---- 99 | * UDP Support? 100 | 101 | -------------------------------------------------------------------------------- /examples/echo.js: -------------------------------------------------------------------------------- 1 | var rpc = require('msgpack-rpc'); 2 | 3 | var handler = { 4 | 'n' : 0, 5 | 'echo' : function(data, response) { 6 | response.result(data); 7 | }, 8 | 'hello' : function(data) { 9 | this.n += 1; 10 | if(this.n % 100000 == 0) { 11 | console.log(this.n) 12 | } 13 | } 14 | } 15 | 16 | var server = rpc.createServer(); 17 | server.setHandler(handler); 18 | server.listen(8000); 19 | -------------------------------------------------------------------------------- /examples/echo_client.js: -------------------------------------------------------------------------------- 1 | var rpc = require('msgpack-rpc'); 2 | var assert = require('assert'); 3 | 4 | var client = rpc.createClient(8000); 5 | client.on('ready', function() { 6 | var count = 0; 7 | setInterval(function() { 8 | var b = "asdkjfhksjadfhskdjflksdjlfewiurowieurowieuroi"; 9 | for(var i = 0;i < 10000;i++) { 10 | client.invoke('echo', b, function(err, response) { 11 | if(!err) { 12 | count += 1; 13 | var the_buf = response; 14 | assert.equal(b.length, the_buf.length); 15 | } else { 16 | console.log("call failed"); 17 | } 18 | }); 19 | } 20 | }, 500); 21 | }); 22 | -------------------------------------------------------------------------------- /examples/notify_client.js: -------------------------------------------------------------------------------- 1 | var rpc = require('msgpack-rpc'); 2 | var assert = require('assert'); 3 | 4 | var client = rpc.createClient(8000); 5 | client.on('ready', function() { 6 | var count = 0; 7 | setInterval(function() { 8 | for(var i = 0;i < 10000;i++) { 9 | client.notify('hello', "world") 10 | } 11 | }, 500); 12 | }); 13 | -------------------------------------------------------------------------------- /lib/msgpack-rpc.js: -------------------------------------------------------------------------------- 1 | var net = require('net'), 2 | msgpack = require('msgpack'), 3 | events = require('events'), 4 | sys = require('sys'); 5 | 6 | var REQUEST = 0; 7 | var RESPONSE = 1; 8 | var NOTIFY = 2; 9 | var MAX_SEQID = Math.pow(2,32)-1; 10 | 11 | function RPCResponse (stream, seqid) { 12 | this.stream = stream; 13 | this.seqid = seqid; 14 | } 15 | 16 | RPCResponse.prototype.result = function(args) { 17 | this.stream.respond(this.seqid, null, args); 18 | } 19 | 20 | RPCResponse.prototype.error = function(error) { 21 | this.stream.respond(this.seqid, error, null); 22 | } 23 | 24 | // The heart of the beast, used for both server and client 25 | var MsgpackRPCStream = function(stream, handler) { 26 | events.EventEmitter.call(this); 27 | var self = this; 28 | this.last_seqid = undefined; 29 | this.stream = stream; 30 | this.handler = handler; 31 | this.cbs = []; 32 | this.timeout = undefined; 33 | 34 | this.msgpack_stream = new msgpack.Stream(this.stream); 35 | this.msgpack_stream.on('msg', function(msg) { 36 | var type = msg.shift(); 37 | switch(type) { 38 | case REQUEST: 39 | var seqid = msg[0]; 40 | var method = msg[1]; 41 | var params = msg[2]; 42 | var response = new RPCResponse(self, seqid); 43 | 44 | self.invokeHandler(method, params.concat(response)); 45 | self.emit('request', method, params, response); 46 | break; 47 | case RESPONSE: 48 | var seqid = msg[0]; 49 | var error = msg[1]; 50 | var result = msg[2]; 51 | 52 | if(self.cbs[seqid]) { 53 | self.triggerCb(seqid, [error, result]); 54 | } else { 55 | self.emit('error', new Error("unexpected response with unrecognized seqid (" + seqid + ")")) 56 | } 57 | break; 58 | case NOTIFY: 59 | var method = msg[0]; 60 | var params = msg[1]; 61 | 62 | self.invokeHandler(method, params); 63 | self.emit('notify', method, params); 64 | break; 65 | } 66 | }); 67 | this.stream.on('connect', function() { self.emit('ready'); }); 68 | 69 | // Failures 70 | this.stream.on('end', function() { self.stream.end(); self.failCbs(new Error("connection closed by peer")); }); 71 | this.stream.on('timeout', function() { self.failCbs(new Error("connection timeout")); }); 72 | this.stream.on('error', function(error) { self.failCbs(error); }); 73 | this.stream.on('close', function(had_error) { 74 | if(had_error) return; 75 | self.failCbs(new Error("connection closed locally")); 76 | }); 77 | } 78 | 79 | sys.inherits(MsgpackRPCStream, events.EventEmitter); 80 | 81 | MsgpackRPCStream.prototype.triggerCb = function(seqid, args) { 82 | this.cbs[seqid].apply(this, args); 83 | delete this.cbs[seqid]; 84 | } 85 | 86 | MsgpackRPCStream.prototype.failCbs = function(error) { 87 | for(var seqid in this.cbs) { 88 | this.triggerCb(seqid, [error]) 89 | } 90 | } 91 | 92 | MsgpackRPCStream.prototype.invokeHandler = function(method, params) { 93 | if(this.handler) { 94 | if(this.handler[method]) { 95 | this.handler[method].apply(this.handler, params); 96 | } else { 97 | response.error(new Error("unknown method")); 98 | } 99 | } 100 | } 101 | 102 | MsgpackRPCStream.prototype.nextSeqId = function() { 103 | if(this.last_seqid == undefined) { 104 | return this.last_seqid = 0; 105 | } else if(this.last_seqid > MAX_SEQID ) { 106 | return this.last_seqid = 0; 107 | } else { 108 | return this.last_seqid += 1; 109 | } 110 | } 111 | 112 | MsgpackRPCStream.prototype.invoke = function() { 113 | var self = this; 114 | var seqid = this.nextSeqId(); 115 | var method = arguments[0]; 116 | var cb = arguments[arguments.length - 1]; 117 | var args = []; 118 | for(var i = 1;i < arguments.length - 1;i++) { 119 | args.push(arguments[i]); 120 | } 121 | 122 | this.cbs[seqid] = cb; 123 | if(this.timeout) { 124 | setTimeout(function() { if(self.cbs[seqid]) self.triggerCb(seqid, ["timeout"]); }, this.timeout); 125 | } 126 | if(this.stream.writable) { return this.msgpack_stream.send([REQUEST, seqid, method, args]) }; 127 | } 128 | 129 | MsgpackRPCStream.prototype.respond = function(seqid, error, result) { 130 | if(this.stream.writable) { return this.msgpack_stream.send([RESPONSE, seqid, error, result]) }; 131 | } 132 | 133 | MsgpackRPCStream.prototype.notify = function(method, params) { 134 | var method = arguments[0]; 135 | var args = []; 136 | for(var i = 1;i < arguments.length;i++) { 137 | args.push(arguments[i]); 138 | } 139 | 140 | if(this.stream.writable) { return this.msgpack_stream.send([NOTIFY, method, args]) }; 141 | } 142 | 143 | MsgpackRPCStream.prototype.setTimeout = function(timeout) { 144 | this.timeout = timeout; 145 | } 146 | 147 | MsgpackRPCStream.prototype.close = function() { 148 | this.stream.end(); 149 | } 150 | 151 | exports.createClient = function(port, hostname,cb) { 152 | var s = new MsgpackRPCStream(new net.createConnection(port, hostname)); 153 | if(typeof hostname == 'function') s.on('ready', hostname); 154 | if(cb) s.on('ready', cb); 155 | 156 | return s; 157 | } 158 | 159 | var Server = function(listener) { 160 | net.Server.call(this); 161 | var self = this; 162 | this.handler = undefined; 163 | 164 | this.on('connection', function(stream) { 165 | stream.on('end', function() { stream.end(); }); 166 | var rpc_stream = new MsgpackRPCStream(stream, self.handler); 167 | if(listener) listener(rpc_stream); 168 | }); 169 | } 170 | sys.inherits(Server, net.Server); 171 | 172 | Server.prototype.setHandler = function(handler) { 173 | this.handler = handler; 174 | } 175 | 176 | exports.createServer = function(handler) { 177 | return new Server(handler); 178 | } 179 | 180 | var SessionPool = exports.SessionPool = function() { 181 | this.clients = {}; 182 | }; 183 | 184 | SessionPool.prototype.getClient = function(port, hostname) { 185 | var address = hostname + ":" + port; 186 | if(this.clients[address]) { 187 | return this.clients[address]; 188 | } else { 189 | return this.clients[address] = exports.createClient(port, hostname); 190 | } 191 | }; 192 | 193 | SessionPool.prototype.closeClients = function() { 194 | for(var i in this.clients) this.clients[i].stream.end(); 195 | }; 196 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | 'name' : 'msgpack-rpc', 3 | 'version' : '0.0.1', 4 | 'description' : 'node-msgpack-rpc is an implementation of the Msgpack-RPC protocol specification for node.js', 5 | 'homepage' : 'https://github.com/bpot/node-msgpack-rpc', 6 | 'main' : './lib/msgpack-rpc', 7 | 'repository' : { 8 | "type" : "git", 9 | "url" : "https://github.com/bpot/node-msgpack-rpc.git" 10 | }, 11 | 'directories': { 12 | "lib": "lib" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/errors.test.js: -------------------------------------------------------------------------------- 1 | var rpc = require('msgpack-rpc'); 2 | 3 | module.exports = { 4 | 'outstanding request should be failed on transport errors' : function(assert, beforeExit) { 5 | var failure_triggered = false; 6 | var s = rpc.createServer(function(stream) { 7 | stream.on('request', function() { 8 | stream.stream.end(); 9 | s.close(); 10 | }); 11 | }); 12 | 13 | s.listen(8000, function() { 14 | var c = rpc.createClient(8000, function() { 15 | c.invoke("hello", "world", function(err, response) { 16 | failure_triggered = true; 17 | assert.equal("connection closed by peer", err.message); 18 | }); 19 | }); 20 | }); 21 | 22 | beforeExit(function() { 23 | assert.ok(failure_triggered); 24 | }); 25 | }, 26 | 'server side failures should be passed to the response method' : function(assert, beforeExit) { 27 | var failure_triggered = false; 28 | var s = rpc.createServer(function(stream) { 29 | stream.on('request', function(method, arg, responseHandler) { 30 | responseHandler.error("can't hello"); 31 | }); 32 | }); 33 | 34 | s.listen(8001, function() { 35 | var c = rpc.createClient(8001, function() { 36 | c.invoke("hello", "world", function(err, response) { 37 | failure_triggered = true; 38 | c.close(); 39 | s.close(); 40 | assert.equal("can't hello", err); 41 | }); 42 | }); 43 | }); 44 | 45 | beforeExit(function() { 46 | assert.ok(failure_triggered); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/session.test.js: -------------------------------------------------------------------------------- 1 | var rpc = require('msgpack-rpc'); 2 | 3 | module.exports = { 4 | 'should reuse clients' : function(assert, beforeExit) { 5 | var sp = new rpc.SessionPool(); 6 | var server_listened = false; 7 | 8 | var s = rpc.createServer(); 9 | s.listen(8010, function() { 10 | var client1 = sp.getClient(8010, '127.0.0.1'); 11 | var client2 = sp.getClient(8010, '127.0.0.1'); 12 | assert.equal(client1,client2); 13 | server_listened = true; 14 | s.close(); 15 | }); 16 | 17 | beforeExit(function() { 18 | assert.ok(server_listened); 19 | }); 20 | }, 21 | 'closeClients() should close connections' : function(assert, beforeExit) { 22 | var sp = new rpc.SessionPool(); 23 | 24 | var server_listened = false; 25 | var stream_ended = false; 26 | var s = rpc.createServer(function(rpc_stream) { 27 | rpc_stream.stream.on('close', function() { 28 | stream_ended = true; 29 | s.close(); 30 | }); 31 | }); 32 | 33 | s.listen(9000, function() { 34 | server_listened = true; 35 | var client1 = sp.getClient(9000, '127.0.0.1'); 36 | client1.on('ready', function() { 37 | sp.closeClients(); 38 | }); 39 | }); 40 | 41 | beforeExit(function() { 42 | assert.ok(server_listened); 43 | assert.ok(stream_ended); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/simple_server_client.test.js: -------------------------------------------------------------------------------- 1 | var msgpack_rpc = require('msgpack-rpc'); 2 | module.exports = { 3 | 'simple server handler' : function(assert, beforeExit) { 4 | var addition_called = false; 5 | var response_received = false; 6 | var notification_recieved = false; 7 | var handler = { 8 | 'add' : function(a, b, response) { 9 | addition_called = true; 10 | response.result(a + b); 11 | }, 12 | 'temperature' : function(temp, response) { 13 | assert.equal(102.1, temp); 14 | assert.equal(undefined, response); 15 | notification_received = true; 16 | } 17 | }; 18 | 19 | var server = msgpack_rpc.createServer(); 20 | server.setHandler(handler); 21 | 22 | server.listen(8030, function() { 23 | var client = msgpack_rpc.createClient(8030,function() { 24 | client.invoke('add', 5, 7, function(err, response) { 25 | response_received = true; 26 | assert.equal(5 + 7, response); 27 | server.close(); 28 | client.stream.end(); 29 | }); 30 | 31 | client.notify('temperature', 102.1); 32 | }); 33 | 34 | }); 35 | 36 | beforeExit(function() { 37 | assert.ok(addition_called); 38 | assert.ok(response_received); 39 | assert.ok(notification_received); 40 | }); 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /test/timeout.test.js: -------------------------------------------------------------------------------- 1 | var rpc = require('msgpack-rpc'); 2 | 3 | module.exports = { 4 | 'trigger timeout' : function(assert, beforeExit) { 5 | var cb_triggered = false; 6 | var s = new rpc.createServer(); 7 | s.listen(8020, function() { 8 | var c = rpc.createClient(8020); 9 | c.setTimeout(3000); 10 | c.invoke("hello", "world", function(err, response) { 11 | cb_triggered = true; 12 | c.close(); 13 | s.close(); 14 | assert.equal("timeout", err); 15 | }); 16 | }); 17 | 18 | beforeExit(function() { 19 | assert.ok(cb_triggered); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/unix_socket.test.js: -------------------------------------------------------------------------------- 1 | var rpc = require('msgpack-rpc'); 2 | 3 | module.exports = { 4 | 'should work over unix sockets' : function(assert, beforeExit) { 5 | var received_response = false; 6 | var s = rpc.createServer(function(stream) { 7 | stream.on('request', function(method, params, response) { 8 | response.result("sup"); 9 | }); 10 | }); 11 | s.listen("/tmp/node.msgpack.rpc.sock", function() { 12 | 13 | var c = rpc.createClient("/tmp/node.msgpack.rpc.sock", function() { 14 | c.invoke("hello", "world", function(err, response) { 15 | received_response = true; 16 | assert.equal("sup", response); 17 | c.close(); 18 | s.close(); 19 | }); 20 | }); 21 | }); 22 | 23 | beforeExit(function() { 24 | assert.ok(received_response); 25 | }); 26 | } 27 | } 28 | --------------------------------------------------------------------------------