This module is the actual serialization format and protocol for the remoteagent system.
It is transport agnostic so that it can work on any duplex socket. A modified
version of msgpack is used as the serialization format when using the binary
socket transport (undefined
and Buffer
types are added). Other transports
such as socket.io in the browser can be used.
Encoded on top of the serialzation format is functions and cycles. This
almost any basic value can be encoded and sent across the socket.
Since functions can be serialized, rpc using callbacks is natural. Simply
pass your callback as an argument and the other side will get a proxy function
when it's deserialized. When they call that proxy function, a message will be
sent back and your callback will get called with the deserialized arguments
(which can include yet another callback). Since the callbacks are executed on
their native side, closure variables and all other state is preserved.
Callbacks functions may only be called once. The internal reference is
deleted after the first call. For functions that are called multiple times,
use the named functions map passed into the Agent.
Message Encoding
On top of serializing primitive values and basic data structures, remoteagent-
protocol can encode proxy functions and cycles in an object. This is encoded
using objects with the $
magic key. Any existing keys that start with $
will be escaped by prefixing an extra $.
Function Encoding
Functions are encoded with the $
key. The value of this object is the
unique function index in the local function repository. Function keys
integers. An example encoded function can look like {$: 3}
where
callbacks[3]
in the server is the real function.
Cycle Encoding
Sometimes objects have cycles in them. It would be nice if these could be
encoded, serialized, and send to the other side intact without blowing up the
rpc system. Cycles are encoded with the $
key. The value is the path to
the actual value as an array of strings. In this way it works like a file-
system symlink. Currently the path is absolute starting at the root of the
message. For example. Given the following cyclic object:
var entry = {
name: "Bob",
boss: { name: "Steve" }
};
entry.self = entry;
entry.manager = entry.boss;
The following encoded object is generated by the internal freeze
function in
protocol.serializer()
.
{
name: 'Bob',
boss: { name: 'Steve' },
self: { $: [] },
manager: { $: [ 'boss' ] }
}
See that the path []
point to the object itself, and ['boss']
points to
the boss property in the root.
Agents
The main public interface is the Agent class. There is typically one of these per network node (process) in a remoteagent mesh. The agent holds the named functions that this node serves to the other nodes.
var Agent = require('architect-agent').Agent;
var agent = new Agent({
add: function (a, b, callback) {
callback(a + b);
}
});
In this example, we created a new agent, gave it the ID "main"
and declared that it provides an add function.
Transports
Transports handle internally the serialization of static objects. This can be
JSON or msgpack or something else.
Transports must have a .send()
property for sending messages to the other
side and emit "message"
events (or call their onMessage
property) whenever a message arrives from the other
side. The message is an object not a json string or msgpack buffer. The
transport is not responsible for encoding functions and cycles, that is done
at a higher level.
When using the built-in socket-transport, messages are framed in the stream
using a 4 byte length header (UInt32BE) before every message. This way the
receiving end knows how much buffer to allocate and can efficiently scan and
deframe the incoming message stream. This also means that the msgpack parser
can assume it has the entire message in memory once the message emit from the
deframer.
Here is an example networkserver that accepts remoteagent connections over a
tcp port. We are assuming the agent
variable declared in the sample above.
var net = require('net');
var socketTransport = require('architect-socket-transport');
net.createServer(function (socket) {
agent.attach(socketTransport(socket), function (client) {
});
}).listen(1337);
Then to connect to this, from the client connect to this server.
var net = require('net');
var socketTransport = require('architect-socket-transport');
var Agent = require('architect-agent').Agent;
var agent = new Agent();
var client = net.connect(1337, function () {
agent.attach(socketTransport(client), function (server) {
server.add(1, 2, function (result) {
});
});
});