DEV Community

loading...

How to build a real time chat app with Node.js and Socket.io

dberri profile image David Berri Originally published at dberri.com on ・10 min read

Lots of applications rely on real time data delivery to provide value to the user. From instant messaging, to online multiplayer games, IoT, ad servers, and stock exchange brokers, at one point or another in your career you might be challenged to build something like that. And with Node.js and Socket.io, it became so simple that you will might as well learn it now.

But before we start building the app, let's talk about some topics and approaches that you could use to build a real time application.

Regular polling

A good and simple way to summarise this is if you have an application that periodically (let’s say every 10s) sends a request to the server like asking: “Hey do you have any new information for me?”. Now, this can work in some situations, but you can imagine what would happen if hundreds of clients kept bombing the server with this amount of request every few seconds. Been there, done that, it was a very stressful day…

Long Polling

Long polling is similar to regular polling, but after a request to the server, the connection hangs and the server will only close the connection with a response once there’s new information. The client, after receiving the response, immediately sends a new request waiting for new information. This is a good option for delivering messages without delay but the server architecture must be able to handle multiple pending connections. Depending on the type of technology used, each pending connection can take up a lot of memory, which was the case when I tried this option… it was a very long day.

WebSocket

While regular polling and long polling make use of HTTP, WebSocket is another communication protocol that enables two-way communication between the server and the client. After the connection is opened, the client can send messages to the server, and receive event-driven responses without having to poll the server for a reply.

Socket.io

In their website, it says: ”Socket.IO enables real-time, bidirectional and event-based communication.”. It tries to establish a WebSocket connection if possible, but will fall back to HTTP long polling if not. Which is an important distinction to consider when you’re thinking about building something on top of it.

Their website also lists examples of applications that make good use of Socket.io like real-time analytics that push data to clients (like counters, charts and logs) or instant messaging and chat (like what we will be doing) and document collaboration where users editing a document can see other users changes in real time (think Google Docs). One can also think of how games could make use of this technology to send and receive multiplayer data.

It’s incredibly easy to integrate it in a Node.js application (they say it works on every platform, but I haven’t tried).

Let’s start 🚀

This is what the chat app will look like by the end of this tutorial:

Screenshot of the chat application

It should go without saying that we need Node.js installed, so if you still don’t have, go to their website and download at least the LTS version.

With that comes npm, the node package manager. I prefer Yarn (and that’s what I’ll be using throughout the tutorial), but feel free to use npm if you want. With that, go ahead and create a folder to store the application files. Now, open your terminal and navigate to the newly created folder (e.g. cd realtime-chat) and run yarn init -y which will quickly create a package.json file and you will be able to add the only dependency we need: yarn add socket.io.

Now, we need am HTML page where the users will be able to use the chat and a Node.js server. So, go ahead and create an index.html and a server.js files.

With that, let’s open package.json and edit a few lines. First, let’s change the main from index.js to server.js, and in scripts we can remove the test script and add "start": "node server.js" which will enable us to run yarn start from the root folder of the application and start up our server. That part of your package.json should look like this:

main: server.js,
scripts: {
  start: node server.js
}

Enter fullscreen mode Exit fullscreen mode

The interface

Since HTML is not the focus here, you can go ahead and copy this to your index.html file:

<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>RealTime</title>
  <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
</head>

<body>
  <div class="bg-white overflow-hidden overflow-hidden shadow rounded-lg px-4 py-4 sm:px-6 w-4/5 mx-auto mt-8">
    <h2 class="text-2xl leading-8 font-extrabold text-gray-900 sm:text-3xl sm:leading-9">
      Chat
    </h2>

    <div class="px-4 py-5 sm:p-6" id="message-box">
      <!-- Content goes here -->
    </div>

    <div class="border-t border-gray-200 px-4 py-4 sm:px-6">
      <form id="form" action="#" method="POST" class="grid grid-cols-1 row-gap-6">
        <div>
          <div class="mt-1 relative rounded-md shadow-sm">
            <input id="input" placeholder="Start typing..."
              class="form-input py-3 px-4 block w-full transition ease-in-out duration-150">
          </div>
        </div>
        <button type="submit"
          class="w-full inline-flex items-center justify-center px-6 py-3 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition ease-in-out duration-150">
          Send message
        </button>
      </form>
    </div>

    <div class="border-t border-gray-200 px-4 py-4 sm:px-6">
      <h3 class="px-4 py-4">Who's online:</h3>
      <ul id="peer-list"
        class="px-6 py-3 max-w-0 w-full whitespace-no-wrap text-sm leading-5 font-medium text-gray-900">
        <!-- Content goes here -->
      </ul>
    </div>

  </div>
