Using Socket.IO to create a chatroom application

·

4 min read

Socket.IO is a JavaScript library for real time web applications. It enables real time, bi-directional communication between web clients and servers. It has two parts: a client-side library that runs on the browser, and a server-side library for Node.js.

Socket.IO uses websocket protocol. Websocket is a computer communication protocol providing full duplex communication channel over single TCP connection. Even though Socket.IO is used as a wrapper around websocket it provides many more features, including broadcasting to multiple sockets, storing data associated with each client, and asynchronous I/O.

Endpoint for creating a chatroom:A user entered room name is obtained from the request body on which regex test is done in order to ensure that the name contains only alphabets.The room name is then checked in the database and if the room name is unique and doesn't already exists in the database then the new room name is saved in the database and is given an Id which is in turn is used to let users enter that particular room.

app.post("/chatroom", [checkAuth], async (req, res) => {
  try {
    const { roomName } = req.body;
    const nameRegex = /^[A-Za-z\s]+$/;
    if (!nameRegex.test(roomName))
      throw new Error("Chatroom name should only contain alphabets");
    const chatroomExists = await chatRoom.findOne({ roomName });
    if (chatroomExists)
      throw new Error("Chatroom with that name already exists");

    const Chatroom = new chatRoom({
      roomName,
    });
    await Chatroom.save()
      .then(() => res.send({ message: "Chatroom created successfully" }))
      .catch((err) => res.send({ error: `${err.message}` }));
  } catch (error) {
    res.send({ error: `${error.message}` });
  }
});

IO authentication for the user is then performed.Token is extracted from the socket and matched with the access token secret of the user and then socket is assigned with a Id.

io.use(async (socket, next) => {
  try {
    const token = socket.handshake.query.token;
    const payload = await verify(token, process.env.ACCESS_TOKEN_SECRET);
    socket.userId = payload.userId;
    next();
  } catch (error) {
    console.log(error);
  }
});

On establishing connection,the socket id is matched with the user Id of the user from the database. When the user joins and leaves the room by respective button click or other actions then the socket also joins and leaves the respective room Id and broadcast message is displayed for other users in the room to see who has joined or left the room.

io.on("connection", async (socket) => {
  console.log("connected:" + socket.userId);
  const userz = await ChatUsers.findOne({ _id: socket.userId });
  socket.on("disconnect", () => {
    console.log("Disconnected" + socket.userId);
    socket.broadcast.emit("newMessage", {
      data: `${userz.userName} left the chat!!`,
    });
  });

  socket.on("joinRoom", ({ chatroomId }) => {
    socket.join(chatroomId);
    socket.emit("newMessage", {
      data: `Welcome to the room, ${userz.userName} !!`,
    });
    socket.broadcast
      .to(chatroomId)
      .emit("newMessage", { data: `${userz.userName} joined the chat!!` });
    console.log("A user joined :" + chatroomId);
  });

  socket.on("leaveRoom", ({ chatroomId }) => {
    socket.leave(chatroomId);
    console.log("A user left :" + chatroomId);
    socket.broadcast
      .to(chatroomId)
      .emit("newMessage", { data: `${userz.userName} left the chat!!` });
  });

For exchange of messages,first the length of message is checked and if message exists then the message along with users socket id and name is emitted to the respective room. It is then saved to the database and can be later retrieved.

 socket.on("chatroomMessage", async ({ chatroomId, message }) => {
    if (message.trim().length > 0) {
      const user = await ChatUsers.findOne({ _id: socket.userId });
      const newMessage = new Message({
        chatroom: chatroomId,
        user: socket.userId,
        message,
      });
      io.to(chatroomId).emit("newMessage", {
        message,
        name: user.userName,
        userId: socket.userId,
      });
       await newMessage.save()
    }
  });
});

To setup socket in front end, first users authenticity is checked using access token.Then the socket is created using token as the query parameter.Then upon connecting and disconnecting ,the socket variable is set to the created socket or null respectively.

const setupSocket = () => {
    const token = user.accessToken;
    if (token && !socket) {
      const newSocket = io("https://app.com", {
        query: {
          token: user.accessToken,
        },
        transports: ["websocket"],
      });

      newSocket.on("disconnect", () => {
        setSocket(null);
        setTimeout(setupSocket, 3000);
        console.log("socket disconnected");
      });

      newSocket.on("connect", () => {
        console.log("success");
      });
      setSocket(newSocket);
    }
  };

To join and leave room ,useEffect hook can be used which on the first render checks for socket and if exists then sends socket as join room with the chatroom id. Return function for the useEffect will send socket as leave room with the chatroom Id.

useEffect(()=>{
        if(socket){
            socket.emit('joinRoom',{
                chatroomId
            })
        }
        return()=>{
            if(socket){
                socket.emit('leaveRoom',{chatroomId})   
            }
        }
    },[])

For transmitting message,if socket exists then the input message is stored into the message array which is then displayed. It is done inside useEffect which updates the array everytime message variable changes so with every message sent or received the UI re-renders to show the new message.

 useEffect(()=>{
        if(socket){
            socket.on('newMessage',(messageInput)=>{
                const newMessages=[...messages,messageInput]
                setMessages(newMessages)
            })
        }

        return()=>{
            if(socket){
            socket.off('newMessage')}
        }}
    ,[messages])