node-websocket-server: possible to have multiple, separate "broadcasts" for a single node.js process?

node.jsWebsocket

node.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.

  1. Not sure if that's actually faster.
  2. 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>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionS MView Question on Stackoverflow
Solution 1 - node.jsShripad KrishnaView Answer on Stackoverflow
Solution 2 - node.jsShawn McleanView Answer on Stackoverflow
Solution 3 - node.jsSean ColomboView Answer on Stackoverflow
Solution 4 - node.jsVolodymyr KrupachView Answer on Stackoverflow