CODE | DEMO*
UPDATE: This is the third of a three part series on CodeIgniter, Redis, and Socket.IO integration. Please be sure to read the other posts as well.
Part 1: A Sample CodeIgniter Application with Login And Session
Part 2: Use Redis instead of MySQL for CodeIgniter Session Data
Part 3: You are here.
Well here it is, folks, the moment I’ve been waiting for :) Real-time updates that actually work! Socket.IO has been integrated to work its magic alongside CodeIgniter. Now when a user is logged in, updates will tumble in like an avalanche with absolutely no refreshes or any user intervention at all! To catch up on what this is all about, be sure to check out A Sample CodeIgniter Application with Login And Session and Use Redis instead of MySQL for CodeIgniter Session Data before reading this.
Take a look at the video above to see the live updates in action. There are three different browser windows open (Chrome, Firefox, and IE9), each logged in with a different user. The top left browser is the Admin, which will get updated when any user posts a message. The Admin is also a member of team one, so when the admin posts a message, members of team one (bottom left) will see it. The browser on the right is team two, so she will not see anyone else’s posts, but the Admin will see what she posts.
* The demo may or may not be working depending on the state of my VPS. Most of my sample projects so far have been hosted on my shared hosting account, but due to the Redis and Node requirement I had to deploy this project to a VPS that I normally use for development and testing. If I am doing some development or testing, then Apache or Node or Redis might not be working properly – hence the video. Your best option really, is to download the code and try it yourself!
The Socket.IO library makes it painfully easy to work with NodeJS (aka, Node). Prior to this project I knew almost nothing about Node other than what had read in a few short books. I still don’t know much about Node, but I know that I like it and will continue to keep investigating it for future projects. One thing in particular that I think pretty cool in this project, is that all of the Node specific functionality (the real-time message updates) runs mostly parallel to the PHP application. So if Node decides to blow up, the application will still work, only without live updates.
Anyway, enough jibber jabber. Here’s the rundown on the changes to the application, and the highlights of the Socket.IO and Node code. Again, this is not meant to be a tutorial, but rather a show and tell and perhaps a nice piece of code for others to experiment with. Use at your own risk.
Installing Socket.IO
First things first: Install Socket.IO. I already had Node and NPM installed, but I needed a spot to create the node ‘server’ within my project. I created a folder in the project root called ‘nodejs’. Through the wonders of Node Package Management (NPM), I installed the socket.io package, as well as the node_redis package. This was done by simply navigating to the nodejs folder and running:
npm install redis
Yeah, that’s it. NPM will create a folder called ‘node_modules’ and download all the dependencies necessary to run the packages. After that I created the cisockServer.js file, and I was off to the races.
To get things working right away, I added code similar to the following, just to get things rolling. The first line instantiates Socket.IO and gets a Node server up and running on port 8080. An event handler is created for the ‘connection’ event, which fires whenever a Socket.IO client connects to the server. When this happens, Socket.IO will emit an event with an identifier of ‘startup’ and attach an object with a message. If the client is listening for the ‘startup’ event, it will receive the message.
io.sockets.on('connection', function (socket) {
// Let everyone know it's working
socket.emit('startup', { message: 'I Am Working!!' });
});
To actually get the Node server fired up, it’s as easy as node --debug cisocketServer.js
to get it going. I added the –debug option because I was using node-inspector for debugging and tracing. There is also an interesting project called Forever available from NodeJitsu that manages one or more Node processes. I use it on my VPS. It’s nice.
The Socket.IO Client
A server is all fine-and-dandy, but it won’t do much good without clients. I created a new javascript file in my CodeIgniter assets folder called ‘socket.js’ to hold all of my Socket.IO related client code. I wrapped most of the code in the MY_Socket namespace so it is more easily accessed by the javascript in main.js. The minimum amount of code needed to work with the server code above is what follows. It simply creates a socket connection, then listens for the ‘startup’ event from the socket connection. When the ‘startup’ event occurs, the attached message will be displayed on the console.
window.MY_Socket = {
// Instantiate the Socket.IO client and connect to the server
socket : io.connect('http://localhost:8080'),
// Set up the initial event handlers for the Socket.IO client
bindEvents : function() {
this.socket.on('startup',MY_Socket.startupMessage);
},
// This just indicates that a Socket.IO connection has begun.
startupMessage : function(data) {
console.log(data.message);
}
} // end window.MY_Socket
// Start it up!
MY_Socket.bindEvents();
});
To get this working, all that is needed are a couple more lines in header.php:
<script src="<?php echo base_url();?>/assets/js/socket.js"></script>
Now upon visiting the login screen (or any screen, really), the words, “I am working!” will appear in the console.
Joining A Room
Socket.IO has a concept called rooms, which is a nice way to segment broadcasted messages from a server so only a subset of users receive the message. In this application, users will join a room based on their team numbers. Team 1 will join room 1, and so on. The exception here are admins. Admin users can be on a team, but will receive messages from all users regardless of their team. To handle this, I created another room called ‘admin’.
The room joining process starts when a user is logged in. I added a bit of jQuery code to check the current page for team badges, and if any are found, run the joinRoom function. Another way to do this would be to just put a call to MY_Socket.joinRoom() at the top of /application/views/main.php so it runs whenever the main view is loaded.
...
if($('.userTeamBadge').children().length > 0){
MY_Socket.joinRoom();
}
}
So the joinRoom function does some interesting things. First it grabs the cookie named, “ci_session” and reads the value. This value is the session ID set by CodeIgniter. This ID will be used to look up some of the other session information stored in Redis by the application’s MY_Session class. When the session ID is obtained, a ‘joinRoom’ event is emitted with the ID attached. If no session ID is found, then nothing happens. The code below is part of the client code in the socket.js file.
// get the CodeIgniter sessionID from the cookie
var sessionId = readCookie('ci_session');
if(sessionId) {
// Send the sessionID to the Node server in an effort to join a 'room'
MY_Socket.socket.emit('joinRoom',sessionId);
} else {
// If no sessionID exists, don't try to join a room.
console.log('No session id found. Broadcast disabled.');
//forward to logout url?
}
}
Socket.IO will be listening for the ‘joinRoom’ event on the server. When it hears the event, it will grab the session ID, use it to construct a string that matches the corresponding session key in the Redis database, and get the data associated with that key. The results returned from Redis will contain the rest of the user’s session information, including teamId and isAdmin (indicating if the user is or is not an admin). The result is parsed into a JSON, and the teamId and isAdmin values are used to join the appropriate ‘rooms’.
For any of this to work, a Redis client must be set up to execute Redis commands. The following code is in cisockServer.js.
var io = require('socket.io').listen(8080);
// Let Node know that you want to use Redis
var redis = require('redis');
// Listen for the client connection event
io.sockets.on('connection', function (socket) {
// Instantiate a Redis client that can issue Redis commands.
var rClient = redis.createClient();
// Handle a request to join a room from the client
// sessionId should match the Session ID assigned by CodeIgniter
socket.on('joinRoom', function(sessionId){
var parsedRes, team, isAdmin;
// Use the redis client to get all session data for the user
rClient.get('sessions:'+sessionId, function(err,res){
console.log(res);
parsedRes = JSON.parse(res);
team = parsedRes.teamId;
isAdmin = parsedRes.isAdmin;
// Join a room that matches the user's teamId
console.log('Joining room ' + team.toString());
socket.join(team.toString());
// Join the 'admin' room if user is an admin
if (isAdmin) {
console.log('Joining room for Admins');
socket.join('admin');
}
});
});
});
Excellent.
Send and Receive Messages
When a user posts a new message, the data is sent to the web server using jQuery’s Ajax function. This happens in the App.postMessage function in the main.js file. If the post is successful, a callback function – App.successfulPost – is executed. In order for the post to be successful, it needs to be processed by the CodeIgniter controller method responsible for saving posts to the database. This method – main.post_message – had to be refactored so that it would not only save the message, but also respond to jQuery’s ajax request with the message wrapped up in the HTML template so it can be sent out to other users.
The HTML template responsible for rendering each individual message was separated out into its own view and saved as /application/views/single_posts.php. It was basically just cut and pasted from the main view.
<div class="otherAvatar">
<img src="../../assets/img/avatars/<?php echo $avatar ?>.png"
alt=""
data-title="<span class='badge badge-info'><?php echo $teamId ?></span> <?php echo $firstName ?> <?php echo $lastName ?>"
data-content="<?php echo $tagline ?>">
</div>
<div class="otherPostInfo">
<div class="otherPostBody"><p><?php echo $body ?></p></div>
<hr/>
<div class="otherPostDate"><p class="pull-right"><?php echo $createdDate ?></p></div>
</div>
</div>
In order to populate that template, CodeIgniter’s Loader.view method was used with the third parameter set to ‘true’ so it will return data instead of immediately rendering the view in the browser. The view is then loaded into the response data as a string, along with the user’s teamId value, and the HTML string that will be prepended to the user’s own message list. The following code is from /application/controller/main.php (the Main controller).
... save message to db code ...
if ( isset($saved) && $saved ) {
// Gather up data to fill the message template
$post_data = array();
$post_data = $this->user_m->fill_session_data($post_data);
$post_data['body'] = $saved['body'];
$post_data['createdDate'] = $saved['createdDate'];
// Create a message html partial from the 'single_post' template and $post_data
$broadcastMessage = $this->load->view('single_post',$post_data,true);
// Create an html snipped for the user's message table.
$myMessage = "<tr><td>". $saved['body'] ."</td><td>". $saved['createdDate'] ."</td></tr>";
// Create some data to return to the client.
$output = array('myMessage'=>$myMessage,
'broadcastMessage'=>$broadcastMessage,
'team'=>$post_data['teamId']);
// Encode the data into JSON
$this->output->set_content_type('application/json');
$output = json_encode($output);
// Send the data back to the client
$this->output->set_output($output);
}
}
The response object is sent back to the jQuery callback function, and it begins the process of broadcasting the message out to all the appropriate users. This really only takes one extra line of code in App.successfulPost.
...
// Send socket.io notification
MY_Socket.sendNewPost( result.broadcastMessage, result.team );
}
All this does is send two pieces of information to the MY_Socket.sendNewPost function. The MY_Socket.sendNewPost function will simply take the message and teamId value and send it to the Node server by emitting a Socket.IO event.
MY_Socket.socket.emit('newPost',msg,team);
}
When the ‘newPost’ event is handled on the server, it will relay the message to the appropriate team room, and also to the ‘admin’ room.
console.log('Broadcasting a post to team: ' + team.toString());
// Broadcast the message to the sender's team
var broadcastData = {message: post, team: team};
socket.broadcast.to(team.toString()).emit('broadcastNewPost',broadcastData);
// Broadcast the message to all admins
broadcastData.team = 'admin';
socket.broadcast.to('admin').emit('broadcastNewPost',broadcastData);
});
The ‘broadcastNewPost’ event is emitted twice, and will therefore be handled twice by the client. This is not normally a problem, unless there is an admin with the same teamId as the sender. Then the admin will receive the message twice, and duplicate messages will be displayed on the screen. To correct this, a little logic prevents the message from being displayed twice. The message attached to the ‘broadcastData’ object is forwarded to the App.showBroadcastedMessage function.
updateMessages : function(data) {
// Because the message is broadcasted twice (once for the team, again for the admins)
// we need to make sure it is only displayed once if the Admin is also on the same
// team as the sender.
if( ( !userIsAnAdmin() && data.team != 'admin') ||
( userIsAnAdmin() && data.team === 'admin') ){
// Send the html partial with the new message over to the jQuery function that will display it.
App.showBroadcastedMessage(data.message);
}
}
When the App.showBroadcastedMessage function receives the message, it appends it to the top of the list of messages from other users using simple jQuery.
$(messageData).hide().prependTo(App.$otherMessages).slideDown('slow');
//App.$otherMessages.prepend(messageData);
App.setElements();
App.setupComponents();
}
Whew. That’s it! The journey is complete.