commit ed5a11b36602384c68cab6fe2ab1ad6a65b24943
Author: James Halliday
Date: Sun Jul 11 06:51:59 2010 +0000

Introducing DNode, a node.js library for asynchronous bidirectional remote method invocation. Network socket and websocket-style socket.io transports are presently available so system processes can communicate with each other and with processes running in the browser using the same interface.

Remote method invocation (RMI) is the object-oriented cousin of remote procedure calls. In RMI, each side of the connection hosts an object that the other side can call the methods of. A popular example of RMI worth checking out is Ruby's DRb.

DNode is different from DRb in that all remote method calls are asynchronous. Instead of returning explicitly, hosted methods pass return values to the other side of the connection by invoking callbacks that were passed in as arguments. These callbacks execute on the side of the connection where they were defined, and a representation of them gets passed to the remote, so there's no eval().

Here's a simple example.

// server:
var DNode = require('dnode');

var server = DNode({
    timesTen : function (n,f) { f(n * 10) },
}).listen(6060);


// client:
var DNode = require('dnode');
var sys = require('sys');

DNode.connect(6060, function (remote) {
    remote.timesTen(5, function (result) {
        sys.puts(result); // 5 * 10 == 50
    });
});

Unlike many asynchronous RPC systems, DNode allows the programmer to pass functions as arguments to remote methods, which when called on the remote end signal the near side to execute the function with the arguments that the remote provides. Callbacks are automatically scrubbed and collected from a recursive walk of the arguments list, so they can be arbitrarily nested.

Additionally, each side of the connection can call the other side's methods or the callbacks the other side provides with callbacks of its own or any arguments that can be serialized to JSON.

Bidirectional Example

Here's an example where the client calls a method on the server that calls a method on the client.

// server:
var DNode = require('dnode');

DNode(function (client) {
    this.timesX = function (n,f) {
        client.x(function (x) {
            f(n * x);
        });
    }; 
}).listen(6060);


// client:
var DNode = require('dnode');

DNode({
    x : function (f) { f(20) }
}).connect(6060, function (remote) {
    remote.timesX(3, function (res) {
        sys.puts(res); // 20 * 3 == 60
    });
});

DNode Browser Example

As I mentioned in the first paragraph, websocket-style connections between the browser and node.js DNode servers are available thanks to socket.io. The following example should work in Chrome, Firefox, Opera, and IE 5.5+. See the browser compatibility page for more info.

In this example, the client code runs on the browser and requests the server's timesTen method. The result of the timesTen call is put into the first span element. The client also calls the server's whoAmI method. The server calls the client's name method and performs some regular expression substitions on the result it gets before sending the result back to the client through a callback. The client's name with the substitions applied shows up in the second span element on the page.

Here's an example of what the browser-side code looks like:

<script type="text/javascript" src="/dnode.js"></script>
<script type="text/javascript">
    DNode({
        name : function (f) { f('Mr. Spock') }
    }).connect(function (remote) {
        remote.timesTen(10, function (n) {
            document.getElementById("result").innerHTML = String(n);
        });
        remote.whoAmI(function (name) {
            document.getElementById("name").innerHTML = name;
        });
    });
</script>

<p>timesTen(10) == <span id="result">?</span></p>
<p>My name is <span id="name">?</span>.</p>

Here's the server-side compliment to the browser-side code above:

#!/usr/bin/env node

var DNode = require('dnode');
var sys = require('sys');
var fs = require('fs');
var http = require('http');

var html = fs.readFileSync(__dirname + '/web.html');
var js = require('dnode/web').source();

var httpServer = http.createServer(function (req,res) {
    if (req.url == '/dnode.js') {
        res.writeHead(200, { 'Content-Type' : 'text/javascript' });
        res.end(js);
    }
    else {
        res.writeHead(200, { 'Content-Type' : 'text/html' });
        res.end(html);
    }
});
httpServer.listen(6061);

DNode(function (client) {
    this.timesTen = function (n,f) { f(n * 10) };
    this.whoAmI = function (reply) {
        client.name(function (name) {
            reply(name
                .replace(/Mr\.?/,'Mister')
                .replace(/Ms\.?/,'Miss')
                .replace(/Mrs\.?/,'Misses')
            );
        })
    };
}).listen({
    protocol : 'socket.io',
    server : httpServer,
    transports : 'websocket xhr-multipart xhr-polling htmlfile'.split(/\s+/),
}).listen(6060);

Installation

DNode is available on npm, a package manager for node.js libraries. You can install dnode by typing:

npm install dnode

Or you can check out the repository and link your development copy with npm:

git clone http://github.com/substack/dnode.git cd dnode npm link .

DNode depends on Socket.IO-node, bufferlist and traverse, all of which are available on npm and are automatically installed when you type npm install dnode.

You can fork and follow DNode on github.

And another thing

I've documented the DNode protocol on the README to make it easier to implement the DNode protocol in other languages. Stay tuned.

Special thanks goes out to pkrumins for getting the DNode browser code to work in Internet Explorer.

Thanks for reading. Enjoy this complimentary robot free of charge:

message-passing robot

more
git clone http://substack.net/blog.git