How to write a chat app using Socket.io: A deep dive into Lounge Chat

Quod AIQuod AI
16 min read

Have you heard of IRCs before? IRC stands for Internet Relay Chat and is a protocol that allows users to install a client application on their systems or on the web browser and chat to other clients through a central server. Most IRC clients are used for communication on forums, but they facilitate one-to-one communication as well. Today, we will look at one of the most popular IRC clients, The Lounge Chat, which makes use of Socket.IO to handle bi-directional communication. Without further ado, let’s get going!

This article was originally posted at: https://quod.ai/post/how-to-write-a-chat-app-using-socket-io-a-deep-dive-into-lounge-chat

Objectives

In this deep-dive, we will mainly be looking at Socket.IO and how it enables bi-directional real-time communication in The Lounge Chat through its event mechanism. The article will be structure in the following way:

1. Socket.IO and Lounge Chat

  a.  Socket.IO - How does it work?
  b.  Connection between users

2. Socket.IO Events

a.  Auth Event
b.  Join Event
c.  Mentions Event 

Socket.IO and Lounge Chat

Socket.IO is a JavaScript library that enables applications to use WebSockets to provide real-time, low-latency, bi-directional communication. It is a very lightweight wrapper over the original WebSocket API, therefore, it allows you to stray away from conventional WebSocket code, and use a streamlined API. Socket.IO works through events that are emitted between the client and server. Both the client and server can emit events as well as listen to them and use a plethora of data structures as well as any number of arguments. We will look at the syntax in detail in the events section.

Lounge Chat is an entirely free and open-source chat application that uses the IRC protocol. The application uses Vue.js as the frontend library and has Socket.IO for all the communication between client and server. You can set the application using this guide, which is pretty comprehensive. Since this is not a complete guide on how to build it from scratch, we assume some proficiency of using these libraries.

Socket.IO - How does it work?

Like we said earlier, Socket.IO is a lightweight wrapper over the original WebSocket API. Websocket connections take place when the client sends a connection request to the server using an HTTP request. The server responds with a 101 code establishing a full duplex connection. When the client disconnects, the communication link is dropped. Websockets can be thought of as a very low level implementation of how you can establish two-way communication. But with Socket.IO, we have a library that has a lot of additional functionalities and an easy to use API to do this. Below is a small intro on how Socket.IO works. We have the server code first, followed by the client code.

var server = require("net").createServer();
var io = require("socket.io")(server);


var handleClient = function (socket) {
    socket.emit("message", {msg: "Hello World"});
};


io.on("connection", handleClient);


server.listen(4000);

We are going to start from the lines below and then explain the function.

Line 7: We are starting the server on port number 4000.
Line 5: We are creating a Socket.IO server instance which has been attached to the server during the imports itself. We are listening for the “connection” event, which is fired when the client visits the server page at port 4000. Once the connection is established we invoke the handleClient() method. This is used to fire a message using the socket.emit() method. Here, the event name is “message” and we pass a simple JSON with the message “Hello World”.

socket.on("message", function(sample) {
    console.log("Message:", sample.msg);
});

Line 1-3: This bit of code can be added to any of your client applications. We are listening for the event “message” from anyone who is connected through Socket.IO. Once you visit the server running on port 4000, the connection event will be invoked. This will fire the “message” event from the server sending the message to the client and since the client is listening in for the “message” event, it will log it to the console.

Socket.IO works entirely based on this logic, where there is a connection establishment at the onset and event based triggering of listeners and methods. The best part is that both parties can send and receive these.

Sending and receiving messages

The Lounge Chat has a public mode as well as a private mode. The public mode allows anyone to join a public chat room. The default connection details are all pre-populated, so you can get started right away. It runs on localhost:9000 when you first install it. At a very high level, this is how the chats between the clients and servers work in The Lounge Chat.

socket.emit("input", {target, text});

