Chat boxes in front of a scenic winter location

VIDEO DEMO

I first started learning how to code at age 12 when I taught myself how to make basic Windows batch scripts. It wasn’t long after that when the easy-to-use GUI drawing feature of Visual Basic 6 had me hooked on programming for good.

I graduated onto making proper network connected applications shortly thereafter. I was obsessed with making client-server programs that could talk to each other over TCP. I spent many of my younger years hacking away at the VB6 WinSock API. First learning how to make one client talk to a server, then figuring out how to make the server handle multiple clients at once and so on…

More recently I was tasked with making a real-time scoreboard for a company I’m currently contracting for. It involved an AngularJS front-end with a WebSocket client and a Node.JS server that would accept connections and broadcast the latest scores from a text file.

To be honest, before taking on this challenge, my Node.JS/Angular experience was sparse at best. But I saw the challenge as an opportunity and quickly learned what I needed to get the job done.

I was amazed at how quickly you can make a multi-threaded network application using Node.JS and the standard JavaScript WebSocket library. In a few minutes you can do something that used to take hours to figure out.

I was so amazed I thought I would make a tutorial explaining how to create a multi threaded chat client and server using Ionic and Node.JS.

how-to-build-a-websocket-multi-client-chat-using-ionic-and-node-js_fb
how-to-build-a-websocket-multi-client-chat-using-ionic-and-node-js-pint

Here is the step-by-step guide to making a multi-client chat with Ionic Framework and Node.JS. The screenshots will be from a Windows machine but it will work equally well on Linux or macOS X.

This tutorial is split into three parts.

  1. Setting up your system by installing the pre-requisites.
  2. Creating the Node.JS server which will accept the connections and forward the messages to other clients.
  3. Building the chat client using Ionic Framework which will connect to the server and send/receive messages.

Setting up Your System

The first step is to install Node.JS.

Go to https://nodejs.org and click on v6.5.0 Current (this may be different in the future, but just choose the one which has “Latest Features” and you should be good).

Then, click on the installer and follow the instructions to install Node.JS.

Now once that’s done, let’s install Ionic Framework.

First, load up Command Prompt (Windows) or Terminal (Mac OS/Linux) and type the following:

Windows

npm install -g ionic

Mac OS X / Linux

sudo npm install -g ionic

This will use npm (Node Package Manager) to install Ionic. There are a lot of dependencies so it may take a few minutes to install. The -g tells it to install the package globally, and not just in the local directory we are in.

Next, we need to install Cordova. Similar to before, use npm to install Cordova globally by running the following command:

Windows

npm install -g cordova

Mac OS X / Linux

sudo npm install -g cordova

Next, you will want to have the latest version of Google Chrome installed for debugging the Ionic app. You can download Chrome from: https://www.google.co.uk/chrome/browser/desktop/

Finally, if you don’t have one already, you will need to install a code editor. My favourite is Atom, which is a simple cross platform editor built using Electron. This guide is written specifically for Atom, but feel free to use another if you wish.

You can install Atom by browsing to https://atom.io and downloading the installer.

That’s all you need to do to prepare your system.

Creating the Node.JS Server

Let’s move onto the next step: Creating the server using Node.JS.

First, create a folder on your system where you wish to keep your server project (I will use C:\workspace\tutorial-multi-chat-server). Then open your Command Prompt (Windows) or Terminal (Mac OS X and Linux) window, and use the cd command to change to that directory.

cd C:\workspace\tutorial-multi-chat-server

Now let’s initialize our project as an npm package by typing the following and pressing enter:

npm init

You will be asked the following questions. Don’t worry if you make a mistake, you can always correct it manually by editing the package.json file.

Here is what you should put (to leave default, just press enter):

name: (tutorial-multi-chat-server) #Leave default.
version: (1.0.0) #Leave default.
description: #Multi client chat server.
entry point: (index.js) #Leave default.
test command: #Leave blank.
git repository: #Leave blank.
keywords: #Leave blank.
author: #Your name.
licence: (ISC) #Leave default.

Is this ok? (yes) #yes

Once done, a package.json file will automatically be created with the details you provided. If you want to change any of your answers, simply modify this file.

Next, we will use npm to install ws, a handy package which allows us to easily create a WebSocket server. Type the following and press enter:

npm install ws --save

This tells npm (Node Package Manager) to install a package called ws. The –save switch tells it to save this dependency to the package.json file.

The output should look something like this:

Output of npm install ws

Leave your Command Prompt or Terminal window open, as we will use this later, and open Atom.

Once Atom loads, select File > Add Project Folder

Atom Add Project Folder

