Skip to content

Instantly share code, notes, and snippets.

@xeoncross
Created May 6, 2015 16:00
Show Gist options
  • Save xeoncross/85feef5806647cfc9483 to your computer and use it in GitHub Desktop.
Save xeoncross/85feef5806647cfc9483 to your computer and use it in GitHub Desktop.
Simple Socket.io (v1.3+) Multi-room server + benchmark client
var io = require('socket.io-client');
var i, clients = 0, sent = 0, received = 0;
var TOTAL_CLIENTS = process.argv[2] || 500;
var CLIENTS_PER_ROOM = process.argv[3] || 3;
console.log("Total clients: " + TOTAL_CLIENTS);
console.log("Clients per room: " + CLIENTS_PER_ROOM);
console.log("\nclients\tsent\treceived\tsent/c\treceived/c");
setInterval(function() {
console.log([clients, sent, received, '',
Math.round(sent/clients), Math.round(received/clients)].join("\t"));
sent = 0;
received = 0;
}, 2000);
function sendMessage(socket, room) {
var message = { msg: 'hello from ' + socket.id, room: room };
socket.emit('message', message, function(error, target) {
if(error) die(arguments);
++sent;
setTimeout(function() {
sendMessage(socket, room);
}, 1000);
});
};
function die(err) {
console.log(err);
process.exit();
}
function pickUsername(socket) {
var name = 1;
do {
} while($taken);
}
function connectClient(i) {
var channel = 'test' + Math.floor(i / CLIENTS_PER_ROOM);
setTimeout(function() {
var socket = io.connect('http://localhost:3000', { 'force new connection': true });
// On any of the standard connection errors
for(var type in ['disconnect', 'connect_error', 'connect_timeout', 'reconnect_failed']) {
function(type) {
socket.on(type, function() {
console.log(socket.id, type);
process.exit(1);
});
}(type);
}
socket.on('connect_timeout', function() {
console.log(socket.id, 'connect_timeout');
process.exit(1);
});
socket.emit('join', channel, function(err) {
if(err) die(err);
++clients;
socket.on('message', function(msg) {
// die(msg);
++received;
});
sendMessage(socket, channel);
});
socket.on('clients', function(room))
}, i*10);
};
for (i=0; i<TOTAL_CLIENTS; ++i) {
connectClient(i);
}
{
"name": "socket.io-rooms",
"description": "A simple example of multiple rooms with Socket.IO, benchmark included",
"version": "0.0.1",
"dependencies": {
"bower": "^1.4.1",
"ejs": "^2.3.1",
"express": "^4.12.3",
"socket.io": "^1.3.5",
"socket.io-client": "^1.3.5",
"underscore": "^1.8.3"
},
"scripts": {
"postinstall": "./node_modules/bower/bin/bower install",
"start": "node server.js"
},
"engines": {
"node": ">= 0.4.10"
}
}
var express = require('express')
, app = express()
, server = require('http').createServer(app)
, io = require("socket.io").listen(server)
// , npid = require("npid")
// , uuid = require('node-uuid')
// , Room = require('./room.js')
, _ = require('underscore')._;
app.set('port', process.env.OPENSHIFT_NODEJS_PORT || 3000);
app.set('ipaddr', process.env.OPENSHIFT_NODEJS_IP || "127.0.0.1");
// app.use(express.bodyParser());
// app.use(express.methodOverride());
app.use(express.static(__dirname + '/public'));
app.use('/', express.static(__dirname + '/public'));
// app.use('/components', express.static(__dirname + '/components'));
// app.use('/js', express.static(__dirname + '/js'));
// app.use('/icons', express.static(__dirname + '/icons'));
// app.set('views', __dirname + '/views');
app.engine('html', require('ejs').renderFile);
/* Store process-id (as priviledged user) */
// try {
// npid.create('/var/run/advanced-chat.pid', true);
// } catch (err) {
// console.log(err);
// process.exit(1);
// }
app.get('/', function(req, res) {
res.render('index.html');
});
/* You can send messages to chat rooms via AJAX
app.post('/send/:room/', function(req, res) {
var room = req.params.room
message = req.body;
io.sockets.in(room).emit('message', { room: room, message: message });
res.end('message sent');
});
*/
server.listen(app.get('port'), app.get('ipaddr'), function(){
console.log('Express server listening on IP: ' + app.get('ipaddr') + ' and port ' + app.get('port'));
});
var exec = require('child_process').exec;
exec('node benchmark.js 4 2', function (error, stdout, stderr) {
console.log('benchmark running');
});
var messageCount = 0;
var debugLog = setInterval(function() {
//console.log("\nclients\tsent\treceived\tsent/c\treceived/c");
console.log('Messages: ' + messageCount);
console.log(_.map(io.sockets.sockets, function(currentObject) {
return _.pick(currentObject, "id", "username");
}));
console.log(io.sockets.adapter.rooms);
// console.log(io.nsps["/"].adapter.rooms);
// console.log(io.sockets.sockets);
console.log();
}, 3000);
function die(err) {
console.log(err);
process.exit();
}
// io.set("log level", 1);
var MAX_SOCKETS_IN_ROOM = 100;
var MAX_ROOMS_IN_SERVER = 1000000;
var MAX_ROOMS_PER_SOCKET = 2; // Including the default socket.id room!
var ALLOW_GLOBAL_MESSAGES = false; // Messages must be to a room or specific client
// io.sockets.clients('room').length = number of clients in room
// socket.rooms.length = number of rooms client is in
// Is the string/array empty?
var isEmpty = function(str) {
return (str.length === 0 || !str.trim());
};
// https://github.com/chriso/validator.js/blob/master/validator.js#L621
var escape = function (str) {
return (str.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\//g, '&#x2F;')
.replace(/\`/g, '&#96;'));
};
// Inform all clients in the room about the other clients
var sendClientList = function(room) {
var clients = io.nsps["/"].adapter.rooms[room];
// var clients = io.sockets.clients(room);
io.sockets.in(room).emit('clients', {
room: room,
clients: clients
});
};
var usernames = {};
// On username change or disconnect free our usernames
var removeSocketUsername = function(socket) {
if(socket.username) {
delete usernames[socket.username];
delete socket.username;
}
}
// @todo expand to support all Unicode letters and numbers
// var isSafeString = function(str) {
// if(typeof str === "string" || str instanceof String) {
// return str.length !== 0 && ! str.match(/[^\w\-]/)); // [a-zA-Z0-9_]
// }
// };
/*
* 1. All ack() functions take an ERROR code as their first argument
* 2. All "message" events must be objects with either a _.room or _.to property
* if global messaging is disabled.
* 3. All usernames must be unqiue
*/
io.sockets.on('connection', function(socket) {
// FYI, Socket.io auto-joins the socket to a room by the same name
// socket.join(socket.id);
// @todo this is unique, so it's safe for us
// (do we actually want to share this with others though?)
socket.username = socket.id;
usernames[socket.id] = socket.id;
// One of the most common requests is being able to set a unique
// username on the clientside. Something like this could be built
// for OAuth, session cookies, or a username + password combo.
socket.on('username', function(username, ack) {
if(isEmpty(username)) {
ack('USERNAME_EMPTY');
return;
}
if(usernames[username]) {
if(usernames[username] !== socket.id) {
ack("USERNAME_TAKEN");
} else {
ack();
}
return;
}
removeSocketUsername(socket);
username = escape(username);
usernames[username] = socket.id;
socket.username = username;
ack(null);
});
// Returns the user's current connected rooms on success
socket.on('join', function(room, ack) {
// console.log(socket.id, "joining", room);
if(MAX_ROOMS_PER_SOCKET && socket.rooms.length >= MAX_ROOMS_PER_SOCKET) {
ack("MAX_ROOMS_PER_SOCKET", socket.rooms);
return;
}
socket.join(room);
ack(null, socket.rooms);
sendClientList(room);
});
// Returns the user's current connected rooms on success
socket.on('leave', function(room, ack) {
// console.log(socket.id, "leaving", room);
if(room === socket.id || ! socket.rooms[room]) {
ack("INVALID_ROOM", socket.rooms);
return;
}
socket.leave(room);
ack(true, socket.rooms);
sendClientList(room);
});
// On client message to server
socket.on('message', function(packet, ack) {
packet.from = socket.username;
messageCount++;
// console.log('message', socket.id, packet);
if( ! ALLOW_GLOBAL_MESSAGES) {
if( ! packet.room && ! packet.to) {
ack("INVALID_TARGET");
return;
}
}
// Sending a message to a room/channel
if(packet.room) {
if(socket.rooms.indexOf(packet.room) === -1) {
// die(socket.rooms);
ack("INVALID_ROOM", packet.room);
return;
}
// console.log('message', socket.id, packet);
// process.exit();
// Leave trace of the room this packet belongs too
//var room = packet.room;
//delete packet.room;
socket.broadcast.to(packet.room).emit('message', packet);
ack();
return;
}
// Sending a message to specific client
if(packet.to) {
if( ! usernames[packet.to]) {
ack("INVALID_USER");
return;
}
socket.broadcast.to(packet.to).emit('message', packet);
ack();
return;
}
// Only possible if global messages are allowed
io.sockets.emit('message', packet);
});
// Free username if taken
socket.on('disconnect', function() {
console.log('disconnect', socket.id, socket.username, socket.rooms);
// Other than the global room and the client's own room
for(var room in socket.rooms) {
// if(room !== '/' && room !== socket.id) {
// socket.broadcast.do(room).emit('clients', socket.username);
if(room !== socket.id) {
// Let the disconnect finish before we inform everyone
// setTimeout(function() {
// sendClientList(room);
// }, 500);
sendClientList(room);
}
}
removeSocketUsername(socket);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment