Now we have support for multiple rooms, but it is very clumsy to enter a nickname every time we enter a room. Let us modify our system to accept the nickname once when entering the system and use it in all the rooms.
For this, let us start by modifying the landing page to add an input box to accept a nickname and add a JavaScript file to add the logic:
extends layout block append head script(type='text/javascript', src='/javascripts/landing.js') block content section#welcome div Welcome span input#nickname(type="text", placeholder="Enter a nickname") a#startchat(class="btn") Login
Here, in the preceding code, we are adding a script entry to add landing.js
and replacing the Start now button with the field to enter a name and a Login button. Next, let us take a look at landing.js
:
$(function(){ $('#startchat').click(function(){ document.cookie = "nickname=" + $('#nickname').val() + ";; path=/"; window.location = "/rooms"; }); });
In the previous code, we are attaching a click
handler to the startchat
button. In the handler, we are adding the nickname entered by the user to the cookie and redirecting the user to /rooms
. We will be reading this cookie information while connecting the socket and then setting it on the socket. Before this cookie information can be accessed in the socket connection, we need to lay down some ground work to enable cookies in the Express.js application. For this, edit the app.js
code by referring to the following code block:
var express = require('express') , routes = require('./routes') , http = require('http') , path = require('path') , connect = require('connect'), var app = express(); var sessionStore = new connect.session.MemoryStore(); app.configure(function(){ //EXISTING CODE app.use(express.bodyParser()); app.use(express.cookieParser('somesuperspecialsecrethere')); app.use(express.session({ key: 'express.sid', store: sessionStore})); app.use(express.methodOverride()); app.use(app.router); app.use(express.static(path.join(__dirname, 'public'))); }); //EXISTING CODE
The first step is to add connect
as a dependency in the package.json
file and the require
keyword in app.js
. The connect
keyword is used to create a session store; in this case, an in-memory session store:
var sessionStore = new connect.session.MemoryStore();
We also enable the cookieParser
middleware and the session
module in the express application. Express' cookieParser
middleware will take a secret
parameter, which will be used to encrypt the cookies. The express' session
module is initialized along with passing it the key (express.sid
is the key for a session) and a store where the session should be maintained. In the following code, we are passing it an in-memory store, which we created in the previous step:
app.use(express.bodyParser()); app.use(express.cookieParser('somesuperspecialsecrethere')); app.use(express.session({ key: 'express.sid', store: sessionStore})); app.use(express.methodOverride()); app.use(app.router);
One important point to note about the previous code is the order of adding these two middleware components. These should be added after adding the bodyParser
middleware and before adding the router
middleware. If you open the browser and browse to the landing page now, you can see the cookie with the express.sid
key in the browser's debugging tools under the Cookies tab. If you enter a name and click the Enter button, you will again see a new cookie, named after your nickname, being set:
var io = require('socket.io'), exports.initialize = function (server) { io = io.listen(server); io.set('authorization', function (data, accept) { if (data.headers.cookie) { data.cookie = require('cookie').parse(data.headers.cookie); data.sessionID = data.cookie['express.sid'].split('.')[0]; data.nickname = data.cookie['nickname']; } else { return accept('No cookie transmitted.', false); } accept(null, true); }); var self = this; this.chatInfra = io.of("/chat_infra"); this.chatInfra.on("connection", function (socket) { socket.on("join_room", function (room) { var nickname = socket.handshake.nickname; socket.set('nickname', nickname, function () { socket.emit('name_set', {'name': socket.handshake.nickname}); socket.send(JSON.stringify({type:'serverMessage', message:'Welcome to the most interesting ' + 'chat room on earth!'})); 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}); }); }); //EXISTING CODE }
The first change in the preceding code block introduces us to a new feature in socket.io; this change is shown in the following highlighted code block:
io.set('authorization', function (data, accept) { if (data.headers.cookie) { data.cookie = require('cookie').parse(data.headers.cookie); data.sessionID = data.cookie['express.sid'].split('.')[0]; data.nickname = data.cookie['nickname']; } else { return accept('No cookie transmitted.', false); } accept(null, true); });
In this code snippet, we are setting an authorization
method for the socket. This method will get two parameters, the data that contains all the HTTP request information and the accept
method callback. The
authorization
method is called when a socket.io connection is requested but before it is established.
We can use this method for actually performing an authorization, but in our case we will just use it to get the nickname from the cookies, as this is the only socket.io method that will have the HTTP data available with it.
We are reading the cookie headers from the HTTP data and are parsing it using the cookie
module's parse
method. From the cookie, we are extracting the sessionID
value and the nickname and setting it to the data
object. This object is available on the socket as the handshake
property. Finally, we will call the accept
callback, which accepts two parameters, first a message and another a Boolean variable, indicating whether the authorization was successful or not.
We will remove the set_name
handler, as this handler need not be called because we already have the name with us. We will move the logic from the set_name
handler to the join_room
handler:
socket.on("join_room", function (room) { var nickname = socket.handshake.nickname; socket.set('nickname', nickname, function () { socket.emit('name_set', {'name': nickname}); socket.send(JSON.stringify({type:'serverMessage', message:'Welcome to the most interesting ' + 'chat room on earth!'})); 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 join_room
handler, we will fetch the nickname from the socket.handshake
map and set it as a property on the socket. On setting the nickname
property, we will still trigger the name_set
event so as to keep the changes on the client to a minimum:
var chatInfra = io.connect('/chat_infra'),
chatCom = io.connect('/chat_com'),
var roomName = decodeURI((RegExp("room" + '=' + '(.+?)(&|$)').exec(location.search) || [, null])[1]);
if (roomName) {
chatInfra.emit('join_room', {'name':roomName});
chatInfra.on('name_set', function (data) {
//EXISTING CODE
});
}
As the join_room
handler is the initializer for the room on the server, we will take it out of the name_set
handler and directly call it during the page load. The rest of the code remains as is.
To try this code, you will have to open two different browsers or browsers in different incognito sessions as the cookies/sessions will be shared for the same browser.