Navigate to the directory we created for our project, select it and click Select Folder.

Atom Select Folder

Note: My one has a .git and README.md file because I initialized it as a Git repository, you can ignore these.

Right click on the project name and choose New File.

Atom Editor New File

Call the new file index.js. This will be the only file we need for this app.

Atom new file path

Below is the code for the contents of the index.js file. I have used comments to describe what is happening at each step.

// Import the ws module as a variable called WebSocketServer.
var WebSocketServer = require("ws").Server;

// Create a new WebSocketServer running on port 7007.
var wss = new WebSocketServer({port: 7007});

// Output a log to say the server is running.
console.log("Server is Running...");

// Create a "broadcast" function on our WebSocketServer object.
// The function will take a "msg" paramter. When called, it will
// loop through all the connected clients and send them the msg.
wss.broadcast = function broadcastMsg(msg) {
    wss.clients.forEach(function each(client) {
        client.send(msg);
    });
};

// Create a listener function for the "connection" event.
// Each time we get a connection, the following function
// is called.
wss.on('connection', function connection(ws) {
    // Store the remote systems IP address as "remoteIp".
    var remoteIp = ws.upgradeReq.connection.remoteAddress;
    // Print a log with the IP of the client that connected.
    console.log('Connection received: ', remoteIp);
    // Add a listener which listens for the "message" event.

    // When a "message" event is received, take the contents
    // of the message and pass it to the broadcast() function.
    ws.on('message', wss.broadcast);
});

Once updated, save the file and switch back to your Command Prompt or Terminal window.

Run the following to start our server:

node index.js
Server is running

That’s it, you have your server in no more than 13 lines of code (simple right!?).

Creating the Client using Ionic Framework

Now, let’s create our client application…

Open a new Command Prompt or Terminal window and use cd to change to our new directory where we would like to create our client project. I will use C:\workspace.

cd C:\workspace

Next, enter the following command to create our new Ionic project from the blank template (this can take a few minutes as there are lots of dependencies to install).

ionic start tutorial-multi-chat-client blank

Once the project is created, change to that directory by running:

cd tutorial-multi-chat-client

Next, let’s run our project using the ionic serve command:

ionic serve

The Ionic Blank Starter app will load in Google Chrome.

Ionic Blank Starter in Good Chrome

Next, press the following key(s) to open Google Chrome’s development tools:

Windows: F12
Mac OS X: Cmd + Shift + C
Linux: If you are using Linux, you know how to do this.

Once the developer tools open, click on the Toggle device toolbar icon on the top left:

Chrome toggle device toolbar

Once this is done, you can now toggle which device you wish to simulate by clicking the device name at the top of the screen (you will need to refresh the page each time you change this):

Chrome developer switch device.

OK, now that we have the project up and running, let’s start building our app…

First, we will create the interface.

Switch back to the Atom editor window and add the tutorial-multi-chat-client project by clicking File > Add Project Folder.

Atom Add Project Folder

Navigate to the tutorial-multi-chat-client directory and click Select Folder.

Atom Select Folder

Expand the www directory, and click the index.html file to edit it.

Atom index.html edit

Locate the following line:

<h1 class="title">Ionic Blank Starter</h1>

And update it to read:

<h1 class="title">Chat Client App</h1>

Save the file. You will notice that Chrome will automatically refresh to reflect the changes (this is a nice feature of Ionic).

Chat Client App Blank

Next, let’s build the rest of the screen. Locate the <ion-content> tags, add the following inside them. I’ve added comments inline to explain what is happening:

