Part 3: Real-Time Browser to Browser Communication, ExtJS Based Chat Application Using Node.Js and Socket.Io
In the previous entries, found here and here, I wrote about how to enable browser to server communication, using Node.Js and Socket.Io and how to emit events and return responses. In this entry I will describe how to route those events among different connected browsers, and how to create a basic chat application using Sencha ExtJS as a user interface, to notify clients when a new user is connected/disconnected and enable chat messaging.
Sencha ExtJS is a JavaScript framework, used for creating Rich Desktop Web Applications. It provides out of the box widgets, such as Windows, Grids, Panels, model data handling and others.
Server side: Handling and routing messages
Step1: Create a new object, that provides event handlers and routing logic.
/**
* Chat application.
* @class Provides message routing, along with other chat functionality.
* @constructor
*/
var ChatApplication = function() {
// Prepare the client object (storing socket objects, by id)
this._clientSockets = {};
};
Step 2: Add a client connection handler to this object used for keeping track of active connections, and notifying user about a new.
/**
* Method used for keeping track of client connections.
* @param {Object} socket Socket object, storing the client data.
* @function
*/
ChatApplication.prototype.clientConnectionHandler = function( socket ) {
// Push data to the client
this._clientSockets[socket.id] = socket;
// Notify others, by routing the 'newClient' event, along with a socket id
this.emitEvent( 'newClient', { id: socket.id }, socket );
// Send a list of client ids to the new client
this.clientListHandler( {}, socket );
}
Step 3: Add an event handler ‘clientList’ that returns a list of connected sockets.
/**
* Method used for handling a client list request.
* @param {Object} data Data object.
* @param {Object} socket Socket object, storing the client data.
* @function
*/
ChatApplication.prototype.clientListHandler = function( data, socket ) {
var clientIds = [];
for ( var socketId in this._clientSockets ) {
if ( this._clientSockets[socketId] ) {
clientIds.push( { id: socketId } );
}
}
socket.emit( 'clientList', { ids: clientIds } );
}
Step 4: Add an event handler ‘clientMessage’ that handles messages.
/**
* Method used for handling a client text message event.
* @param {Object} data Data object. Always null for a disconnecting client.
* @param {Object} socket Socket object, storing the client data.
* @function
*/
ChatApplication.prototype.clientMessageHandler = function( data, socket ) {
// Notify others, by routing the 'clientMessage' event, along with a socket id and text value
this.emitEvent( 'clientMessage', { id: socket.id, text: data.text }, socket );
}
Step 5: Add a client disconnect handler that updates the list of connected sockets.
/**
* Method used for keeping track of disconnecting clients.
* @param {Object} data Data object. Always null for a disconnecting client.
* @param {Object} socket Socket object, storing the client data.
* @function
*/
ChatApplication.prototype.clientDisconnectHandler = function( data, socket ) {
// Remove from sockets objects, to ignore it upon next notification
this._clientSockets[socket.id] = false;
// Notify existing clients, by routing the 'disconnectingClient' event, along with a socket id
this.emitEvent( 'disconnectingClient', { id: socket.id }, socket );
}
Step 6: Add a function that distributes messages among connected sockets.
/**
* Method used for emitting events, to all clients.
* @param {String} eventName Event name.
* @param {Object} data Data object to push to clients.
* @param {Object} ignoreSocket Optional socket object to ignore. Used when sending messages to all users, except the specified socket user.
*/
ChatApplication.prototype.emitEvent = function( eventName, data, ignoreSocket ) {
// Loop all existing connections
var me = this;
Object.keys( this._clientSockets ).forEach( function( socketId ) {
// Verify if we must ignore a socket
if ( typeof ignoreSocket !== "undefined" ) {
// Verify if that socket is the current one
if ( me._clientSockets[socketId] && me._clientSockets[socketId].id == ignoreSocket.id ) {
return; // Ignore
}
}
// Emit event
if ( me._clientSockets[socketId] ) {
me._clientSockets[socketId].emit( eventName, data );
}
} );
}
Step 7: Instantiate the object, and register the event handlers with the Server object.
var ChatApp = new ChatApplication();
// Create server
ChatServer = new Server( {
port: 10000 // Listening port
,socket: { // Socket configuration
log: false // Disable loggings
}
// Set the scope to the instance of ChatApp
,scope: ChatApp
// Add event handlers
,events: {
// Disconnecting client event
disconnect: ChatApp.clientDisconnectHandler
// Message event handler
,clientMessage: ChatApp.clientMessageHandler
// Client list handler
,clientList: ChatApp.clientListHandler
}
// Connection handler
,connectionHandler: ChatApp.clientConnectionHandler
} ).init();
Client side: Display the user interface, initiate a connection, send and receive messages
Step 1: Create a new class, that provides the chat application.
/**
* Chat Application Object.
* @class Provides chat functionality.
* @constructor
*/
var ChatJs = function() {
// Client id array
this._clients = [];
// Client instance
this.client = {};
// Create the UI as soon as ExtJS is ready
Ext.onReady( function() {
// Prepare the client list
this.clientList = Ext.create( 'Ext.grid.Panel', {
region: 'west'
,width: 180
,columns: [
{ header: 'Client Id', dataIndex: 'id', flex: 1 }
]
,store: Ext.create( 'Ext.data.Store', {
fields: [ 'id' ]
,data: []
} )
} );
// Handle a text sending UI action
var handleSendText = function() {
if ( this.textField.getValue() ) {
this.addText( "Me: " + Ext.htmlEncode( this.textField.getValue() ) );
// Emit event
this.client.emit( 'clientMessage', { text: this.textField.getValue() } );
}
this.textField.setValue( "" );
}
// Text field
this.textField = Ext.create( 'Ext.form.field.Text', {
width: 560
,enableKeyEvents: true
,listeners: {
keydown: function( field, e, eOpts ) {
if ( e.getKey() === 13 ) {
handleSendText.bind( this )();
}
}.bind( this )
}
} );
// Prepare the text window
this._firstTime = true; // Prevent the welcome text from displaying twice
this.textPanel = Ext.create( 'Ext.panel.Panel', {
region: 'center'
,border: false
,autoScroll: true
,html: ' '
,bbar: [
this.textField
, '-'
, Ext.create( 'Ext.button.Button', {
text: 'Send'
,handler: handleSendText.bind( this )
} )
]
,listeners: {
// Display welcome text
afterlayout: function() {
if ( this._firstTime === true ) {
this.addText( 'Welcome to ChatJS.' );
this._firstTime = false;
}
}.bind( this )
}
} );
// Prepare the window
this.chatWindow = Ext.create( 'Ext.window.Window', {
title: 'ChatJS'
,closable: false
,maximizable: false
,minimizable: false
,resizable: false
,height: 500
,width: 800
,layout: 'border'
,items: [
this.clientList
,this.textPanel
]
} );
// Show
this.chatWindow.show();
}.bind( this ) );
};
Step 2: Add an event handler ‘clientMessageHandler’.
/**
* Method used for handling an incoming message.
* @param {Object} data Data object.
* @function
*/
ChatJs.prototype.clientMessageHandler = function( data ) {
// Add text to window
this.addText( '' + data.id + ': ' + Ext.htmlEncode( data.text ) );
}
Step 3: Add an event handler ‘clientListMessageHandler’.
/**
* Method used for handling a client list event.
* @param {Object} data Data object.
* @function
*/
ChatJs.prototype.clientListMessageHandler = function( data ) {
// Store the list of clients, for later use
this._clientList = data.ids;
// Reload UI list
this.clientList.getStore().loadRawData( data.ids );
}
Step 4: Add a method used for updating the chat text.
/**
* Method used for appending text.
* @param {String} text String to add to window.
* @function
*/
ChatJs.prototype.addText = function( text ) {
// Get DOM component
var obj = Ext.get( 'messageArea' );
this.textPanel.body.insertHtml( "beforeEnd", text + '
' );
this.textPanel.body.scroll( 'b', Infinity );
}
Step 5: Add an event handler ‘newClientHandler’.
/**
* Method used for handling a new client connection.
* @param {Object} data Data object.
* @function
*/
ChatJs.prototype.newClientHandler = function( data ) {
// Add text to window
this.addText( 'Client connected: ' + data.id );
// Request a new list of clients
this.client.emit( 'clientList', {} );
}
Step 6: Add an event handler ‘disconnectingClientHanlder’.
/**
* Method used for handling a disconnecting client event.
* @param {Object} data Data object.
* @function
*/
ChatJs.prototype.disconnectingClientHandler = function( data ) {
// Add text to window
this.addText( 'Client left: ' + data.id );
// Request a new list of clients
this.client.emit( 'clientList', {} );
}
Step 7: Update the Client object to make use of the new events.
// Create a new instance of the chat application
var ChatApplication = new ChatJs();
var Example = new Client( {
port: 10000
,host: 'http://localhost'
,scope: ChatApplication
// Example event handlers, not bound to any scope
,events: {
// Client message handler
clientMessage: ChatApplication.clientMessageHandler
// Client disconnection handler
,disconnectingClient: ChatApplication.disconnectingClientHandler
// Connecting clients handler
,newClient: ChatApplication.newClientHandler
// Client list handler
,clientList: ChatApplication.clientListMessageHandler
}
} );
// Initialise the server
Example.init();
// Add the chat server to the chat application
ChatApplication.client = Example;
Update the HTML file, to include ExtJS, and the new client.js file
While the three articles provides a rough introduction to node.js and socket.io, without going into details, this example can be used as a starting point for enabling browser to browser communication. This can be used either for real time chat systems, gaming or other event handling mechanism.
The application can be seen here:
http://grosan.co.uk/chatjs/
The source code can be viewed or forked here:
https://github.com/fgheorghe/jsIRC/tree/64a288921fc0170644e68c5d7e135798b4b8ea5a
Here is a screenshot of the user interface, displaying a list of connected socket ids, a message from a second browser, from the current browser and a client disconnecting and connecting:
