In this article, we are going to build a simple chat application using Socket.IO and Backbone.js.
Socket.IO is aimed at developers who want to start developing highly
interactive, real-time web applications, such as chat systems or
multiplayer games. Backbone.js will add structucture to our client-side
code and make it easy to manage and decouple concerns in our
application.
Readers should be familiar with Node.js and Express. Familiarity with Backbone is a plus, as well as Underscore.js, which is used for basic templating.
We use Backbone collections to listen for changes on the collection. The updates on the collections are reflected automatically by our view.
Next, we define our home template inside
The
Next, let’s define our Socket.IO chat client. It communicates with the server by sending messages to the server and listening for notifications. These notifications trigger events on the event bus to communicate with the controller. The following code is found in
Socket.IO really makes it easy to send/receive messages between the client and server. Here, we use two methods:
Below is a diagram showing what our chat protocol looks like:
To bootstrap everything, we simply create a
That’s it for the client side. If you encounter any errors, Chrome
has excellent debugging tools. Use it’s network tab to see if messages
are really exchanged.
Now we’ve seen how to handle sockets, let’s define a user model inside
Finally our chat server goes in
Readers should be familiar with Node.js and Express. Familiarity with Backbone is a plus, as well as Underscore.js, which is used for basic templating.
Introduction
A diagram illustrating the structure of our client side code is shown below. In the middle is a controller, which acts as a bridge between the socket client and view. The controller gets updates from the socket client, and changes the model. Updates are reflected in the view using Backbone bindings.Client Side
We’ll begin by looking at the client side code. All chat interactions are handled in
HomeView
. Let’s start by defining HomeModel
in /public/js/models/main.js
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| var HomeModel = Backbone.Model.extend({ defaults: { // Backbone collection for users onlineUsers: new UserCollection(), // Backbone collection for user chats, initialized with a predefined chat model userChats: new ChatCollection([ new ChatModel({sender: '' , message: 'Chat Server v.1' }) ]) }, // method for adding a new user to onlineUsers collection addUser: function (username) { this .get( 'onlineUsers' ).add( new UserModel({name: username})); }, // method for removing a user from onlineUsers collection removeUser: function (username) { var onlineUsers = this .get( 'onlineUsers' ); var u = onlineUsers.find( function (item) { return item.get( 'name' ) == username; }); if (u) { onlineUsers.remove(u); } }, // method for adding new chat to userChats collection addChat: function (chat) { this .get( 'userChats' ).add( new ChatModel({sender: chat.sender, message: chat.message})); }, }); |
We use Backbone collections to listen for changes on the collection. The updates on the collections are reflected automatically by our view.
Next, we define our home template inside
/public/index.html
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| < script type = "text/template" id = "home-template" > < div class = "row" > < div class = "col-md-10" > < div class = "panel panel-default" > < div class = "panel-heading" >Lobby</ div > < div class = "panel-body" > < div class = "nano" > < div class = "content" > < div class = "list-group" id = "chatList" ></ div > </ div > </ div > < form > < input class = "form-control" type = "text" id = "chatInput" ></ input > </ form > </ div > </ div > </ div > < div class = "col-md-2" > < div class = "panel panel-default" > < div class = "panel-heading" > < h3 class = "panel-title" >Online Users < span class = "badge pull-right" id = "userCount" ></ span ></ h3 > </ div > < div class = "panel-body" > < div class = "nano" > < div class = "content" > < div class = "list-group" id = "userList" ></ div > </ div > </ div > </ div > </ div > </ div > </ div > </ script > |
The
HomeView
is located in /public/js/views/main.js
. The file is relatively long, so it is left to the reader to explore.Chat Client
Next, let’s define our Socket.IO chat client. It communicates with the server by sending messages to the server and listening for notifications. These notifications trigger events on the event bus to communicate with the controller. The following code is found in
/public/js/socketclient.js
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| var ChatClient = function (options) { // redefine this to avoid conflicts var self = this ; // app event bus self.vent = options.vent; // server hostname replace with your server's hostname eg: http://localhost self.hostname = 'http: //chatfree.herokuapp.com'; // connects to the server self.connect = function () { // connect to the host self.socket = io.connect(self.hostname); // set responseListeners on the socket self.setResponseListeners(self.socket); } // send login message self.login = function (name) { self.socket.emit('login ', name); } // send chat message self.chat = function(chat) { self.socket.emit(' chat ', chat); } self.setResponseListeners = function(socket) { // handle messages from the server socket.on(' welcome ', function(data) { // request server info socket.emit(' onlineUsers '); self.vent.trigger(' loginDone ', data); }); socket.on(' loginNameExists ', function(data) { self.vent.trigger(' loginNameExists ', data); }); socket.on(' loginNameBad ', function(data) { self.vent.trigger(' loginNameBad ', data); }); socket.on(' onlineUsers ', function(data) { console.log(data); self.vent.trigger(' usersInfo ', data); }); socket.on(' userJoined ', function(data) { self.vent.trigger(' userJoined ', data); }); socket.on(' userLeft ', function(data) { self.vent.trigger(' userLeft ', data); }); socket.on(' chat ', function(data) { self.vent.trigger(' chatReceived', data); }); } } |
Socket.IO really makes it easy to send/receive messages between the client and server. Here, we use two methods:
socket.emit(message, [callback])
– Used to send messages to the server.socket.on(message, callback)
– Used to receive messages from the server.callback
is invoked on reception.
Below is a diagram showing what our chat protocol looks like:
Main Controller
For the final part on client side, we have our controller, orchestrating between views, models, and the socket client. Place this in
/public/js/main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
| var MainController = function () { var self = this ; // Event Bus for socket client self.appEventBus = _.extend({}, Backbone.Events); // Event Bus for Backbone Views self.viewEventBus = _.extend({}, Backbone.Events); // initialize function self.init = function () { // create a chat client and connect self.chatClient = new ChatClient({vent: self.appEventBus}); self.chatClient.connect(); // create our views, place login view inside container first. self.loginModel = new LoginModel(); self.containerModel = new ContainerModel({ viewState: new LoginView({ vent: self.viewEventBus, model: self.loginModel }) }); self.containerView = new ContainerView({model: self.containerModel}); self.containerView.render(); }; // View Event Bus Message Handlers self.viewEventBus.on( 'login' , function (name) { // socketio login self.chatClient.login(name); }); self.viewEventBus.on( 'chat' , function (chat) { // socketio chat self.chatClient.chat(chat); }); // Socket Client Event Bus Message Handlers // triggered when login success self.appEventBus.on( 'loginDone' , function () { self.homeModel = new HomeModel(); self.homeView = new HomeView({vent: self.viewEventBus, model: self.homeModel}); // set viewstate to homeview self.containerModel.set( 'viewState' , self.homeView); }); // triggered when login error due to bad name self.appEventBus.on( 'loginNameBad' , function (name) { self.loginModel.set( 'error' , 'Invalid Name' ); }); // triggered when login error due to already existing name self.appEventBus.on( 'loginNameExists' , function (name) { self.loginModel.set( 'error' , 'Name already exists' ); }); // triggered when client requests users info // responds with an array of online users. self.appEventBus.on( 'usersInfo' , function (data) { var onlineUsers = self.homeModel.get( 'onlineUsers' ); var users = _.map(data, function (item) { return new UserModel({name: item}); }); onlineUsers.reset(users); }); // triggered when a client joins the server self.appEventBus.on( 'userJoined' , function (username) { self.homeModel.addUser(username); self.homeModel.addChat({sender: '' , message: username + ' joined room.' }); }); // triggered when a client leaves the server self.appEventBus.on( 'userLeft' , function (username) { self.homeModel.removeUser(username); self.homeModel.addChat({sender: '' , message: username + ' left room.' }); }); // triggered when chat receieved self.appEventBus.on( 'chatReceived' , function (chat) { self.homeModel.addChat(chat); }); } |
To bootstrap everything, we simply create a
MainController
and call it’s init
method, inside /public/js/main.js
:
1
2
3
4
5
| $(document).ready( function () { var mainController = new MainController(); mainController.init(); }); |
Server Side
Next, we’ll turn to the server side which is implemented in Node.js, Express, and Socket.IO. Place this code, which implements the Express server component, in
/scripts/web.js
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // requirements var express = require( 'express' ); var http = require( 'http' ); var socketio = require( 'socket.io' ); var path = require( 'path' ); // routes var routes = require( '../routes/index.js' ); var app = express(); // routes middleware app.use(app.router); // serve public folder app.use(express.static(path.join(__dirname, '../public' ))); // serve index.html for every path app.use(routes.index); // this is how you use socket io with express var server = http.createServer(app); var io = socketio.listen(server); var port = process.env.PORT || 8080; server.listen(port, function () { console.log( ' - listening on ' + port+ ' ' + __dirname); }); // require our chatserver var ChatServer = require( './chatserver' ); // initialize a new chat server. new ChatServer({io: io}).init(); |
Chat Server
The last part of our application is the chat server. This is responsible for keeping a list of online users, and broadcasting chat messages. The first event that our server will receive on a new client connection is aptly named
connection
. connection
events handlers, pass along the socket
that was just established. The socket
handles the following events:socket.on(message, callback)
–callback
is called when a new message is received.message
can be any type of data, depending on what was sent.socket.on('disconnect', callback)
–callback
is called when the socket disconnects.socket.emit(message, args)
– Sendmessage
over the socket.socket.broadcast.send(message, args)
– Broadcastsmessage
to all sockets except the sender.
Now we’ve seen how to handle sockets, let’s define a user model inside
/scripts/chatserver.js
:
1
2
3
4
5
6
7
8
9
| // User Model var User = function (args) { var self = this ; // Socket field self.socket = args.socket; // username field self.user = args.user; } |
Finally our chat server goes in
/scripts/chatserver.js
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
| var Server = function (options) { var self = this ; self.io = options.io; // users array self.users = []; // initialize function self.init = function () { // Fired upon a connection self.io.on( 'connection' , function (socket) { self.handleConnection(socket); }); } // socket handler for an incoming socket self.handleConnection = function (socket) { // wait for a login message socket.on( 'login' , function (username) { var nameBad = !username || username.length < 3 || username.length > 10; // check for badname if (nameBad) { socket.emit( 'loginNameBad' , username); return ; } var nameExists = _.some(self.users, function (item) { return item.user == username; }); // check for already existing name if (nameExists) { socket.emit( 'loginNameExists' , username); } else { // create a new user model var newUser = new User({ user: username, socket: socket }); // push to users array self.users.push(newUser); // set response listeners for the new user self.setResponseListeners(newUser); // send welcome message to user socket.emit( 'welcome' ); // send user joined message to all users self.io.sockets.emit( 'userJoined' , newUser.user); } }); } // method to set response listeners self.setResponseListeners = function (user) { // triggered when a socket disconnects user.socket.on( 'disconnect' , function () { // remove the user and send user left message to all sockets self.users.splice(self.users.indexOf(user), 1); self.io.sockets.emit( 'userLeft' , user.user); }); // triggered when socket requests online users user.socket.on( 'onlineUsers' , function () { var users = _.map(self.users, function (item) { return item.user; }); user.socket.emit( 'onlineUsers' , users); }); // triggered when socket send a chat message user.socket.on( 'chat' , function (chat) { if (chat) { self.io.sockets.emit( 'chat' , { sender: user.user, message: chat }); } }); } } |
No comments:
Post a Comment