<!--
    This is the wrapper for the name input. The ng-show attribute tells
    Angular to only show this div (and it's contents) if the "showNameInput"
    scope variable is set to True. This allows us to hide/show this screen
    from our controller.
-->
<div ng-show="showNameInput" class="card">
  <!-- Set the heading of the name input card -->
  <div class="item item-divider">
      Enter Name
  </div>
  <!-- This is the main input area where the name input will be. -->
  <div class="item item-text-wrap">
      <!-- Define the input element for the name text field. -->
      <label class="item item-input">
          <!--
            The ng-model attribute links the contents of this input
            box with the "userName" scope variable.
          -->
          <input ng-model="userName" type="text" placeholder="Name">
      </label>
      <!-- Define the join button.  -->
      <label class="item item-input">
          <!--
            This is the button definition. The ng-click attribute tells
            Angular to call the "submitName()" function, passing in the
            "userName" scope variable (that we defined above) when clicked.
          -->
          <button ng-click="submitName(userName)" class="button button-block button-positive">
              Join Chat
          </button>
      </label>
  </div>
</div>

<!--
  This is the wrapper for the chat screen. The ng-show attribute tells
  Angular to only show this div (and it's contents) if the "showChatScreen"
  scope variable is set to True. This allows us to hide/show this screen
  from our controller.
-->
<div ng-show="showChatScreen" class="card">
  <!--
    This is the chat screen heading. The {{userName}} part simply prints
    the "userName" scope variable.
  -->
  <div class="item item-divider">
      Chatting as <strong><em>{{userName}}</em></strong>
  </div>
  <!-- This is the chat input/message log card. -->
  <div class="item item-text-wrap">
      <div class="list">
          <!-- Define the textarea which will be used to display the messages. -->
          <label class="item item-input">
              <!--
                This is the text area which will contian the chat messages.
                The id attribute is set because we need to access the element
                in order to keep the scroll focus to the bottom of the text,
                so users can see the latest messages without having to scroll.
                The ng-model sets tht contents of this textarea to the
                "messageLog" scope variable.
                The "style" attribute is required to prevent users being able
                to resize the box.
                The "readonly" attribute means users cannot directly edit
                the contents of the chat.
              -->
              <textarea
                id="messageLog"
                ng-model="messageLog"
                style="resize: none;"
                rows="10"
                readonly>
              </textarea>
          </label>

          <!-- This is for the message input and send button -->
          <div class="item item-input-inset">
              <label class="item-input-wrapper">
                  <!-- Set the contents of this input to be the "message" scope variable -->
                  <input ng-model="message" type="text" placeholder="Message">
              </label>
              <!--
                The ng-click attribute in this button will firstly call the
                "sendMessage() scope function, passing in the "message" scope
                variable which is defined in the input field above...
                Finally, it will set the "message" scope varibale to blank,
                ready for the users next input.
              -->
              <button ng-click="sendMessage(message); message = ''" class="button button-small">
                  Send
              </button>

          </div>

      </div>
  </div>
</div>

Once you have inserted the above code into the project, save the file.

Ionic will refresh, and the screen will still be blank. This is because we haven’t set the “showChatScreen” or “showNameInput” scope variables yet, which is preventing both the name and chat input screens from showing.

Next, open the www > js > app.js file in Atom.

Open app.js

This is the JavaScript component of our app, where all the magic happens. Inside this function, you should see the Angular project definition and the .run() function that is automatically created as part of the blank project template.

Right at the bottom of the app.js file, insert the following code. I’ve explained what is happening in the comments in-line.

// First, we define our controller. The $scope and $document parameters tell angular
// to inject these objects, making them accessible from our controller.
.controller('MainCtrl', function($scope, $document) {
    // Output to the log so we know when our controller is loaded.
    console.log('MainCtrl loaded.');

    // Define the URL for our server. As we are only running it locally, we will
    // use localhost.
    var SERVER_URL = 'ws://localhost:7007';
    // This is a variable for our WebSocket.
    var ws;

    // Below we set the "showNameInput" and "showChatScreen" scope variables,
    // which allow us to toggle the screens so we can show the name input
    // in the beginning, and then the chat input once they have entered their
    // name.
    // Note:
    $scope.showNameInput = true;
    $scope.showChatScreen = false;

    // Set the message log and the name input to blank.
    $scope.messageLog = '';
    $scope.userName = '';

    /**
        This function toggles between the screens. It basically just inverts
        the values of the "showNameInput" and "showChatScreen" scope variables.
        This works for our demo, but in a real app you might want to
        use different screens as opposed to showing/hiding elements on one view.
    */
    function toggleScreens() {
        $scope.showNameInput = !$scope.showNameInput;
        $scope.showChatScreen = !$scope.showChatScreen;
    }

    /** This function initiates the connection to the web socket server. */
    function connect() {
        // Create a new WebSocket to the SERVER_URL (defined above). The empty
        // array ([]) is for the protocols, which we are not using for this
        // demo.
        ws = new WebSocket(SERVER_URL, []);
        // Set the function to be called when a message is received.
        ws.onmessage = handleMessageReceived;
        // Set the function to be called when we have connected to the server.
        ws.onopen = handleConnected;
        // Set the function to be called when an error occurs.
        ws.onerror = handleError;
    }

    /**
        This is the function that is called when the WebSocket receives
        a message.
    */
    function handleMessageReceived(data) {
        // Simply call logMessage(), passing the received data.
        logMessage(data.data);
    }

    /**
        This is the function that is called when the WebSocket connects
        to the server.
    */
    function handleConnected(data) {
        // Create a log message which explains what has happened and includes
        // the url we have connected too.
        var logMsg = 'Connected to server: ' + data.target.url;
        // Add the message to the log.
        logMessage(logMsg)
    }

    /**
        This is the function that is called when an error occurs with our
        WebSocket.
    */
    function handleError(err) {
        // Print the error to the console so we can debug it.
        console.log("Error: ", err);
    }

    /** This function adds a message to the message log. */
    function logMessage(msg) {
        // $apply() ensures that the elements on the page are updated
        // with the new message.
        $scope.$apply(function() {
            //Append out new message to our message log. The \n means new line.
            $scope.messageLog = $scope.messageLog + msg + "\n";
            // Update the scrolling (defined below).
            updateScrolling();
        });
    }

    /**
        Updates the scrolling so the latest message is visible.
        NOTE: This is not really best practice... In your real app, you
        would have this logic in the directive.
    */
    function updateScrolling() {
        // Set the ID of our message log element (textarea) in the HTML.
        var msgLogId = '#messageLog';
        // Get a handle on the element using the querySelector.
        var msgLog = $document[0].querySelector(msgLogId);
        // Set the top of the scroll to the height. This makes the box scroll
        // to the bottom.
        msgLog.scrollTop = msgLog.scrollHeight;
    }

    /** This is our scope function that is called when the user submits their name. */
    $scope.submitName = function submitName(name) {
        // If they left the name blank, then return without doing anything.
        if (!name) {
            return;
        }
        // Set the userName scope variable to the submitted name.
        $scope.userName = name;
        // Call our connect() function.
        connect();
        // Toggle the screens (hide the name input, show the chat screen)
        toggleScreens();
    }

    /** This is the scope function that is called when a user hits send. */
    $scope.sendMessage = function sendMessage(msg) {
        // Create a variable for our message (append their message to their name).
        var nameAndMsg = $scope.userName + ": " + msg;
        // Send the data to our WebSocket connection.
        ws.send(nameAndMsg);
    }
})

Now, save the file.

Next, we need to set our new controller on the body of our template. Open index.html and locate the following line:

<body ng-app="starter">

Update it to read this:

<body ng-app="starter" ng-controller="MainCtrl">

Save the page. Ionic will refresh, and your Enter Name prompt should display. Enter your name and click Join Chat.

Client Chat App Screenshot

A message will appear stating that you are connected to the server.

Server Connected Screenshot

Now, copy the URL in your Chrome window, open a new window and paste it in (you can open as many windows as you like)… Enter a different name on each one, and test by sending a message.

Side by side screenshot

There you go! So we just created a chat client and a server that can handle multiple connections.

Now that’s done, hopefully you are inspired to built some amazing network connected applications using Node.JS and Ionic. If you feel like it, here are some features you could add to this app to improve it:

  • Add username and password authentication.
  • Add encryption (SSL or something else?)
  • Create some push notifications when a message is received.
  • Create different chat rooms which can be joined.
  • Log message history to a database.
  • Automatically add your friends who are in your contact list (this is quite advanced).
  • De-centralize it so you can have a scalable system with multiple servers which can automatically expand and shrink based on users (again this is very advanced).

If you want to see the code, it’s available on GitHub:

Client: https://github.com/LondonAppDev/tutorial-multi-chat-client
Server: https://github.com/LondonAppDev/tutorial-multi-chat-server

Thanks for reading and as always, please leave feedback or questions in the comments below.

Cheers,
Mark

12 replies
  1. WH Chan
    WH Chan says:

    Thanks. But it is impossible to do SSL with internal WebSocket. I tried to add ws into my Ionic2 project, but ending up with a runtime error: XHR error. Nothing showed on console, totally have no idea what’s going wrong. Could you help me out? Many thanks in advance.
    I am new to Ionic2, it seems it’s quite difficult to debug in a Ionic2 project, because everything are concatenated in serval big files like main.js, main.css and polyfill.js. NativeScript is much more friendly at this aspect, but NativeScript lacks of blur-like events, which leads me to left.

  2. subhash
    subhash says:

    how to create from this list of multiple user tap one user and send message again tap another user send message here you shown only one user please make that script for us

  3. mayur
    mayur says:

    awesome tutorials work fine upto connect server after that getting error “WebSocket is already in CLOSING or CLOSED state”.

  4. Desmond
    Desmond says:

    Hi Mark,

    Thank you so much for this simple tutorial and implementation.

    However i have 2 questions:-
    1) Is there any way that we can tie/add color to different user in the chat room?
    2) How do i create different dynamic chatroom?

    Hope to hear from you soon.

Comments are closed.