WebSockets Unlocked: Mastering the Art of Real-Time Communication
Raunak Gurud
Posted on January 14, 2024
Hey there! ๐ Ready to dive into the exciting world of real-time communication with WebSocket? ๐ Join me in this series where we'll start from the basics, craft awesome chat/video apps, and master scaling with Redis and Kafka. Get set for a journey filled with interactive learning! ๐๐ฌโจ #RealTimeWeb #WebSocketAdventures
Learning about websockets can be a bit tricky at first, but once you get the hang of it, it becomes quite easy. I would definitely recommend taking the time to learn about websockets, as they are an essential part of modern web development and can greatly enhance the real-time capabilities of your applications. Plus, mastering websockets opens up a whole new world of possibilities for interactive and dynamic web experiences. So, don't be discouraged by the initial challenges โ the rewards of learning websockets are well worth the effort!
This blog serves as an introduction to websockets, explaining what they are and why you should consider building your application using websockets. It also covers the difference between http and websockets, as well as the benefits of using websockets.
You can only learn by building so in this blog we will also learn how to build a basic message application using websockets with Socket.io, Node.js, and React.
What is websocket
WebSockets are a communication protocol that provides full-duplex communication channels over a single, long-lived connection. Unlike traditional HTTP connections, which are stateless and involve the client making a request and the server responding, WebSockets allow for bidirectional communication between the client and server. This is built on top of TCP/IP, which means it is reliable and ensures that every packet is received.
HTTP is like sending letters where you ask for something, and then you wait for a reply. WebSocket is like having a phone call where you can talk back and forth instantly. Chat applications, online gaming, financial platforms, and live streaming is build using websockets.
let's look at some benefits of websocket
Full-duplex Communication can be imagine like using walkie-talkies where you and your friend can talk and listen at the same time. Full-duplex is like having a chat where both can speak without waiting, and that's how WebSockets let computers talk back and forth instantly.
Persistent Connection means once you connect your device to the internet (or a server), the link stays open as long as needed. It's like staying on a call without hanging up, making communication faster and more efficient.
Low Latency means there's minimal delay when sending and receiving information. It's like a really fast text conversation.
WebSockets use a
ws://
orwss://
URI scheme, wherews
stands for WebSocket andwss
is WebSocket Secure (encrypted using TLS/SSL).
Playing with websockets
You might be asking where can you start using WebSocket and how can you use this well, Modern web browsers provide a WebSocket API that allows developers to work with WebSockets using JavaScript. This API includes methods for opening a WebSocket connection, sending and receiving messages, and handling events.
Open a web browser (Chrome, Edge, Safari, Brave) and type ctrl + shift + i
to open the developer tools. Then, go to the console tab and write the following command.
// Create a variable called socket with a URL (echo wss)
let socket = new WebSocket("wss://ws.postman-echo.com/raw");
// Define a function to respond to the data received on the socket
socket.onmessage = function(event) {
console.log('Message received: ' + event.data);
};
// Send a message using the socket.send() method
socket.send('Hello, server!');
wss://ws.postman-echo.com/raw (postman echo websocket server)
As you can see, we are creating a variable called "socket" which has a URL (echo wss). We are also defining a function which will respond to the data received on the socket. We are sending a message using the "socket.send()" method.
Now, move to the network tab and find the raw name response. You can see that this raw response is of type websocket.
Clicking on raw will allow you to see additional information about the request, such as the status code and headers.
Click on the message tab to view all the WebSocket messages that have been sent and received. The UpArrow denotes the message sent by us, while the DownArrow denotes the message received. Since we are using an echo server, we are getting the same response (refer to the previous image).
Go ahead and try this out on your own and experiment with it. You can also learn more about the WebSocket API on the MND documentation.
Building a simple chat application
Learning without building is not effective, which is why we are going to create a simple chat application where users can send and receive messages in real time.
This is a basic app. We will build the proper UI in future blogs. Let's start coding.
Make sure you have node installed
node --version
I am using Node.js version v20.10.0. Any version above v18.17 should work.
Create the project folder containing two sub-folders named client and server.
mkdir chat-app
cd chat-app
mkdir client server
Server
Next, navigate into the server folder and create a package.json
file.
cd server
npm init -y
Install Socket.io Server API,We would be using TypeScript for a better developer experience (DX) and take advantage of tsc-watch for constant reloading.
# Add socket.io
npm add socket.io
# Add dev dependencies
npm add -D tsc-watch typescript
# Preinstalled tsc globally
tsc --init
# Change tsconfig.json
"rootDir": "./src",
"outDir": "./dist",
# start the server in dev
npm dev
set scripts in package.json
{
"name": "server",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "tsc-watch --onSuccess \"node dist/index.js\"",
"build": "tsc -p .",
"start": "node dist/index.js"
},
"devDependencies": {
"tsc-watch": "^6.0.4",
"typescript": "^5.3.3"
}
}
Lets start by creating a basic server using the built-in http module and start the server.
# index.ts
import http from "http";
async function init() {
const httpServer = await http.createServer();
const PORT = process.env.PORT ? process.env.PORT : 8000;
httpServer.listen(PORT, () => {
console.log(`http server started at ${PORT}...`);
});
}
init();
npm run dev
if you see this everything is working as expected
Now we will be implementing socket logic using socket.io for building websockets. This library provides an abstraction layer on top of WebSockets, simplifying the process of creating real-time applications. For better maintainability, it is recommended to create a separate file for socket calls. To do this, navigate to the src folder, create a folder named services, and inside it, create a file named socket.ts under the services folder.
Here are the commands to create a class called SocketService which initializes a socket connection. You can obtain this connection by using the getIo()
method and export this class.
# services/socket.ts
import { Server } from "socket.io";
class SocketService {
// Create a variable _io with type Server from socket.io
private _io: Server;
constructor() {
// Log a message when the SocketService is initialized
console.log("init socket server");
// Setup CORS policy to allow all ('*')
this._io = new Server({
cors: {
origin: "*",
allowedHeaders: ["*"],
},
});
}
// Initialize event listeners for the socket connection
public initListeners() {
// Get a reference to the _io instance
const io = this._io;
// Event listener for when a client connects to the WebSocket server
io.on("connect", async (socket) => {
// Log a message when a client connects, including their socket id
console.log(`โก userId ${socket.id} connected`);
// Log a message when socket user is disconnected
socket.on("disconnect", () => {
console.log(`๐ซ userId ${socket.id} disconnected`);
});
});
}
// Getter method to access the _io instance from outside the class
get io() {
return this._io;
}
}
// Export the SocketService class as the default export of this module
export default SocketService;
initListeners()
method This method initializes event listeners for the WebSocket server. In this case, it sets up a listener for the "connect" event, which fires when a client connects to the server. It logs a message indicating the user id (socket id) of the connected client.
get io()
method : This getter method allows external components to access the _io
instance from outside the class.
Now add a event listener for message event this block will run when the server recevies a messages
// Event listener for "event:message"
socket.on("event:message", (msg) => {
// Log the received message to the server console
console.log("message received", msg);
// Emit a "message" event to all clients with the received message
io.emit("message", JSON.stringify(msg));
});
Import the SocketService to create a new socket instance and attach the socket to the server. Then, initialize all the listeners.
# index.ts
import http from "http";
// import socket service
import SocketService from "./services/socket";
async function init() {
const httpServer = await http.createServer();
// initializing the socket server
const socketService = new SocketService();
const PORT = process.env.PORT ? process.env.PORT : 8000;
// attaching the server to the SocketService
socketService.io.attach(httpServer);
httpServer.listen(PORT, () => {
console.log(`http server started at ${PORT}...`);
});
// initialize the socket listeners
socketService.initListeners();
}
init();
start the server
npm run dev
as you can see the socket server is initialize and server is working on 8000 and a user is connected
Server work is done here๐ฅณ๐ฅณ
Client
Navigate to the client folder using your terminal and then create a new Next.js project with Tailwind CSS and TypeScript.
learn more about
cd client
npx create-next-app@latest ./
# options
# tailwindcss typescript app/
Install socket.io-client
, socket.io a library that provides an abstraction layer on top of WebSockets, simplifying the process of creating real-time applications.
npm install socket.io-client
Create a folder called context
and add a file SocketProvider.tsx
. This file serves as a wrapper around the app, allowing us to use the socket from anywhere within the app.
// context/SocketProvider.tsx
import React, {
createContext,
useCallback,
useContext,
useEffect,
useState } from 'react'
import { Socket, io } from 'socket.io-client'
// Interface defining the structure of the SocketContext
interface ISocketContext {
messages: string[]
socket: any
sendMessage: (msg: string) => void
}
const SocketContext = createContext<ISocketContext | null>(null)
// Custom hook for easily accessing the socket context
export const useSocket = () => {
const state = useContext(SocketContext);
if (!state) throw new Error(`state is undefined`);
return state;
}
export const SocketProvider: React.FC<{
children: React.ReactNode
}> = ({ children }) => {
// State to hold the socket instance
const [socket, setSocket] = useState<Socket>()
// To hold all messages in socket connection
const [messages, setMessages] = useState<string[]>([])
// function to send a message from chat
const sendMessage: ISocketContext["sendMessage"] = useCallback(
(msg: string) => {
console.log("sending msg...", msg)
if (!socket) throw new Error("socket not ready")
// emit event to send message from socket
socket?.emit("event:message", { message: msg })
}, [socket])
const onMessageReceived = useCallback((msg: string) => {
const { message } = JSON.parse(msg) as { message: string }
// Set the msg received in the messages state
setMessages((prev) => [...prev, message])
}, [])
// Effect to set up the socket connection when the component mounts
useEffect(() => {
// Create a new socket instance
const _socket = io("http://localhost:8000")
// Set up an event listener for the "message" event
// This function if fired when the `message` is received from the backend
_socket.on("message", onMessageReceived)
// Set the socket instance in the state
setSocket(_socket)
// Cleanup function to disconnect the socket when the component unmounts
// for better performace
return () => {
setSocket(undefined)
_socket.disconnect();
_socket.off("message", onMessageReceived)
}
}, [])
// Provide the socket context to the wrapped components
return (
<SocketContext.Provider value={{ socket, messages, sendMessage }}>
{children}
</SocketContext.Provider>
)
}
The SocketContext defines a context named SocketContext that will hold the socket instance, messages, and a function to send messages.
The SocketProvider Component is a React functional component that wraps the entire application. It sets up the WebSocket connection, manages messages, and provides the socket context to its children.
The useEffect hook runs when the component mounts. It creates a new socket instance, sets up event listeners, and cleans up the socket and listeners when the component unmounts.
The useSocket Hook is a custom hook that uses the useContext hook to access the socket context. It throws an error if the context is not available.
Wrap the socket provider around the app to enable access to the socket from any part of the application.
// layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { SocketProvider } from '@/context/SocketProvider'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
// This socketProvier will provide the state variable to all the
// cihldrens
<SocketProvider>
<body className={inter.className}>{children}</body>
</SocketProvider>
</html>
)
}
Creating a Chat Application Layout and Design Using Tailwind CSS
To create the layout and design for a chat application, we will be using Tailwind CSS. In order to mark the file as a client component, we will utilize socket works as the client component only.
The handleSubmit
function is called when the user submits a message. It checks if the message is not empty, clears the input field, and can potentially handle sending the message .
// page.tsx
"use client"
import { useState } from "react"
import { useSocket } from "@/context/SocketProvider"
export default function Home() {
const [message, setMessage] = useState('')
const { messages, sendMessage } = useSocket()
const handleSubmit = () => {
if (message === "") return;
setMessage('');
sendMessage(message)
}
return (
<div className="m-10 h-[600px] flex flex-col border-2 rounded-md border-white p-2">
<div className="h-full overflow-y-auto flex flex-col p-2 overflow-x-hidden gap-1">
// space for displaying chat
</div>
<div className="flex ">
<input
onChange={(e) => setMessage(e.target.value)}
type="text"
value={message}
placeholder="message..."
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSubmit()
}
}}
className="bg-zinc-700 w-full px-6 py-2 rounded-md"
/>
<button
onClick={() => handleSubmit()}
className="px-6 py-2 border-2 border-white rounded-md"
>
send
</button>
</div>
</div >
)
}
We can import the socket provider that we created earlier to send and display messages. usesocket()
is a custom hook that enables us to interact with the state and function create.
Start the development server and open the browser to view the application at http://localhost:3000.
npm run dev
Open the application in two different tabs in incognito mode to ensure that it is working properly.
๐ฅณYaaaaaaay!!!๐ฅณ
Congratulations on building your first chat application using WebSockets!. It looks like you've provided a detailed guide on setting up a chat application using WebSockets with Socket.io, Node.js, and React. This is a comprehensive overview, covering both server and client implementations, as well as the layout and design using Tailwind CSS. Additionally, you've introduced key concepts like Full-duplex Communication, Persistent Connection, and Low Latency associated with WebSockets.
Here's a summary of the blog:
Introduction to WebSockets: Real-time communication using WebSockets for chat and video applications.
Benefits of WebSockets: Full-duplex Communication, Persistent Connection, Low Latency.
**Practical Example:**Connecting to a WebSocket server using browser developer tools.
Building a Simple Chat Application:
Setting up the server with Socket.io and Node.js.
Creating a basic layout for the chat application using React and Tailwind CSS.
This is just the beginning of your journey into real-time communication. Stay tuned for the next blog where we'll explore the limitations of WebSockets for scaling and how to overcome them.
๐ If you had a blast exploring WebSockets and building that chat app, you're in for a treat! ๐ This is just the beginning of your journey into real-time communication. Stay tuned for the next blog where we'll explore the limitations of WebSockets for scaling and how to overcome them. Don't forget to share this blog with your tech-savvy pals! Together, we're coding our way to greatness. ๐๐ป #TechAdventures #StayCurious"
Follow me @Raunak Gurud โค๏ธโก
Posted on January 14, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 16, 2024
October 16, 2024