node-websocket-server: possible to have multiple, separate "broadcasts" for a single node.js process?
node.jsWebsocketnode.js Problem Overview
I'd like to know if it's possible to broadcast on different websocket "connections" running from the same https://github.com/miksago/node-websocket-server">node-websocket-server</a> app instance. Imagine a chatroom server with multiple rooms, only broadcasting messages to the participants specific to each room, on a single node.js server process. I've successfully implemented a one-chatroom-per-process solution, but I want to take it to the next level.
node.js Solutions
Solution 1 - node.js
You would probably like to try Push-it: http://github.com/aaronblohowiak/Push-It which is built on top of Socket.IO. Design adheres to the Bayeux Protocol.
However, if you need something that uses redis pubsub you can check http://github.com/shripadk/Socket.IO-PubSub
Specifically answering your question: You can maintain an array of all the clients connected to the websocket server. And probably just broadcast to a subset of those clients? The broadcast method does essentially that under the hood. node-websocket-server/Socket.IO maintains an array of all the clients connected and just loops through all of them "send"ing a message to each of the clients. Gist of the code:
// considering you storing all your clients in an array, should be doing this on connection:
clients.push(client)
// loop through that array to send to each client
Client.prototype.broadcast = function(msg, except) {
for(var i in clients) {
if(clients[i].sessionId !== except) {
clients[i].send({message: msg});
}
}
}
So if you want to relay messages only to specific channels, just maintain a list of all the channels subscribed by the client. Here is a simple example (to just get you started) :
clients.push(client);
Client.prototype.subscribe = function(channel) {
this.channel = channel;
}
Client.prototype.unsubscribe = function(channel) {
this.channel = null;
}
Client.prototype.publish = function(channel, msg) {
for(var i in clients) {
if(clients[i].channel === channel) {
clients[i].send({message: msg});
}
}
}
To make it even easier use EventEmitters. So in node-websocket-server/Socket.IO see where the messages are being received and parse the message to check the type (subscribe/unsubscribe/publish) and emit the event with the message depending on the type. Example:
Client.prototype._onMessage = function(message) {
switch(message.type) {
case 'subscribe':
this.emit('subscribe', message.channel);
case 'unsubscribe':
this.emit('unsubscribe', message.channel);
case 'publish':
this.emit('publish', message.channel, message.data);
default:
}
}
Listen to the events emitted in your app's on('connection') :
client.on('subscribe', function(channel) {
// do some checks here if u like
client.subscribe(channel);
});
client.on('unsubscribe', function(channel) {
client.unsubscribe(channel);
});
client.on('publish', function(channel, message) {
client.publish(channel, message);
});
Hope this helps.
Solution 2 - node.js
I'm not sure if rooms were a feature when the other answers were created, but in the documentation, they have a feature exactly what you are looking for. So go to that link and search for rooms
.
Here is an example from the site:
var io = require('socket.io').listen(80);
io.sockets.on('connection', function (socket) {
socket.join('justin bieber fans');
socket.broadcast.to('justin bieber fans').emit('new fan');
io.sockets.in('rammstein fans').emit('new non-fan');
});
Based on the other answers, it was more focused on scaling, I would love some insight if the built in version scales well as the proposed answers.
Solution 3 - node.js
Shripad K's answer is very well structured. Good job.
I think that solution will have some scaling issues though.
If you had 10,000 concurrent users in 500 chat rooms, then every time any user sent a message, you'd have to loop through all 10,000 clients. I suspect that it would be faster to store the list of clients in a given room in a structure in redis and just grab this list and send to those clients.
- Not sure if that's actually faster.
- Not sure what could be stored in redis that would then allow us to reference clients. Maybe there could be a hash of all clients in the server, by a unique id and in redis, we could just store a set of the user id's per chat room?
Does this seem any more scalable?
I've written a node chat server based on fzysqr's and need to make it scalable for multiple chats before we roll it out widely.
Solution 4 - node.js
With rooms my simple test chat looks like
chat.js:
var app = require('http').createServer(handler)
, io = require('socket.io').listen(app)
, fs = require('fs')
app.listen(80);
function handler (req, res) {
fs.readFile(__dirname + '/chat.html',
function (err, data) {
if (err) {
res.writeHead(500);
return res.end('Error loading chat.html');
}
res.writeHead(200);
res.end(data);
});
}
io.sockets.on('connection', function (socket) {
socket.on('join', function (room) {
if (Array.isArray(room)) {
var i;
for (i = 0; i < room.length; ++i) {
console.log('join room ' + room[i]);
socket.join(room[i]);
}
} else if (typeof room === 'string') {
console.log('join room ' + room);
socket.join(room);
}
});
socket.on('leave', function (room) {
if (typeof room === 'string') {
console.log('leave room ' + room);
socket.leave(room);
}
});
socket.on('post', function (data) {
io.sockets.in(data.room).emit('publish', data);
});
});
and chat.html:
<html>
<head>
<title>Node js test</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js"></script>
<script src="http://127.0.0.1:80/socket.io/socket.io.js"></script>
</head>
<body>
<h2>Node js test</h2>
<div style="height:400px;">
<div id="controls" style="height:400px; display: inline-block; width:20%; float:left; background-color:lightyellow;">
<input id="room1_check" type="checkbox" value="room_1" checked /><label for="room1_check">Room 1</label><br/><br/>
<input id="room2_check" type="checkbox" value="room_2" /><label for="room2_check">Room 2</label><br/><br/>
<input id="room3_check" type="checkbox" value="room_3" checked /><label for="room3_check">Room 3</label><br/><br/>
<input id="room4_check" type="checkbox" value="room_4" /><label for="room4_check">Room 4</label><br/><br/>
<input id="room5_check" type="checkbox" value="room_5" /><label for="room5_check">Room 5</label><br/><br/>
</div>
<div id="stream" style="height:400px; display: inline-block; width:40%; background-color:white; overflow:auto;"></div>
<div id="post" style="height:400px; display: inline-block; width:40%; float:right; background-color:yellow;">
<label for="postRoom">Room: </label>
<select id="postToRoom">
<option value="room_1">Room 1</option>
<option value="room_2">Room 2</option>
<option value="room_3">Room 3</option>
<option value="room_4">Room 4</option>
<option value="room_5">Room 5</option>
</select>
<br/><br/>
<label for="postBy">By: </label>
<select id="postBy">
<option value="User 1">User 1</option>
<option value="User 2">User 2</option>
<option value="User 3">User 3</option>
<option value="User 4">User 4</option>
<option value="User 5">User 5</option>
</select>
<br/><br/>
<label for="postMessage">Message:</label><br/>
<textarea id="postMessage" style="width:80%; height:100px;" ></textarea>
<br/><br/>
<input id="postBtn" type="button" value="post message" />
</div>
</div>
<script>
var socket = io.connect('http://127.0.0.1:80');
var checkedRooms = [];
$('#controls :checked').each(function() {
checkedRooms.push($(this).val());
});
socket.emit('join', checkedRooms);
socket.on('publish', function (post) {
//console.log(data);
$("#stream").html($("#stream").html() + "room: " + post.room + "<br/>");
$("#stream").html($("#stream").html() + "by: " + post.by + "<br/>");
$("#stream").html($("#stream").html() + "on: " + post.on + "<br/>");
$("#stream").html($("#stream").html() + "message: " + unescape(post.message) + "<br/>");
$("#stream").html($("#stream").html() + "=============================================<br/>");
});
$('#controls :checkbox').change(function () {
socket.emit(this.checked ? 'join' : 'leave', $(this).val());
});
$("#postBtn").click(function() {
socket.emit('post', {room: $("#postToRoom").val(), message: escape($("#postMessage").val()), by: $("#postBy").val(), on: (new Date() + "") });
});
</script>
</body>
</html>