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: