├── README.md ├── examples ├── loopback.js ├── receiver.js └── sender.js ├── lib ├── hasher.js └── multicast-eventemitter.js ├── multicast-eventemitter-test.js ├── package.json └── test └── hasher-test.js /README.md: -------------------------------------------------------------------------------- 1 | multicast-eventemitter 2 | ---------------------- 3 | 4 | Status: FIXED, now works with NodeJS v0.8.x and v0.10.x. 5 | 6 | This package provides a cluster-wide event emitter. Events sent from any process on any machine on a LAN can be subscribed to by any other process on any other machine. 7 | 8 | Multicast is more efficient than broadcast. If messages were broadcast, then each subscriber would need to discard the the events it wasnt't interested in. With multicast, this is done by the NIC. 9 | 10 | It works efficiently by hashing event names into 24 bits of address space and 15 bits of port number - thus a 1 in half-a-trillion chance of event name collision. 11 | 12 | This currently uses plain multicast. This has the following drawbacks: 13 | 14 | 1. Messages are limited to about 1.5KB in length - exceeding this *may* produce a parsing error on receivers. 15 | 16 | 2. It is potentially unreliable (though we receive 100% of sent messages on our LAN - YMMV) 17 | 18 | -------------------------------------------------------------------------------- /examples/loopback.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Thorcom Systems Ltd. All Rights Reserved. 2 | 3 | var mee = require('../lib/multicast-eventemitter'); 4 | 5 | var emitter = mee.getEmitter(); 6 | 7 | // emit a packet everu second, 8 | setInterval(function() { 9 | var now = new Date().getTime(); 10 | console.log('emitting eventA', now); 11 | emitter.emit('eventA', 'this is eventA', now); 12 | console.log('emitting eventB', now); 13 | emitter.emit('eventB', 'this is eventB', now); 14 | }, 1000); 15 | 16 | // subscribe to eventB events 17 | emitter.on('eventB', function(text, time) { 18 | console.log('eventB received...', 'text:', text, 'time:', time); 19 | }); 20 | 21 | -------------------------------------------------------------------------------- /examples/receiver.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Thorcom Systems Ltd. All Rights Reserved. 2 | 3 | var mee = require('../lib/multicast-eventemitter'); 4 | 5 | var emitter = mee.getEmitter(); 6 | 7 | // subscribe to eventA events 8 | emitter.on('eventA', function(text, time) { 9 | console.log('eventA received...', 'text:', text, 'time:', time); 10 | }); 11 | -------------------------------------------------------------------------------- /examples/sender.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Thorcom Systems Ltd. All Rights Reserved. 2 | 3 | var mee = require('../lib/multicast-eventemitter'); 4 | 5 | var emitter = mee.getEmitter(); 6 | 7 | // emit a packet everu second, 8 | setInterval(function() { 9 | console.log('emitting eventA'); 10 | emitter.emit('eventA', 'this is eventA', new Date().getTime()); 11 | console.log('emitting eventB'); 12 | emitter.emit('eventB', 'this is eventB', new Date().getTime()); 13 | }, 1000); 14 | -------------------------------------------------------------------------------- /lib/hasher.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Thorcom Systems Ltd. All Rights Reserved. 2 | 3 | var crypto = require('crypto'); 4 | 5 | var options = exports.options = { 6 | firstOctet: '237' 7 | , portBase: 1024 8 | , hashAlgorithm: 'md5' 9 | } 10 | 11 | exports.hash = function hash(text) { 12 | var hash = crypto.createHash(options.hashAlgorithm); 13 | hash.update(text); 14 | var bytes = hash.digest('binary'); 15 | var addressParts = [options.firstOctet, bytes.charCodeAt(0), bytes.charCodeAt(1), bytes.charCodeAt(2)]; 16 | // bodge any zeros or 255s just in case 17 | for (var i in addressParts) { 18 | if (addressParts[i] > 254) { addressParts[i] == 254; } 19 | if (addressParts[i] < 1) { addressParts[i] == 1; } 20 | } 21 | 22 | var port = options.portBase + bytes.charCodeAt(3) + 256 * (bytes.charCodeAt(4) % 128); 23 | return { address: addressParts.join('.'), port: port }; 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/multicast-eventemitter.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Thorcom Systems Ltd. All Rights Reserved. 2 | 3 | var VERSION_0_8_X = "v0.8.x"; 4 | var VERSION_0_10_X = "v0.10.x"; 5 | var version = nodeVersion(); 6 | 7 | /* 8 | * This is required because they changed the way that bind works between 0.8.x and 0.10.x 9 | * I have not yet found a way of writing code which works on both, without this switch. 10 | */ 11 | function nodeVersion() { 12 | if (process.version.match(/^v0\.8\./)) { 13 | return VERSION_0_8_X; 14 | } else if (process.version.match(/^v0\.10\./)) { 15 | return VERSION_0_10_X; 16 | } else { 17 | throw 'multicast-eventemitter can only work with NodeJS v0.8.x and v0.10.x'; 18 | } 19 | } 20 | 21 | var dgram = require('dgram') 22 | , util = require('util') 23 | , hasher = require('./hasher') 24 | ; 25 | 26 | 27 | var options = exports.options = { 28 | 'transport': 'multicast' // 'pgm' (zeromq) to follow... 29 | , 'multicastInterface': undefined 30 | , 'ttl': 64 31 | , 'overrides' : {} // should be of form "event_name: { address: 'address', port: port }" 32 | } 33 | 34 | var emitter; // singleton 35 | 36 | 37 | /* 38 | * A function to get or create the singleton emitter. 39 | */ 40 | exports.getEmitter = getEmitter; 41 | function getEmitter() { 42 | if (!emitter) { 43 | emitter = new MulticastEventEmitter(); 44 | } 45 | return emitter; 46 | } 47 | 48 | /* 49 | * The constructor. Perhaps this shouldn't be exported? 50 | */ 51 | exports.MulticastEventEmitter = MulticastEventEmitter; 52 | function MulticastEventEmitter() { 53 | this.src = getIPv4Address() + '/' + process.pid; 54 | this.listenersByEvent = {}; // a hash of arrays of functions 55 | this.serversByEvent = {}; // a hash of bound datagram rx servers 56 | this.lastSeq = {}; // a hash of last heard sequence number, by src and event 57 | this.seqByEvent = {}; // a hash of last sent sequence numbers, by event 58 | this.sender = dgram.createSocket('udp4'); 59 | var that = this; 60 | this.sender.bind(function() { 61 | that.sender.setBroadcast(true); 62 | that.sender.setMulticastTTL(options.ttl); 63 | that.sender.setMulticastLoopback(true); // needed from inter-process intra-box comms 64 | }); 65 | } 66 | 67 | /* 68 | * This adds a listener. 69 | */ 70 | MulticastEventEmitter.prototype.addListener = function(event, listener) { 71 | //console.log('addListener', event, listener); 72 | if (!this.listenersByEvent[event]) { // make a new multicast listener and an empty array 73 | this.listenersByEvent[event] = []; 74 | var hash; 75 | if (options.overrides[event]) { 76 | hash = options.overrides[event]; 77 | } else { 78 | hash = hasher.hash(event); 79 | } 80 | 81 | // FIXME: add code to cope with hash collisions (should be rare as we have ~39 bits of hash space) 82 | // create and keep track of udp multicast listening server 83 | var that = this; 84 | var server = this.serversByEvent[event] = dgram.createSocket('udp4'); 85 | 86 | // work around lack of backwards compatibility of NodeJS v0.10.X re: multicast 87 | if (version === VERSION_0_10_X) { 88 | server.bind(hash.port, hash.address, function() { 89 | server.setMulticastTTL(options.ttl); 90 | server.addMembership(hash.address, options.multicastInterface); 91 | server.setMulticastLoopback(true); // needed from inter-process intra-box comms 92 | server.on('message', function(msg, rinfo) { that.handleMessage(event, msg, rinfo); }); 93 | }); 94 | } else { // VERSION_0_8_X 95 | server.bind(hash.port, hash.address); 96 | server.setMulticastTTL(options.ttl); 97 | server.addMembership(hash.address, options.multicastInterface); 98 | server.setMulticastLoopback(true); // needed from inter-process intra-box comms 99 | server.on('message', function(msg, rinfo) { that.handleMessage(event, msg, rinfo); }); 100 | } 101 | } 102 | this.listenersByEvent[event].push(listener); 103 | } 104 | 105 | /* 106 | * This handles the incoming messages. 107 | */ 108 | MulticastEventEmitter.prototype.handleMessage = function(event, msg, rinfo) { 109 | //console.log('handleMessage', event, msg, rinfo); 110 | try { 111 | var message = JSON.parse(msg.toString('utf8')); 112 | //if (this.lastSeq[message.src] && CONTINUIE HERE 113 | if (message.seq) { 114 | var key = message.src + '/' + event; 115 | if (!this.lastSeq[key]) this.lastSeq[key] = 0; 116 | var missed = message.seq - (this.lastSeq[key] + 1); 117 | if (missed) { 118 | console.warn(missed, 'messages missed', this.lastSeq[key]); 119 | } else { 120 | //console.info(missed, 'messages missed', this.lastSeq[key], key); 121 | } 122 | this.lastSeq[key] = message.seq; 123 | } 124 | } catch(e) { 125 | console.warn('error parsing multicast message:', msg); 126 | } 127 | 128 | var list = this.listenersByEvent[event]; 129 | //console.log('list', list); 130 | for (var i in list) { 131 | var listener = list[i]; 132 | // TODO: should we use nextTick here? 133 | if (message.args) { 134 | listener.apply(undefined, message.args); 135 | } else { 136 | // If the message doesn't follow the spec, pass it all on. 137 | // (Needed for legacy apps). 138 | listener.call(undefined, message); 139 | } 140 | } 141 | } 142 | 143 | MulticastEventEmitter.prototype.on = MulticastEventEmitter.prototype.addListener; 144 | 145 | /* 146 | * Emit an event. 147 | */ 148 | MulticastEventEmitter.prototype.emit = function(event) { // varargs... 149 | var args = Array.prototype.slice.call(arguments); 150 | args.shift(); // don't send the event name as one of the arguments 151 | //console.log('emit', args); 152 | if (!this.seqByEvent[event]) this.seqByEvent[event] = 0; 153 | var hash; 154 | var message; 155 | if (options.overrides[event]) { 156 | hash = options.overrides[event]; 157 | message = new Buffer(JSON.stringify(args[0])); 158 | } else { 159 | hash = hasher.hash(event); 160 | message = new Buffer(JSON.stringify({ event: event, args: args, src: this.src, seq: this.seqByEvent[event]++ })); 161 | } 162 | //console.log("this.sender.send(" + message + ", " + 0 + ", " + message.length + ", " + hash.port + ", " + hash.address + ");"); 163 | this.sender.send(message, 0, message.length, hash.port, hash.address); 164 | } 165 | 166 | 167 | /* 168 | * Get the first non-localhost IPv4 address. 169 | * This is only used for unique identification (with pid), not comms. 170 | */ 171 | function getIPv4Address() { 172 | var interfaces = require('os').networkInterfaces(); 173 | for (var devName in interfaces) { 174 | var iface = interfaces[devName]; 175 | 176 | for (var i = 0; i < iface.length; i++) { 177 | var alias = iface[i]; 178 | if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) 179 | return alias.address; 180 | } 181 | } 182 | 183 | return '0.0.0.0'; 184 | } 185 | -------------------------------------------------------------------------------- /multicast-eventemitter-test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Thorcom Systems Ltd. All Rights Reserved. 2 | 3 | var vows = require('vows') 4 | , assert = require('assert') 5 | , hasher = require('../lib/hasher') 6 | ; 7 | 8 | var suite = vows.describe('Hasher').addBatch( 9 | { "Hash 'hello world'" 10 | : { topic 11 | : hasher.hash("hello world") 12 | , "check address" 13 | : function(topic) { assert.deepEqual(topic.address, "237.94.182.59"); } 14 | , "check port" 15 | : function(topic) { assert.deepEqual(topic.port, 25787); } 16 | } 17 | , "Hash 'table.update'" 18 | : { topic 19 | : hasher.hash("table.update") 20 | , "check address" 21 | : function(topic) { assert.deepEqual(topic.address, "237.62.152.15"); } 22 | , "check port" 23 | : function(topic) { assert.deepEqual(topic.port, 10076); } 24 | } 25 | } 26 | ) 27 | 28 | suite.export(module); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name" : "multicast-eventemitter" 2 | , "description" : "LAN wide eventemitter, using multicast." 3 | , "version" : "0.10.4" 4 | , "maintainers" : 5 | [ { "name": "Chris Dew" 6 | , "email": "cmsdew@gmail.com" 7 | } 8 | ] 9 | , "bugs" : { "web" : "http://github.com/chrisdew/multicast-eventemitter/issues" } 10 | , "licenses" : [ ] 11 | , "repositories" : 12 | [ { "type" : "git" 13 | , "url" : "http://github.com/chrisdew/event-emitter" 14 | } 15 | ] 16 | , "main" : "./lib/multicast-eventemitter.js" 17 | , "engines" : { 18 | "node" : "~0.8.0 ~0.10.0" 19 | } 20 | , "devDependencies" : { "vows" : ">=0.5.2" 21 | , "docco" : ">=0.3.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/hasher-test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Thorcom Systems Ltd. All Rights Reserved. 2 | 3 | var vows = require('vows') 4 | , assert = require('assert') 5 | , hasher = require('../lib/hasher') 6 | ; 7 | 8 | var suite = vows.describe('Hasher').addBatch( 9 | { "Hash 'hello world'" 10 | : { topic 11 | : hasher.hash("hello world") 12 | , "check address" 13 | : function(topic) { assert.deepEqual(topic.address, "237.94.182.59"); } 14 | , "check port" 15 | : function(topic) { assert.deepEqual(topic.port, 25787); } 16 | } 17 | , "Hash 'table.update'" 18 | : { topic 19 | : hasher.hash("table.update") 20 | , "check address" 21 | : function(topic) { assert.deepEqual(topic.address, "237.62.152.15"); } 22 | , "check port" 23 | : function(topic) { assert.deepEqual(topic.port, 10076); } 24 | } 25 | } 26 | ) 27 | 28 | suite.export(module); 29 | --------------------------------------------------------------------------------