Building a multimedia chat app using Express, Socket.IO, Redis and Docker - Part 1

header image

Websocket is a bidirectional protocol built on top of TCP which enables a client establish a long living connection to the server and over this connection, messages can be exchanged between the two. There are many libraries out there that provide support for websockets in various server side languages. With Node, we have a few options, such as socket.io, ws, uwebsockets etc. 

You can follow this tutorial with my Github repo: here

Here’s a quick overview of what we’ll be building throughout this series

Tech Stack

  1. Express JS (http://npmjs.com/package/express) - server framework for node.js
  2. Socket.IO (https://www.npmjs.com/package/socket.io) - library to handle websockets
  3. Busboy (http://npmjs.com/package/busboy) - handles file uploads
  4. Nodemon (https://www.npmjs.com/package/nodemon) - watches files and applies changes in real time
  5. Docker (https://www.docker.com/) - software containerization tool
  6. Redis (https://redis.io) - in-memory data store with pub/sub and other functionality

Getting started

Follow the steps to set up a development environment:

mkdir mm-chat
cd mm-chat && npm init -y

This sets up a working directory and creates the package.json file which stores our dependencies and scripts. Let’s install our dependencies

npm i express socket.io busboy
npm i -D nodemon

The -D means we install nodemon as a development dependency, which will only be used at the time of development.
Open your package.json file and do the following changes to it:

{
 "name": "mm-chat",
 "version": "1.0.0",
 "description": "",
 "main": "index.js",
 "scripts": {
   "start": "nodemon server.js"
 },
 "keywords": [],
 "author": "",
 "license": "ISC",
 "dependencies": {
   "busboy": "^0.3.1",
   "express": "^4.17.1",
   "socket.io": "^2.3.0"
 },
 "devDependencies": {
   "nodemon": "^2.0.2"
 }
}

Notice the scripts property. Now everytime you want to run your server, just do npm start and nodemon will take over. It will run your app and you don’t need to restart the server every time you change something. That is nodemon’s job now.

Create a file server.js in the root of your project with the following contents:

const express = require('express');
 
const app = express();
 
app.get('/', (req, res) => {
   res.sendFile(`${__dirname}/index.html`);
});
 
app.listen(3000, () => {
   console.log('> http://localhost:3000');
});

The __dirname variable is a global variable in node.js which holds the current directory which the file currently executing in resides.

So we need some front end for our app that the user can interact with. We’ll keep it simple for now, no templating engines or front end framework. Let’s just make an index.html file at the root of the project and have express serve it to the browser. Paste the following content in the index.html file:

<!DOCTYPE html>
<html>
<head>
   <title>Multimedia chat!</title>
</head>
<body>
   Hello world!
</body>
</html>

Now let’s insert the websocket client library in the html file. Paste the following code in the head tag of the file:

<script src="/socket.io/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>

Let’s now make use of this client library. In the body tag of the HTML file, just paste in the following code:

<script>
let socket;
$(document).ready(function() {
   socket = io();
   socket.on('connect', () => {
      console.log('connected!');
   })
});
</script>

Let’s integrate this stuff on the server as well.
This is how our server.js file looks, with minimal setup:

const express = require('express');
const app = express();
var http = require('http').createServer(app);
var io = require('socket.io')(http);
 
app.get('/', (req, res) => {
   res.sendFile(`${__dirname}/index.html`);
});
 
io.on('connection', socket => {
   console.log('socket connected!');
   socket.on('disconnect', () => {
       console.log('socket disconnected!');
   })
});
 
http.listen(3000, () => {
   console.log('> http://localhost:3000');
});

Notice that we wrapped our express app in node.js http module’s httpServer function. We instantiated our websocket server using line 4. This enables our server to send the socket.io client library to be used on the front end on the /socket.io/socket.io.js route. Remember, we included this script in the index.html file. Open the browser on localhost:3000. You should see ‘connected’ being printed on the console.
Take a look at this simple diagram explaining the flow that we will try to achieve.

socket client server interactions

Let’s ask for user’s name when the socket connection is established.

<script>
       // add all helper functions here
       function getName() {
           return prompt('Please enter your name.');
       }
       
      let socket, name = '';
       $(document).ready(function() {
           socket = io();

           // register all event handlers here
           socket.on('connect', () => {
             name = getName();
             socket.emit('join', name);
           });
        });
</script>

After the user enters their name, we emit an event of type ‘join’ to the server. Notice that an event has a type (string) and some arbitrary data.
Now let’s make the server accept this event of type join. Here are the changes to the server:

io.on('connection', socket => {
   console.log('socket connected!');
   socket.on('disconnect', () => {
       console.log('socket disconnected!');
   });
 
   socket.on('join', name => {
       console.log(`${name} joined!`);
       socket.name = name;
       io.emit('join', name);
   });
});

When the server receives a join event, we set a name property on the socket to the one received from the client. You will see why this is useful. The server also notifies other clients of this event. Save everything and visit the webpage again. Once you enter your name, check the terminal server is running in. You will see the name printed out.

Let’s continue working on the client.

Let’s have our client react to the join event it receives from the server.
Create a <ul> tag with id of messages like so:

<ul id='messages'></ul>

Now add the following code just below the connect event handler, like so

<script>
      function handleJoinUser(name) {
          $('#messages').append(`
              <li>User joined: ${name}</li>
          `);
       }

       $(document).ready(function() {
           // code from above... socket.on('connect'

           socket.on('join', handleJoinUser);
       });
</script>

When a join event is received from the server, just add a list item to the list. Pretty simple, right?
Open your browser and make sure it works as expected.

checkpoint1

Let’s take it a step further.

I added a simple UI to have a chat box for the user to type in. Here’s the code.

<input type='text' placeholder="Message" id='inp' />
<button onclick="sendMsg()">Send</button>

Here’s the sendMsg function:

function sendMsg() {
   const text = $('#inp').val();
   socket.emit('chat', { name, text });
}

Notice we are emitting an event of type chat with an object. Let’s move to the server and do something with this event.

socket.on('chat', data => {
   console.log(`chat data: ${JSON.stringify(data, null, 2)}`);
   io.emit('chat', data);
});

Notice the function io.emit(), what this does is pretty simple. It sends an event to all the connected sockets. Here’s the emit cheat sheet which has all the functions socket.io provides to send data to connected sockets. https://socket.io/docs/emit-cheatsheet/

We’re not done yet though. Let’s make sure front end can handle the ‘chat’ event sent by the server.
Here’s what I’d do:

socket.on('chat', handleChat);      
function handleChat(data) {
    $('#messages').append(`
        <li>
            User: ${data.name}
            <br />
            Message: ${data.text}
        </li>
    `);
}

Remember, we emitted the chat event with a data object having name and text keys and the server just emits it back to all other clients as is. Open your browser and play around with it.

checkpoint2A quick little feature you can add is when enter key is pressed, just send the message. It’s straightforward. I know you can do it :)

Let’s have a simple ‘User disconnected’ message as well when a client signs off. Remember we stored the name property of the user on the socket object earlier? It was for this exact moment. Well, now we can easily do this:

socket.on('disconnect', () => {
       console.log(`${socket.name} disconnected!`);
       io.emit('leave', socket.name);
});

We can simply have the frontend now react to the ‘leave’ event emitted by the server like so:

socket.on('leave', handleLeaveUser);
function handleLeaveUser(name) {
   $('#messages').append(`
       <li>User left: ${name}</li>
   `);
}
checkpoint3

In this part, we made a working websocket server capable of exchanging messages between clients. 
In the next part, we’ll look at how we can have more interactivity between clients and also sending files. You can read the next part here

Share :
Tags :