</body>
</html>

Enter fullscreen mode Exit fullscreen mode

This is the basic structure of the chat app. There’s a box to display all messages, a form to type the message and a button to send it. All of the important parts have ids so that we can retrieve them in JavaScript later. Here, I’m using TailwindCSS to make it look good fast.

The server

Now, open server.js and add the following:

const fs = require('fs');
const http = require('http');
const SocketIO = require('socket.io');

// Prepares HTML file to be served
const content = fs.readFileSync(__dirname + '/index.html', 'utf8');
const httpServer = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'text/html');
  res.setHeader('Content-Length', Buffer.byteLength(content));
  res.end(content);
})

// Creates socket.io connection
const io = SocketIO(httpServer);

// Handles "connect" event
io.on('connect', socket => {
    // Handles "message" event sent by client
  socket.on('message', data => {
        // Emits new message to every connected client
        io.emit('newMessage', {
            message: data
        })
    })
});

// Starts up server
httpServer.listen(3000, () => {
  console.log("🔥 Listening on http://localhost:3000");
})
Enter fullscreen mode Exit fullscreen mode

This enough for the basic functionality of the app. We could further simplify things by using a framework like express, but for now, a classic Node.js server will suffice. It serves the index.html file and then creates a Socket.io connection on line 14. Then we can use the event listening functionality to listen for a “connect” event emitted from the client and handle that connection. You can create your own event keywords (like “connect”), but you have to keep in mind that there are a few keywords that should not be used as they conflict with the ones implemented by Socket.io. A few examples include connect, disconnect, reconnect and error. A full list of these event names can be found here.

On line 16 we listen for an event named “message” and pass a callback to handle the data received by that event. Then on line 18 we emit an event named “newMessage” to all connected sockets. Note that we listened on socket which is an individual client connected and we emit with io which is sort of a pool of sockets. You can always refer to this emit cheatsheet to see all the options you have, like emitting events to all connected sockets but the emitter, or emitting to “rooms” or sending privately from socket to socket.

Now, I want to make things more interesting and assign random names to the clients send these names to all clients so they know who’s connected and able to chat. Let’s add this:

const animals = [
  'fish',
  'cat',
  'tiger',
  'bear',
  'bull',
  'fox'
]

const colors = [
  'red',
  'green',
  'blue',
  'yellow',
  'purple',
  'pink'
]

/**
 * Generates a random name based on an animal and a color
 * 
 * @return {String}
 */
function randomName() {
  const color = colors[Math.floor(Math.random() * colors.length)]
  const animal = animals[Math.floor(Math.random() * animals.length)]

  return `${color}-${animal}`;
}

// Stores the names and ids of connected clients
const sockets = {}

// Creates socket.io connection
const io = SocketIO(httpServer);
Enter fullscreen mode Exit fullscreen mode

Then, inside the “connect” event handling, let’s add a few new even handlers:

// Handles “connect” event
io.on('connect', socket => {
  sockets[socket.id] = randomName();
  socket.emit('name-generated', sockets[socket.id]);
  io.emit('update-peers', Object.values(sockets));

  // Handles “message” event sent by client
  socket.on('message', data => {
    // Emits new message to every connected client
    io.emit('newMessage', {
      sender: sockets[socket.id],
      message: data
    })
  });

  // Handles “disconnect” event
  socket.on('disconnect', () => {
    delete sockets[socket.id]
    io.emit('update-peers', Object.values(sockets))
  })
})
Enter fullscreen mode Exit fullscreen mode

Here we’re basically wait for a client to connect, then we assign a random name to their socket id and send their “random name” so they know who they are. Then we send the list of connected socket names. We also need to handle the disconnect event, so if anyone disconnects, we update the list of connected sockets and send that to everyone in the chat to update their user interface.

Cool, now let’s implement the client so it can connect to the server and do its magic.

The client

Go to the index.html file and before closing the body tag, add the following:

<script src="/socket.io/socket.io.js"></script>
<script>

</script>

Enter fullscreen mode Exit fullscreen mode

This will “import” the Socket.io script (when you’re building a more complex application and are using a module bundler, this will probably look different, as the import will happen in another JavaScript file, but this is out of the scope of this article).

