In this section we will use another multiplexing feature of socket.io, called rooms. And we will use it to do just what the name says, create rooms. A chat room will be very noisy and confusing if everyone in the network is chatting in the same room. So as the first step, let's move our chat away from the landing page of our website to /chatroom
. For this, we should move our code from index.jade
to chatroom.jade
and put the following code in index.jade
:
extends layout block content section#welcome div Welcome a#startchat(type="button", class="btn", href="/chatroom") Start now
Basically, we will create a landing page with a welcome message and a link to go to the chat room. Let's also add the following styles for the landing page in style.css
:
#welcome div{ font-family: fantasy; font-size: 100px; margin-left: 20px; margin-top: 100px; } .btn { background-color: #5BB75B; background-image: linear-gradient(to bottom, #62C462, #51A351); background-repeat: repeat-x; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); color: #FFFFFF; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); border-image: none; border-radius: 4px 4px 4px 4px; border-style: solid; border-width: 1px; box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 1px 2px rgba(0, 0, 0, 0.05); cursor: pointer; display: inline-block; font-size: 14px; line-height: 20px; margin-bottom: 0; padding: 4px 12px; text-align: center; vertical-align: middle; position: absolute; right: 40px; bottom: 80px; text-decoration: none; }
Now our landing page will look like this:
The Start now link will send you to the chat room, but there is nothing there yet. So let us modify our routes/index.js
file to serve chatroom
. Add the following snippet to the end of the file:
exports.chatroom = function(req, res){ res.render('chatroom', { title: 'Express Chat' }); }
We will also have to add the mapping to app.js
:
app.get('/chatroom', routes.chatroom);
Now that we have a landing page, we are ready to add multiple rooms. We will now add support for the chat room page so that it can accept a room
parameter and will connect to that room when requested. So the call to connect to enter the chat room will look like this:
http://localhost:3000/chatroom?room=jsconf
For this we need to edit our chat.js
client script file:
var chatInfra = io.connect('/chat_infra'), chatCom = io.connect('/chat_com'), var roomName = decodeURI( (RegExp("room" + '=' + '(.+?)(&|$)').exec(location.search) || [, null])[1]); if (roomName) { chatInfra.on('name_set', function (data) { chatInfra.emit('join_room', {'name':roomName}); //EXISTING CODE }); } $(function () { $('#setname').click(function () { chatInfra.emit("set_name", {name:$('#nickname').val()}); }); });
The first thing is to parse the URL query to get the room name, here is how this is done:
var roomName = decodeURI( (RegExp("room" + '=' + '(.+?)(&|$)').exec(location.search) || [, null])[1]);
Here, in the preceding code, we are creating a regex to parse out the value between room=
and &
or to the end of the content. In the next line, we check if a room name was provided and once the user has entered the name, we will join the room.
To join the room, we emit the
join_room
event with roomName
as a parameter. This event will be handled on the server:
if (roomName) {
chatInfra.on('name_set', function (data) {
chatInfra.emit('join_room', {'name':roomName});
Since we will use the room only to restrict the broadcast messages (others are anyhow sent only to the recipient's socket), this is all we need to do on the client.
Now we will edit the sockets.js
file on our server to handle the join_room
event on chat_infra
and to change the broadcasts to send messages in the room they are meant for. Let us take a look at the changes in sockets.js
:
var io = require('socket.io'), exports.initialize = function (server) { io = io.listen(server); var self = this; this.chatInfra = io.of("/chat_infra"); this.chatInfra.on("connection", function (socket) { //EXISTING CODE }); socket.on("join_room", function (room) { socket.get('nickname', function (err, nickname) { socket.join(room.name); var comSocket = self.chatCom.sockets[socket.id]; comSocket.join(room.name); comSocket.room = room.name; socket.in(room.name).broadcast .emit('user_entered', {'name':nickname}); }); }); }); this.chatCom = io.of("/chat_com"); this.chatCom.on("connection", function (socket) { socket.on('message', function (message) { message = JSON.parse(message); if (message.type == "userMessage") { socket.get('nickname', function (err, nickname) { message.username = nickname; socket.in(socket.room).broadcast.send(JSON.stringify(message)); message.type = "myMessage"; socket.send(JSON.stringify(message)); }); } }); }); }
So this brings in some minor structural changes. Since we will need to refer chatCom
in chatInfra
, we add them both to the current object, which is also stored as itself, so that they are accessible in the closures. In the chat_infra
connection handler, we register a new event handler for join_room
:
socket.on("join_room", function (room) { socket.get('nickname', function (err, nickname) { socket.join(room.name); var comSocket = self.chatCom.sockets[socket.id]; comSocket.join(room.name); comSocket.room = room.name; socket.in(room.name).broadcast .emit('user_entered', {'name':nickname}); }); });
In the handler, we are receiving the room
object, which will in turn have the name of the room to join. Next we connect the chat_infra
socket to the room. This is done using the join
method of the socket
object:
socket.join(room.name);
The join
method takes a name string for the room. The room will be created if not present, else the socket will be connected to an existing room.
Now, once our client joins the room, it will get all the messages intended for the specific room in the chat_infra
namespace. But, this will not be useful until we also join the room in the chat_com
namespace. For this, we will need to obtain the socket
object, corresponding to the current socket
object in the chat_com
namespace and then call the same join
method on it:
var comSocket = self.chatCom.sockets[socket.id];
comSocket.join(room.name);
To get the corresponding socket
object on chat_com
, we fetch it using the current socket
object's ID (as it will be similar) from the sockets
array in the chatCom
namespace object. The next line simply calls the join
method on it. Now both have joined the room in both the namespaces. But when we receive the messages in the chat_com
namespace, we will need the name of the room this socket is connected to. For this, we will set the room
property on the comSocket
object to the room it is connected to:
comSocket.room = room.name;
Now that all is set up, we will announce in the room that the user has joined:
socket.in(room.name).broadcast .emit('user_entered', {'name':nickname}); });
As we did earlier, we still use broadcast.emit
, but instead of calling it on the socket, we restrict it to be sent only in the room, using in(room.name)
. Another change we make will be that of broadcasting the user messages again by restricting them to the room:
socket.in(socket.room).broadcast.send(JSON.stringify(message));
Now you can open the chat room by going to the following URL:
http://localhost:3000/chatroom?room=test001
Open this in two browser windows and log in with different names. Open another chat room in another browser window using the following URL:
http://localhost:3000/chatroom?room=test002
The messages and alerts sent only in the room test001
will be visible in the first two browsers, while the one connected to test002
will not be able to see them:
Here is the second user connected to the room test001
:
Here, in the following screenshot, the third user is shown connected to the room test002
: