Learn socket.io in 30 Minutes
Learn socket.io in 30 Minutes

In this post you will learn everything that you need to know about websockets and socket.io for your everyday use.

In this tutorial you will learn:
- What are websockets
- Emitting and subscribing for events
- Broadcasting messages
- Private messaging
- Rooms functionality

What are websockets?

The first question is what are websockets? Why do we need them at all if we can do simple API requests. Actually when we make just API request from client to backend it is just a single request. Which means our client sends some information to the server. And our backend will respond with some information. And this is it. It is an end of the communication between client and server.

If you will open Devtools on any website you will see in Network tab a lot of requests. All this are just normal API requests.

Network requests

The main point is that the request is completely stateless. After end of the request backend doesn't know anything about that specific client. This request is already done. If the same client sends the next request then it is completely new request and only the information which is provided inside the request can show backend what customer it is.

With websockets it is completely different. We have client - backend communication. It means that we have an open connection between our client and backend. Which means we don't close our request after backend responded with data. This request is still hanging there and until this request will be closed we can transfer some data from the client to backend or back.

This is extremely efficient if you need to transfer lots of data (Chat systems, Real-time communication, games)

The most popular solution to work with websockets inside Javascript is a socket.io library. This is a library for frontend and backend which means it covers full implementation of websockets in the project.

Project

Here I already prepared a small project for us. First of all we have 2 installed packages. Express and socket.io for our node server. Our server.js file is completely empty and we need to implement it.

But I also prepared some markup to implement our chat features.


<!DOCTYPE html>
<html>
  <head>
    <title>This is the title of the webpage</title>
  </head>
  <body>
    <div>
      <input placeholder="Enter your name" class="name" />
      <button class="login">Login</button>
    </div>
    <br />
    <div>
      <input placeholder="Send to" class="recipient" />
      <input placeholder="Message" class="message" />
      <button class="send">Send</button>
    </div>
    <br />
    <div>
      <input placeholder="Enter room name" class="room" />
      <button class="join">Join room</button>
    </div>

    <script>
      const $loginBtn = document.querySelector(".login");
      const $nameInput = document.querySelector(".name");
      const $sendBtn = document.querySelector(".send");
      const $recipientInput = document.querySelector(".recipient");
      const $messageInput = document.querySelector(".message");
      const $roomInput = document.querySelector(".room");
      const $joinBtn = document.querySelector(".join");
      $loginBtn.addEventListener("click", () => {
        console.log("login as", $nameInput.value);
      });

      $sendBtn.addEventListener("click", () => {
        console.log("send to", $recipientInput.value, $messageInput.value);
      });

      $joinBtn.addEventListener("click", () => {
        console.log("join", $roomInput.value);
      });
    </script>
  </body>
</html>

Initial markup

As you can see we have 5 inputs and 3 buttons. Also you can see event listeners inside our html page. So for example when we type a name and hit login we get a console.log message.

Now it's time to create our server. And actually there are different ways to create our server with socket.io. By default it is really easy you can just start a web server of socket.io. But it is not a production solution. For production use we want to mix Express with Socket.io because it is nice to have routing, serving static files and having socket.io. This is why I want to show you more complicated variant but it is suitable for production.

Setting up socket.io

For this we must jump to server.js file.

const express = require("express");
const app = express();
const http = require("http");
const server = http.createServer(app);
const { Server } = require("socket.io");
const io = new Server(server);

It might look scary but this is a production variant. We want to mix here express, http and socket.io.

  • We create express server
  • We create http server with app from express
  • We create new socket.io server where we provide our http server

With this approach we get possibility to use everything from express, start a webserver and add socket.io subscription. Now let's start our server.

server.listen(3000, () => {
  console.log("started");
});

What we can also do it serve our html file through Express.

app.get("/", (req, res) => {
  res.sendFile(__dirname + "/index.html");
});

And last but not least is adding our socket.io events.

io.on("connection", (socket) => {
  console.log("connected");
});

We used here io to create a connection. He have io.on where we can provide the name of our event and a callback. But io connection is just a backend part. Now we must establish our connection on client. For this inside index.html we must include socket.io javascript file.

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

This line will create io property inside window. Now at the beginning of our script we want to call it.

<script>
  const socket = io();
  ...
</script>

Now let's start our webserver with yarn start. We can just in http://localhost:3000 and we see our index.html page. Also inside node console we get connected message. It means that we successfully created a socket.io connection on client and on server. Inside window we have our created socket property.

Socket property

Inside we have lot's of useful information but the most interesting for us is ID. This is our unique identifier of this specific socket connection.

It's important to remember that this socket ID is just unique for this tab. If we duplicate a tab then we get there another connection.

It's not unique in browser or in your operational system but only in browser tab.

Also it is extremely important to call io() just once. If you will call it twice this means you have 2 socket connections and everything is wrong because your connected client is not unique and your backend doesn't really know what client connected to it.

Implementing Login

Now let's start to do some events. For example let's say that we want to login a user in our system with websockets. And actually it would be nice to set a name of the user before loginning. So we can type our name and hit login and we get a console.log message with our name.

To send the message from our client to the backend we use socket.emit.

$loginBtn.addEventListener("click", () => {
  console.log("login as", $nameInput.value);
  socket.emit("login", $nameInput.value);
});

As a first parameter we provided a unique name for our event. In our case it's login. As a second parameter we can provide some additional data but it is not mandatory. For our case it is important because we must pass filled username to our backend.

But it is not enough as our backend doesn't know anything about this event. Let's open our server.js and add a listener for this event.

io.on("connection", (socket) => {
  console.log("connected");
  socket.on("login", (name) => {
    console.log("login", name);
  });
});

Here we used socket.on to subscribe to this specific event. As you can see it's the same unique name login and a callback which will be called without or data that we passed.

Now when we reload our webserver and login in browser we can see a console.log in our server. So to create and even we must

  • Do socket.emit on client
  • Subscribe on server with socket.on

Now inside our console.log we have access not only to the name but also to socket.id. As you can see on the top in our io.connection we get a socket which is exactly the same socket that we saw on client.

console.log('login', name, socket.id)

This socket.id is exactly what we use as our unique identifier for connected client. In this case we know what client sent us a message.

Broadcasting

Now we successfully notified our backend about new login. But it's important to remember that our client can't send a message directly to other clients. Which means we can't notify other clients about new login from one of the client. When we want to send a message to all other clients (for example when user was logged in) it means that first we must send an event to the backend and then backend must notify all our clients.

// server.js
socket.on("login", (name) => {
  console.log("login", name);
  io.emit('new-login', name)
});

We used here io.emit in backend just like we did on client. But this line will notify all our client with this event. But it won't work as our clients are not subscribed to this new event new-login. For must add on subscription on the client.

// index.html
socket.on("new-login", (name) => {
  console.log("new-login", name);
});

The code is exactly the same like in server to subscribe to event from backend on client.

We try what we did I will open 2 tabs. We need to login in first tab and then in second tab we will be notified about our new login.

new login

As you can see in both tabs after loginning the user we get a console.log. Which actually means io.emit on backend sends the message to all our client and we can subscribe to it on client with socket.on.

But here is a problem. As you can see even our user who just logged in received this even. It doesn't make any sense as our client that just logged in for sure knows that we logged in. We don't need to notify this user back. We just need to send messages to all other users.

This is why instead of io.emit we must use socket.broadcast.emit.

socket.broadcast('new-login', name)

The main difference is that socket.broadcast exclude current client. This is exactly why it is a method of socket and not io. In this case socket.io knows what client to exclude.

As you can see now in browser our client that just logged in was not notified about new login.

socket broadcast

So almost always you want in your application to use socket.broadcast and not io.emit.

Private messages

Now we need to talk about private messages. It can be completely possible that you need to implement logic about sending secure private messages. What does it mean? We can't really use broadcast because it will send this even to everybody. Instead we want to send out message to a specific client. The typical project example for private messages is a chat or any application where you need to notify specific users about something new.

As you can see we have 2 fields in html to send a private message. We need to write a socketID where we want to send a message and our message. In real application you would you just name of the user which will be resolved in backend to socketID. For simplicity in our application we will send a message directly to other socketID.

First of all we must emit a new message on the client like we did previously.

// index.html
$sendBtn.addEventListener("click", () => {
  console.log("send to", $recipientInput.value, $messageInput.value);
  socket.emit("send-to", {
    recipient: $recipientInput.value,
    message: $messageInput.value,
  });
});

Here we provided an object as our data because we have more than one field to provide.

Now our backend must subscribe to this event.

// server.js
socket.on("send-to", (params) => {
  console.log("send-to", params);
});

As you can see it works and we successfully received our data on the backend. Now we must send this message to a specific recipient because as I said previously we can't send message directly from one client to another.

socket.on("send-to", (params) => {
  console.log("send-to", params);
  socket.to(params.recipient).emit("private-message", {
    message: params.message,
    sender: socket.id,
  });
});

Here we used socket.to(socketID).emit where we pass a socketID inside to send a message to a specific client. As you can see it works exactly the same like normal emit. Inside we pass our message and a sender which is a socketID of our client who send this message. This is needed to show who send a message. In real application you might have a username there instead of socketID.

Now we must subscribe to this event on the client as this is our new event.

socket.on("private-message", (params) => {
  console.log("private-message", params);
});

After restarting our webserver we must open 2 tabs and take a socketID in second tab from socket.id. Now in first tab we can provide this socketID and a message.

Private message

And actually this message was completely private. It is happened because our clients didn't communicate but it was our backend who send this message to a specific user.

Rooms

One more thing that I want to show you is called rooms. Rooms is a super broad concept that allows us to send messages to a list of different users. For example your chat application is divided is different chats that people can join. Which means you want to send a message to all people in the specific chat. For this we have rooms feature.

What is a room? It is just a unique string that we can join and send there our messages later. As you can see in our html we have a room name input and a button to join room. After we joined the room we can send messages to this specific room.

First of all let's implement joining the room.

It is important to remember that rooms are just server side concept. You don't have access to the rooms in client.

So first of all our client must notify our backend that we want to join the room.

// index.html
$joinBtn.addEventListener("click", () => {
  console.log("join", $roomInput.value);
  socket.emit("join-room", $roomInput.value);
});

As you can see we emited a new event with room name from the client. Now we must subscribe to this event inside our server.

// server.js
socket.on("join-room", (roomName) => {
  console.log("join", roomName);
  socket.join(roomName);
});

Here we subscribed to our join-room event and we called socket.join which will add a user to this specific unique room string.

After this you typically want to notify your client that he successfully join a room.

socket.on("join-room", (roomName) => {
  console.log("join", roomName);
  socket.join(roomName);
  io.to(socket.id).emit("joined-room");
});

Here we used io.to(socket.id) which sends an event to specific user. As you can see we used here io.to and not socket.to as we can't send a message to the same socketID.

Now we must subscribe to this emit on our client.

// index.html
socket.on("joined-room", () => {
  console.log("successfully joined");
});

As you can see in browser after we join the room only our client who joins gets this information.

Join room

Now it would be nice to notify all users in this room that our new user join a room.

socket.on("join-room", (roomName) => {
  console.log("join", roomName);
  socket.join(roomName);
  io.to(socket.id).emit("joined-room");
  io.to(roomName).emit("public-message", `New user ${socket.id}`);
});

Here we used io.to but we provided a roomName inside. In this case this event will be send to all users who joined this room. But in order to make it working we must subscribe to this event in the client.

// index.html
socket.on("public-message", (message) => {
  console.log(message);
});

Now all users inside this room got information about new user joined the room.

Public message

Conclusion

So this was everything that you need to know about socket.io to use it in your next project. And actually if you are interested to see how we use socket.io in the real project that we build together make sure to check this post also.

📚 Source code of what we've done