Let’s start the program by getting access to a few elements that we will use throughout the script:

<script>
    const form = document.getElementById('form');
    const input = document.getElementById('input');
    const msgBox = document.getElementById('message-box');
    const peerList = document.getElementById('peer-list');
</script>
Enter fullscreen mode Exit fullscreen mode

Now, in order to make use of Socket.io, we need to call it and store it in a variable, then we will start listening and emitting events:

<script>
  const form = document.getElementById('form');
  const input = document.getElementById('input');
  const msgBox = document.getElementById('message-box');
  const peerList = document.getElementById('peer-list');

  const socket = io();

  // Handles the "name-generated" event by storing the client's name in a variable
  socket.on('name-generated', () => ());

  // Handles the "update-peers" event by updating the peers list
  socket.on('update-peers', () => ());

  // Handles "newMessage" event and add that message to the chat
  socket.on('newMessage', () => ());

</script>
Enter fullscreen mode Exit fullscreen mode

All of the events listed above with socket.on() are emitted by the server at some point, they are still not implemented (i.e. we still don’t do anything after we listened for those events, but we will do it shortly. Before that, let’s handle the submission of a message:

/**
 * Retrieves message from input and emits to the server
 * 
 * @param {Object} evt Event fired by the form submission
 */
function submitHandler(evt) {
  evt.preventDefault();
  socket.emit('message', input.value);
  input.value = ''
  msgBox.focus();
}

form.addEventListener('submit', submitHandler)

Enter fullscreen mode Exit fullscreen mode

Here, we attach an event listener to the form. It will listen for the “submit” event and the submitHandler will prevent the default (just so the form does not trigger a page reload or navigating to the action attribute) and then we emit a “message” event containing the input field value. Then we clear the field and focus on something that is not a field, so if the user is in a mobile device, the keyboard goes away.

Now let’s go back to the other socket’s event listeners, an we will implement them. First, the simplest one, we listen for the “name-generated” event, if you remember, this is the event the server emits after generating a random name for the client. We need to store this name to use in other functions, so let’s create a variable in the same scope as the socket listeners like this:

let myName = ''
const socket = io();

// Handles the “name-generated” event by storing the client’s name in a variable
socket.on('name-generated', name => {
  myName = name
});

Enter fullscreen mode Exit fullscreen mode

And now, let’s handle the “newMessage” event. This event is emitted by the server whenever a socket emits the “message” event. In other words, someone sends a message to the server and the server broadcasts this message to everyone connected:

// Handles “newMessage” event and add that message to the chat
socket.on('newMessage', ({ sender, message }) => {
  let name = document.createElement('strong');
  name.textContent = `${sender} says: `

  let msgEl = document.createElement('span');
  msgEl.textContent = message

  let paragraph = document.createElement('p');
  paragraph.appendChild(name);
  paragraph.appendChild(msgEl);

  msgBox.appendChild(paragraph);
});
Enter fullscreen mode Exit fullscreen mode

Here, we expect the server to send an object containing the message and the sender’s name. We use this information to create a paragraph element that will be something like this: “blue-fish says: I am a new message”. And then appends this paragraph in the message box.

Let’s finish this by implementing the list of online clients:

// Handles the “update-peers” event by updating the peers list
socket.on('update-peers', peers => {
  peerList.innerHTML = ''

  const template = `<li class=“flex items-center space-x-3 lg:pl-2”>
    <div class=“flex-shrink-0 w-2 h-2 rounded-full bg-%PEER_COLOR%-600”></div>
      <span>%PEER_NAME%</span>
  </li>`

  for (const peer of peers) {
    let name = peer
    if (name === myName) {
      name += ' (you)'
    }
    peerList.innerHTML += template.replace('%PEER_NAME%', name).replace('%PEER_COLOR%', peer.split('-')[0])
  }
});
Enter fullscreen mode Exit fullscreen mode

This might seem a little complex, but we just clear the list of online peers whenever we listen to the “update-peers” event and then create an HTML template to attach to the DOM with the names and colors of the connected clients, including yourself (which will use myName variable to add an indication that it’s you).

And that’s it! Now if you go run yarn start in your terminal and go to http://localhost:3000 you should see the chat page and if you connect with other browser windows, tabs or devices you will see the growing list of users connected. And if you close those windows, leaving the chat, the list will also update.

I hope you liked this article and will create awesome applications with this new tool under your belt 🍻

Discussion (1)

pic
Editor guide
Collapse
ryansamaeian profile image