├── .gitignore
├── client
├── audience.swf
├── Audience.as
└── jquery.audience.js
├── package.json
├── lib
├── utils.js
├── stats.js
├── notification.js
├── single-stats.js
├── demo.js
├── master.js
├── hadoop-stats.js
├── subscribers.js
├── worker.js
├── cluster.js
└── audience.js
├── demo.html
├── bench.js
├── audience-meter.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | flash-client/
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/client/audience.swf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hc/audience-meter/master/client/audience.swf
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "audience-meter",
3 | "version": "2.2.0",
4 | "engines":
5 | {
6 | "node": "~0.10.0"
7 | },
8 | "dependencies":
9 | {
10 | "commander": "2.1.0",
11 | "node-uuid": "1.4.1",
12 | "msgpack": "0.2.2"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | exports.merge = function()
2 | {
3 | var result = {};
4 |
5 | Array.prototype.forEach.call(arguments, function(obj)
6 | {
7 | for (var name in obj)
8 | {
9 | if (obj.hasOwnProperty(name))
10 | {
11 | result[name] = obj[name];
12 | }
13 | }
14 | });
15 |
16 | return result;
17 | };
--------------------------------------------------------------------------------
/lib/stats.js:
--------------------------------------------------------------------------------
1 | var net = require('net'),
2 | merge = require('./utils').merge;
3 |
4 | exports.StatsServer = StatsServer;
5 |
6 | function StatsServer(options)
7 | {
8 | if (!(this instanceof StatsServer)) return new StatsServer(options);
9 |
10 | options = merge
11 | ({
12 | audience: null,
13 | port: 8080
14 | }, options);
15 |
16 | var server = net.Server();
17 | server.listen(options.port, 'localhost');
18 | server.on('connection', function(sock)
19 | {
20 | sock.write(JSON.stringify(options.audience.stats()));
21 | sock.end();
22 | });
23 | }
--------------------------------------------------------------------------------
/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Connected users to {namespace}: -
4 |
5 |
6 |
12 |
13 | <script src="http://cdn.sockjs.org/sockjs-0.1.min.js"></script>
14 | <script>
15 | $.audience('http://{hostname}/{namespace}').progress(function(data)
16 | {
17 | document.getElementById("total").innerHTML = data.total;
18 | });
19 | </script>
20 |
21 |
22 |
--------------------------------------------------------------------------------
/lib/notification.js:
--------------------------------------------------------------------------------
1 | var http = require('http'),
2 | url = require('url'),
3 | merge = require('./utils').merge;
4 |
5 | exports.NotificationServer = NotificationServer;
6 |
7 | function NotificationServer(options)
8 | {
9 | if (!(this instanceof NotificationServer)) return new NotificationServer(options);
10 |
11 | options = merge
12 | ({
13 | audience: null,
14 | port: 8080
15 | }, options);
16 |
17 | var server = http.Server();
18 | server.listen(options.port);
19 | server.on('request', function(req, res)
20 | {
21 | var pathname = url.parse(req.url, true).pathname;
22 | var path2ns = /^\/([^\/]+)(?:\/([^\/]+))(?:\/([^\/]*))$/;
23 | if ((pathInfo = pathname.match(path2ns)))
24 | {
25 | var namespace = pathInfo[1],
26 | serviceName = pathInfo[2],
27 | msg = pathInfo[3];
28 | options.audience.sendMessage(namespace, serviceName + '=' + msg);
29 | }
30 | res.writeHead(200, {'Content-Type': 'text/html'});
31 | res.end();
32 | });
33 | }
34 |
--------------------------------------------------------------------------------
/lib/single-stats.js:
--------------------------------------------------------------------------------
1 | var http = require('http'),
2 | url = require('url'),
3 | merge = require('./utils').merge;
4 |
5 | exports.SingleStatsServer = SingleStatsServer;
6 |
7 | function SingleStatsServer(options)
8 | {
9 | if (!(this instanceof SingleStatsServer)) return new SingleStatsServer(options);
10 |
11 | options = merge
12 | ({
13 | audience: null,
14 | port: 8080
15 | }, options);
16 |
17 | var server = http.Server();
18 | server.listen(options.port);
19 | server.on('request', function(req, res)
20 | {
21 | var pathname = url.parse(req.url, true).pathname;
22 | var path2ns = /^\/([^\/]+)?$/;
23 | res.writeHead(200, {'Content-Type': 'text/html'});
24 | if ((pathInfo = pathname.match(path2ns)))
25 | {
26 | var key = '@' + pathInfo[1];
27 | if (key == '@total')
28 | {
29 | res.write(options.audience.get_total_audience().toString());
30 | }
31 | else
32 | {
33 | var data = options.audience.get_audience(key);
34 | if (data)
35 | {
36 | res.write(JSON.stringify(data));
37 | }
38 | }
39 | }
40 | res.end();
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/lib/demo.js:
--------------------------------------------------------------------------------
1 | var http = require('http'),
2 | url = require('url'),
3 | fs = require('fs'),
4 | merge = require('./utils').merge;
5 |
6 | exports.DemoServer = DemoServer;
7 |
8 | function DemoServer(options)
9 | {
10 | if (!(this instanceof DemoServer)) return new DemoServer(options);
11 |
12 | options = merge({port: 8080}, options);
13 |
14 | var server = http.Server();
15 | server.listen(options.port);
16 | server.on('request', function(req, res)
17 | {
18 | var path = url.parse(req.url, true).pathname;
19 |
20 | if (path == '/')
21 | {
22 | res.writeHead(200, {'Content-Type': 'text/html'});
23 | res.end('Welcome to audience-meter. Try an event.');
24 | }
25 | else if(/^[a-z.\/]+\.(js|swf)$/.test(path))
26 | {
27 | if (/\.js$/.test(path))
28 | {
29 | res.writeHead(200, {'Content-Type': 'application/javascript'});
30 | }
31 | else
32 | {
33 | res.writeHead(200, {'Content-Type': 'application/x-shockwave-flash'});
34 | }
35 | fs.readFile('./' + path, function (err, data)
36 | {
37 | res.end(data);
38 | });
39 | }
40 | else
41 | {
42 | res.writeHead(200, {'Content-Type': 'text/html'});
43 | fs.readFile('./demo.html', function (err, data)
44 | {
45 | res.end(data.toString()
46 | .replace(/\{hostname\}/g, req.headers.host.split(':')[0])
47 | .replace(/\{namespace\}/g, path.replace(/^\/|\/.*/g, '')));
48 | });
49 | }
50 | });
51 | }
--------------------------------------------------------------------------------
/lib/master.js:
--------------------------------------------------------------------------------
1 | var cluster = require('cluster');
2 |
3 | exports.Master = Master;
4 |
5 | function Master(options)
6 | {
7 | if (!(this instanceof Master)) return new Master(options);
8 |
9 | if (!options.workers)
10 | {
11 | options.workers = require('os').cpus().length;
12 | }
13 |
14 | var eachWorker = function(callback)
15 | {
16 | for (var id in cluster.workers)
17 | {
18 | callback(cluster.workers[id]);
19 | }
20 | };
21 |
22 | cluster.on('online', function(worker)
23 | {
24 | worker.on('message', function(msg)
25 | {
26 | switch (msg.cmd)
27 | {
28 | case 'join':
29 | options.audience.join(msg.namespace);
30 | break;
31 | case 'leave':
32 | options.audience.leave(msg.namespace);
33 | break;
34 |
35 | case 'exclude':
36 | eachWorker(function(otherWorker)
37 | {
38 | if (worker !== otherWorker)
39 | {
40 | otherWorker.send(msg);
41 | }
42 | });
43 | // TODO: instruct other peers of same UDP multicast segment if cluster is activated
44 | break;
45 | }
46 | });
47 | });
48 |
49 | cluster.on('exit', function(worker, code, signal)
50 | {
51 | if (worker.suicide === true)
52 | {
53 | return;
54 | }
55 |
56 | options.log('warn', 'Respawn worker');
57 | cluster.fork();
58 | });
59 |
60 | for (var i = 0; i < options.workers; i++)
61 | {
62 | cluster.fork();
63 | }
64 |
65 | options.audience.on('notify', function(namespace, msg)
66 | {
67 | eachWorker(function(worker)
68 | {
69 | if (typeof msg == 'undefined')
70 | {
71 | msg = namespace.members;
72 | }
73 |
74 | worker.send({cmd: 'notify', namespace: namespace.name, msg: msg});
75 | });
76 | });
77 |
78 | process.on('SIGTERM', function()
79 | {
80 | eachWorker(function(worker)
81 | {
82 | options.log('debug', 'Disconnect worker ' + worker.id);
83 | worker.kill();
84 | });
85 | process.exit();
86 | });
87 | }
--------------------------------------------------------------------------------
/bench.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var options = require('commander'),
4 | http = require('http'),
5 | util = require('util');
6 |
7 | options
8 | .option('-H, --host ', 'The host to connect to (default localhost)', String, '127.0.0.1')
9 | .option('-e, --concurrent-events ', 'Number of concurrent events to create', parseInt, 10)
10 | .option('-c, --concurrent-clients ', 'Number of concurrent clients', parseInt, 1000)
11 | .option('-p, --event-prefix ', 'String to prefix to generated event names', String, 'bench')
12 | .option('-s, --speed ', 'Number of approx milliseconds to wait between connections', parseInt, 100)
13 | .parse(process.argv);
14 |
15 |
16 | var started = 0,
17 | connected = 0,
18 | ended = 0,
19 | misses = 0,
20 | messages = 0,
21 | errors = 0,
22 | eventNames = [];
23 |
24 | for (var i = 0; i <= options.concurrentEvents; i++)
25 | {
26 | eventNames.push(options.eventPrefix + i);
27 | }
28 |
29 | spawnClient();
30 |
31 | setInterval(function()
32 | {
33 | process.stdout.write(util.format('Started: %d, connected: %d, closed: %d, errors: %d, missed: %d, msgs: %d\r',
34 | started, connected, ended, errors, misses, messages));
35 | }, 500);
36 |
37 | function spawnClient(eventName)
38 | {
39 | if (!eventName)
40 | {
41 | eventName = eventNames[Math.floor(Math.random() * options.concurrentEvents)];
42 | }
43 |
44 | var client = http.get
45 | ({
46 | host: options.host,
47 | port: 80,
48 | path: '/' + eventName,
49 | headers: {Accept: 'text/event-stream'},
50 | agent: false
51 | });
52 | var interval = setInterval(function()
53 | {
54 | if (new Date().getTime() - client.lastMessage > 60000)
55 | {
56 | misses++;
57 | }
58 | }, 60000);
59 | client.on('error', function()
60 | {
61 | started--;
62 | errors++;
63 | });
64 | client.on('response', function(res)
65 | {
66 | connected++;
67 | res.on('data', function(data)
68 | {
69 | client.lastMessage = new Date().getTime();
70 | messages++;
71 | });
72 | res.on('end', function()
73 | {
74 | clearInterval(interval);
75 | started--;
76 | connected--;
77 | ended++;
78 | // reconnect
79 | spawnClient(eventName);
80 | });
81 | });
82 |
83 |
84 | if (++started < options.concurrentClients)
85 | {
86 | setTimeout(spawnClient, Math.random() * options.speed);
87 | }
88 | }
--------------------------------------------------------------------------------
/lib/hadoop-stats.js:
--------------------------------------------------------------------------------
1 | var http = require('http'),
2 | url = require('url'),
3 | merge = require('./utils').merge;
4 |
5 | exports.HadoopStatsServer = HadoopStatsServer;
6 |
7 | function HadoopStatsServer(options)
8 | {
9 | if (!(this instanceof HadoopStatsServer)) return new HadoopStatsServer(options);
10 |
11 | options = merge
12 | ({
13 | audience: null,
14 | port: 8080,
15 | hostname: '',
16 | auth_username: 'hadoop_stats',
17 | auth_password: 'GwKBkEiBXLyqRODTbBWpkhXY5FJvwVk7RthQzasgqTNQlpQDTksYEOnKbejlKBsR'
18 | }, options);
19 |
20 | var server = http.Server();
21 | server.listen(options.port);
22 | server.on('request', function(req, res)
23 | {
24 | if (req.method === 'POST')
25 | {
26 | var header = req.headers['authorization'] || '';
27 | var token = header.split(/\s+/).pop();
28 | var auth = new Buffer(token, 'base64').toString();
29 | var parts=auth.split(/:/);
30 | var username = parts[0];
31 | var password=parts[1];
32 | if (username == options.auth_username && password == options.auth_password)
33 | {
34 | var body = '';
35 | req.on('data', function (data) {
36 | body += data;
37 | });
38 | req.on('end', function () {
39 | data = JSON.parse(body);
40 | options.audience.setHadoopNamespaces(data);
41 | });
42 | res.writeHead(200,
43 | {
44 | 'Content-Length': '0',
45 | 'Connection': 'close'
46 | });
47 | }
48 | else
49 | {
50 | res.writeHead(403,
51 | {
52 | 'Content-Length': '0',
53 | 'Connection': 'close'
54 | });
55 | }
56 | }
57 | else
58 | {
59 | var pathname = url.parse(req.url, true).pathname;
60 | var path2ns = /^\/([^\/]+)$/;
61 | if ((pathInfo = pathname.match(path2ns)))
62 | {
63 | var key = '@' + pathInfo[1];
64 | var data = options.audience.get_audience_from_hadoop(key);
65 | if (data)
66 | {
67 | res.write(JSON.stringify(data));
68 | }
69 | }
70 | else
71 | {
72 | res.write(JSON.stringify(options.audience.getHadoopNamespaces()));
73 | }
74 | }
75 | res.end();
76 | });
77 | }
78 |
--------------------------------------------------------------------------------
/lib/subscribers.js:
--------------------------------------------------------------------------------
1 | var util = require('util'),
2 | events = require('events');
3 |
4 | util.inherits(Subscribers, events.EventEmitter);
5 | exports.Subscribers = Subscribers;
6 |
7 | function Subscribers()
8 | {
9 | if (!(this instanceof Subscribers)) return new Subscribers();
10 |
11 | this.lastTotal = 0;
12 | this.setMaxListeners(0);
13 | }
14 |
15 | Subscribers.prototype.createNotifyMessage = function(msg)
16 | {
17 | return 'data: ' + msg + '\n\n';
18 | };
19 |
20 | Subscribers.prototype.notify = function(total)
21 | {
22 | this.lastTotal = total;
23 |
24 | if (total === 0)
25 | {
26 | this.emit('empty');
27 | }
28 | else
29 | {
30 | this.emit('notify', this.createNotifyMessage(total));
31 | }
32 | };
33 |
34 | Subscribers.prototype.addClient = function(client)
35 | {
36 | var self = this;
37 |
38 | function notify(data) {client.write(data);}
39 | client.write(this.createNotifyMessage(this.lastTotal + 1));
40 |
41 | this.on('notify', notify);
42 |
43 | client.on('close', function()
44 | {
45 | self.removeListener('notify', notify);
46 | self.emit('remove', this);
47 | });
48 |
49 | this.emit('add', client);
50 | };
51 |
52 |
53 | function SubscribersGroup(options)
54 | {
55 | if (!(this instanceof SubscribersGroup)) return new SubscribersGroup(options);
56 |
57 | this.groups = {};
58 | this.options =
59 | {
60 | log: function(severity, message) {console.log(message);}
61 | };
62 |
63 | for (var opt in options)
64 | {
65 | if (options.hasOwnProperty(opt))
66 | {
67 | this.options[opt] = options[opt];
68 | }
69 | }
70 |
71 | this.log = this.options.log;
72 | }
73 |
74 | module.exports.SubscribersGroup = SubscribersGroup;
75 |
76 | SubscribersGroup.prototype.get = function(name, auto_create)
77 | {
78 | var subscribers = this.groups[name];
79 | if (!subscribers && auto_create !== false)
80 | {
81 | this.log('debug', 'Create `' + name + '\' subscribers group');
82 | this.groups[name] = subscribers = new Subscribers();
83 |
84 | var self = this;
85 | subscribers.on('empty', function()
86 | {
87 | self.log('debug', 'Drop `' + name + '\' empty subscribers group');
88 | delete self.groups[name];
89 | });
90 | subscribers.on('add', function(client)
91 | {
92 | self.log('debug', 'Client subscribed to `' + name + '\'');
93 | });
94 | subscribers.on('remove', function(client)
95 | {
96 | self.log('debug', 'Client unsubscribed from `' + name + '\'');
97 | });
98 | }
99 | return subscribers;
100 | };
101 |
--------------------------------------------------------------------------------
/client/Audience.as:
--------------------------------------------------------------------------------
1 | package
2 | {
3 | import flash.display.LoaderInfo;
4 | import flash.display.Sprite;
5 | import flash.external.ExternalInterface;
6 | import flash.net.URLStream;
7 | import flash.events.Event;
8 | import flash.events.ProgressEvent;
9 | import flash.events.IOErrorEvent;
10 | import flash.net.URLRequest;
11 | import flash.net.URLRequestHeader;
12 | import flash.net.URLRequestMethod;
13 | import flash.utils.setTimeout;
14 |
15 | public dynamic class Audience extends Sprite
16 | {
17 | private var offset:Number;
18 | private var stream:URLStream;
19 | private var callback:String;
20 | private var request:URLRequest;
21 | private var retryDelay:Number;
22 |
23 | public function Audience():void
24 | {
25 | var params:Object = LoaderInfo(this.root.loaderInfo).parameters;
26 | var url:String = String(params['url']);
27 | this.callback = String(params['callback']);
28 |
29 | this.request = new URLRequest(url);
30 | this.request.method = URLRequestMethod.POST;
31 | this.request.requestHeaders = new Array(new URLRequestHeader('Accept','text/event-stream'));
32 | this.request.data = 0;
33 |
34 | this.stream = new URLStream();
35 | this.stream.addEventListener(ProgressEvent.PROGRESS, this.dataReceived);
36 | this.stream.addEventListener(Event.COMPLETE, this.reconnect);
37 | this.stream.addEventListener(IOErrorEvent.IO_ERROR, this.reconnect);
38 |
39 | this.connect();
40 | }
41 |
42 | private function connect():void
43 | {
44 | this.offset = 0;
45 | this.stream.load(this.request);
46 | }
47 |
48 | private function reconnect(e:Event):void
49 | {
50 | if (retryDelay >= 0)
51 | {
52 | setTimeout(this.connect, retryDelay);
53 | }
54 | }
55 |
56 | public function dataReceived(e:ProgressEvent):void
57 | {
58 | var buffer:String = stream.readUTFBytes(e.bytesLoaded - this.offset);
59 | this.offset = e.bytesLoaded;
60 | if (!buffer) return;
61 |
62 | var lines:Array = buffer.split('\n');
63 | for (var i:int = 0, l:int = lines.length; i < l; i++)
64 | {
65 | var info:Array = lines[i].split(/:/, 2),
66 | value:Number = parseInt(info[1], 10);
67 | switch (info[0])
68 | {
69 | case 'data':
70 | ExternalInterface.call(this.callback, value);
71 | break;
72 | case 'retry':
73 | this.retryDelay = value;
74 | break;
75 | }
76 | }
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/client/jquery.audience.js:
--------------------------------------------------------------------------------
1 | (function($, global)
2 | {
3 | $.extend($.ajaxSettings.accepts, {stream: "text/event-stream"});
4 |
5 | function openXHRConnection(url, deferred)
6 | {
7 | var offset = 0,
8 | retryDelay = 2000,
9 | xhr = global.XDomainRequest ? new global.XDomainRequest() : new global.XMLHttpRequest();
10 | xhr.open('POST', url, true);
11 | xhr.withCredentials = false;
12 | xhr.setRequestHeader('Accept', 'text/event-stream');
13 | $(xhr).bind('error abort load', function()
14 | {
15 | if (retryDelay >= 0)
16 | {
17 | setTimeout(function() {openConnection(url, deferred);}, retryDelay);
18 | }
19 | });
20 | $(xhr).bind('progress', function()
21 | {
22 | while (true)
23 | {
24 | var nextOffset = this.responseText.indexOf('\n\n', offset);
25 | if (nextOffset === -1) break;
26 | var data = this.responseText.substring(offset, nextOffset);
27 | offset = nextOffset + 2;
28 |
29 | var lines = data.split('\n');
30 | for (var i = 0, l = lines.length; i < l; i++)
31 | {
32 | var info = lines[i].split(/:/, 2),
33 | value = parseInt(info[1], 10);
34 | switch (info[0])
35 | {
36 | case 'data':
37 | deferred.notify(value);
38 | break;
39 | case 'retry':
40 | retryDelay = value;
41 | break;
42 | }
43 | }
44 | }
45 | });
46 | deferred.done(function()
47 | {
48 | $(xhr).unbind();
49 | xhr.abort();
50 | });
51 | xhr.send();
52 | }
53 |
54 | function openFlashConnection(url, deferred)
55 | {
56 | var flashObject = null,
57 | callback = 'audience' + new Date().getTime();
58 |
59 | global[callback] = function(data)
60 | {
61 | deferred.notify(data);
62 | };
63 |
64 | if (navigator.plugins && navigator.mimeTypes && navigator.mimeTypes.length) // Netscape plugin architecture
65 | {
66 | flashObject = $('