Line 1: This line of code emits the text entered by the user in the chat box and it has the event name as “input”. The value of the field target is the channel Id and the text field is the actual contents of what the user typed in. Channel is nothing but the name entered into the “Channels” field while connecting to The Lounge Chat. In public mode, the default channel is “#thelounge”.

    socket.on("input", (data) => {
        if (_.isPlainObject(data)) {
            client.input(data);

Inside the src/server.js file, we have the listener for incoming messages from the client. This is shown in the gist above.

Line 1-3: The first line creates a Socket.IO listener for the “input” event and has a callback function with an argument “data”. This is then checked using the lodash function isPlainObject and this is set as the input data for the client.

Socket.IO Events

Below, we have some of the chat related events that are used between the client and the server in The Lounge Chat. There’s the “auth” event which is used for authentication, the “join” event for when the user wants to join particular channels and the “mentions” event, wherein the user mentions other users’ names in chat.

Auth Event

"use strict";
import socket from "../socket";
import storage from "../localStorage";
import {router, navigate} from "../router";
import store from "../store";
import location from "../location";
let lastServerHash = null;


socket.on("auth:success", function () {
    store.commit("currentUserVisibleError", "Loading messages…");
    updateLoadingMessage();
});


socket.on("auth:failed", function () {
    storage.remove("token");
    if (store.state.appLoaded) {
        return reloadPage("Authentication failed, reloading…");
    }
    showSignIn();
});


socket.on("auth:start", function (serverHash) {
    // If we reconnected and serverHash differs, that means the server restarted
    // And we will reload the page to grab the latest version
    if (lastServerHash && serverHash !== lastServerHash) {
        return reloadPage("Server restarted, reloading…");
    }
    lastServerHash = serverHash;
    const user = storage.get("user");
    const token = storage.get("token");
    const doFastAuth = user && token;


    // If we reconnect and no longer have a stored token, reload the page
    if (store.state.appLoaded && !doFastAuth) {
        return reloadPage("Authentication failed, reloading…");
    }
    // If we have user and token stored, perform auth without showing sign-in first
    if (doFastAuth) {
        store.commit("currentUserVisibleError", "Authorizing…");
        updateLoadingMessage();
        let lastMessage = -1;
        for (const network of store.state.networks) {
            for (const chan of network.channels) {
                if (chan.messages.length > 0) {
                    const id = chan.messages[chan.messages.length - 1].id;
                    if (lastMessage < id) {
                        lastMessage = id;
                    }
                }
            }
        }
        const openChannel =
            (store.state.activeChannel && store.state.activeChannel.channel.id) || null;
        socket.emit("auth:perform", {
            user,
            token,
            lastMessage,
            openChannel,
            hasConfig: store.state.serverConfiguration !== null,
        });
    } else {
        showSignIn();
    }
});

View auth.js in context at Quod AI

Line 1-7: These lines contain all the imports needed within the event. On the last line we have a variable called lastServerHash which we will be using further down.Line 9-12: This sets a socket event listener for the auth:success event which shows the message “Loading messages” followed by the invocation of the updateLoadingMessages() method.Line 14-20: These lines set a socket event listener for the auth:failure event. Firstly, it removes the token field from the local storage. This will unauthorize any existing users. If the app’s state is currently loaded, then we will reload the page stating that authentication has failed. Otherwise, we proceed to showing the sign in page for the user to login again.
Line 22-64: This chunk of code contains the main logic for authentication. It sets up a socket event listener for the auth:start event with a callback function. When the serverHash is received from the server upon starting this event, it is checked with the currently available server hash. If they are not equal, we need to reload the page to get the latest server hash.
If it is the first time being executed, the server hash is stored in the lastServerHash variable. Then, we get the user and token fields from the local storage. If both of them do not exist, we directly go for the sign in page that is shown from lines 61-63. Otherwise, we set a flag known as doFastAuth to true.
If this flag is true, it means that the local storage has a user’s information already and we can log them in directly. We then set the currentUserVisibleError field in the state to “Authorizing” and invoke the updateLoadingMessage() method. This message simply displays the message that is present in the currentUserVisibleError field in the state.
The next few lines are used to check for the existing networks in the store and then pick out the channel id and the last message from the particular channel. We then create a constant openChannel to store the channel using the existing channel id. Lastly, we emit an event auth:perform passing in the user, token, last message, the open channel and whether the state has config information or not.

Join Event

"use strict";
import socket from "../socket";
import store from "../store";
import {switchToChannel} from "../router";


socket.on("join", function (data) {
    store.getters.initChannel(data.chan);
    const network = store.getters.findNetwork(data.network);
    if (!network) {
        return;
    }
    network.channels.splice(data.index || -1, 0, data.chan);
    // Queries do not automatically focus, unless the user did a whois
    if (data.chan.type === "query" && !data.shouldOpen) {
        return;
    }
    switchToChannel(store.getters.findChannel(data.chan.id).channel);
});

View join.js in context at Quod AI

Line 1-4: We import the Vuex store that holds the entire application state. On the fourth line, we import the switchToChannel(channel) function from the router.js file.
Line 6-12: We use the socket.on() method to listen for join events and execute the callback that follows. We initialize a channel on the Vuex store using the data that is received. We then check for existing networks on the store using the data.network field to see if they match. If not, we return. Otherwise, we pick out the first network element in the array using the splice function.
Line 14-18: We then check if the channel is of type query and if the data should be opened. If not, then again, we return. Otherwise, we proceed to switch to that channel using the switchToChannel function that we imported from the router. This will load the RoutedChat Vue component.

Mentions Event

This is the event that is used when a client mentions other usernames on his chat. This event also a client side event that listens for events from the server as well as a server side event that sends events. The mentions are stored into the Vuex store in an array called “mentions”.

socket.on("mentions:list", function (data) {
    store.commit("mentions", data);

Line 1-2: On the client side we create a socket event listener for any events that come with the name “mentions”. It has a callback function that gets executed which stores the list of mentions in the Vuex store. Like with any store, any change in the data, will lead to updation of all the components that use that data.

    socket.on("mentions:get", () => {
        socket.emit("mentions:list", client.mentions);

Line 1-2: The server side has a socket event listener for “mentions:get” event which is emitted from the Vue component. This event listener has a callback that triggers the “mentions:list” event on the client updating the store of the client with the list of mentions.

Want to learn and contribute to Lounge Chat? Check out Quod AI’s real time documentation, powered by AI at https://beta.quod.ai/github/thelounge/thelounge.

Quod AI is code search and navigation on steroids. We turn code into documentation that developers actually use. Do follow us on twitter @quod_ai for updates on our product and DEVs community content. Check our app at: beta.quod.ai

0
Subscribe to my newsletter

Read articles from Quod AI directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Quod AI
Quod AI

The smartest way to search & navigate code. Turn code repositories into real-time documentation using AI.