Learnwizy Technologies Logo

Learnwizy Technologies

Class 39: WebSockets & Real-time Communication

So far, our web applications have primarily relied on the traditional HTTP request-response model. This model is excellent for fetching data, submitting forms, and navigating pages. However, for applications that require instant updates, live interactions, or continuous data streams, HTTP's unidirectional, request-driven nature becomes a limitation.

Today, we'll introduce WebSockets, a powerful protocol that enables true real-time, bidirectional communication between clients and servers.


The Need for Real-time Communication


Introduction to the WebSocket Protocol

WebSocket Handshake

Building a WebSocket Server with Node.js

We'll use the ws library, a simple and fast WebSocket implementation for Node.js. For more advanced features like automatic reconnections, rooms, and fallbacks, Socket.IO is often preferred.

Using the ws library:

npm install ws
// server.js (WebSocket server with 'ws')
const WebSocket = require('ws');
const http = require('http'); // We'll use http server to host WebSocket

const server = http.createServer((req, res) => {
  // Handle regular HTTP requests if any, otherwise just for WebSocket handshake
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('WebSocket server is running');
});

const wss = new WebSocket.Server({ server }); // Create WebSocket server on top of HTTP server

wss.on('connection', ws => {
  console.log('Client connected');

  // Event listener for messages from the client
  ws.on('message', message => {
    console.log(`Received message from client: ${message}`);

    // Send a message back to the client
    ws.send(`Server received: ${message}`);

    // Broadcast message to all connected clients
    wss.clients.forEach(client => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(`Broadcast: ${message}`);
      }
    });
  });

  // Event listener for when a client disconnects
  ws.on('close', () => {
    console.log('Client disconnected');
  });

  // Event listener for errors
  ws.on('error', error => {
    console.error('WebSocket error:', error);
  });
});

const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
  console.log(`WebSocket server listening on port ${PORT}`);
});

Run this server using node server.js.

Introduction to Socket.IO:

Socket.IO is a library that enables real-time, bidirectional, event-based communication. It uses WebSockets when available and falls back to other transport methods (like long polling) if WebSockets are not supported by the client or server. It also provides features like automatic reconnection, namespaces, and rooms.

npm install socket.io
// server.js (WebSocket server with Socket.IO)
const express = require('express');
const http = require('http');
const { Server } = require('socket.io'); // Import Server from socket.io

const app = express();
const server = http.createServer(app); // Create HTTP server from Express app
const io = new Server(server, {
  cors: {
    origin: "*", // Allow all origins for simplicity (adjust in production)
    methods: ["GET", "POST"]
  }
});

// Basic Express route (optional)
app.get('/', (req, res) => {
  res.send('Socket.IO server is running!');
});

// Socket.IO connection handling
io.on('connection', (socket) => {
  console.log(`User connected: ${socket.id}`);

  // Listen for a 'chat message' event from the client
  socket.on('chat message', (msg) => {
    console.log(`Message from ${socket.id}: ${msg}`);
    // Emit the message to all connected clients
    io.emit('chat message', `${socket.id}: ${msg}`);
  });

  // Listen for a 'disconnect' event
  socket.on('disconnect', () => {
    console.log(`User disconnected: ${socket.id}`);
  });
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Socket.IO server listening on port ${PORT}`);
});

Implementing a WebSocket Client in React

Now let's create a simple React component to connect to our WebSocket server.

Using the native WebSocket API:

// src/components/NativeWebSocketChat.jsx
import React, { useState, useEffect, useRef } from 'react';

function NativeWebSocketChat() {
    const [messages, setMessages] = useState([]);
    const [input, setInput] = useState('');
    const ws = useRef(null); // Use useRef to persist WebSocket instance

    useEffect(() => {
    // Establish WebSocket connection
    ws.current = new WebSocket('ws://localhost:8080'); // Connect to your 'ws' server

    ws.current.onopen = () => {
        console.log('WebSocket connected');
        setMessages(prev => [...prev, { type: 'system', text: 'Connected to chat!' }]);
    };

    ws.current.onmessage = (event) => {
        console.log('Message from server:', event.data);
        setMessages(prev => [...prev, { type: 'server', text: event.data }]);
    };

    ws.current.onclose = () => {
        console.log('WebSocket disconnected');
        setMessages(prev => [...prev, { type: 'system', text: 'Disconnected from chat.' }]);
    };

    ws.current.onerror = (error) => {
        console.error('WebSocket error:', error);
        setMessages(prev => [...prev, { type: 'system', text: `Error: ${error.message || 'Unknown error'}` }]);
    };

    // Cleanup function: close WebSocket connection when component unmounts
    return () => {
        if (ws.current) {
        ws.current.close();
        }
    };
    }, []); // Empty dependency array means this effect runs once on mount

    const sendMessage = () => {
    if (ws.current && ws.current.readyState === WebSocket.OPEN && input.trim()) {
        ws.current.send(input);
        setMessages(prev => [...prev, { type: 'self', text: `You: ${input}` }]);
        setInput('');
    }
    };

    return (
    <div style={{ border: '1px solid #ccc', padding: '15px', maxWidth: '400px', margin: '20px auto' }}>
        <h2>Native WebSocket Chat</h2>
        <div style={{ height: '200px', overflowY: 'scroll', border: '1px solid #eee', padding: '10px', marginBottom: '10px' }}>
        {messages.map((msg, index) => (
            <p key={index} style={{ margin: '5px 0', color: msg.type === 'system' ? 'gray' : (msg.type === 'self' ? 'blue' : 'green') }}>
            {msg.text}
            </p>
        ))}
        </div>
        <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyPress={(e) => { if (e.key === 'Enter') sendMessage(); }}
        placeholder="Type a message..."
        style={{ width: 'calc(100% - 70px)', padding: '8px', marginRight: '5px' }}
        />
        <button onClick={sendMessage} style={{ padding: '8px 12px' }}>Send</button>
    </div>
    );
}

export default NativeWebSocketChat;

Integrating with Socket.IO client library:

npm install socket.io-client
// src/components/SocketIOChat.jsx
import React, { useState, useEffect, useRef } from 'react';
import io from 'socket.io-client'; // Import socket.io-client

function SocketIOChat() {
    const [messages, setMessages] = useState([]);
    const [input, setInput] = useState('');
    const socketRef = useRef(null); // Use useRef for socket instance

    useEffect(() => {
    // Connect to the Socket.IO server
    socketRef.current = io('http://localhost:3000'); // Connect to your Socket.IO server

    socketRef.current.on('connect', () => {
        console.log('Socket.IO connected:', socketRef.current.id);
        setMessages(prev => [...prev, { type: 'system', text: `Connected with ID: ${socketRef.current.id}` }]);
    });

    // Listen for 'chat message' event from the server
    socketRef.current.on('chat message', (msg) => {
        console.log('Received message:', msg);
        setMessages(prev => [...prev, { type: 'other', text: msg }]);
    });

    socketRef.current.on('disconnect', () => {
        console.log('Socket.IO disconnected');
        setMessages(prev => [...prev, { type: 'system', text: 'Disconnected from chat.' }]);
    });

    socketRef.current.on('connect_error', (err) => {
        console.error('Socket.IO connection error:', err.message);
        setMessages(prev => [...prev, { type: 'system', text: `Connection Error: ${err.message}` }]);
    });

    // Cleanup function: disconnect socket when component unmounts
    return () => {
        if (socketRef.current) {
        socketRef.current.disconnect();
        }
    };
    }, []); // Empty dependency array means this effect runs once on mount

    const sendMessage = () => {
    if (socketRef.current && input.trim()) {
        socketRef.current.emit('chat message', input); // Emit 'chat message' event
        setMessages(prev => [...prev, { type: 'self', text: `You: ${input}` }]);
        setInput('');
    }
    };

    return (
    <div style={{ border: '1px solid #ccc', padding: '15px', maxWidth: '400px', margin: '20px auto' }}>
        <h2>Socket.IO Chat</h2>
        <div style={{ height: '200px', overflowY: 'scroll', border: '1px solid #eee', padding: '10px', marginBottom: '10px' }}>
        {messages.map((msg, index) => (
            <p key={index} style={{ margin: '5px 0', color: msg.type === 'system' ? 'gray' : (msg.type === 'self' ? 'blue' : 'black') }}>
            {msg.text}
            </p>
        ))}
        </div>
        <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyPress={(e) => { if (e.key === 'Enter') sendMessage(); }}
        placeholder="Type a message..."
        style={{ width: 'calc(100% - 70px)', padding: '8px', marginRight: '5px' }}
        />
        <button onClick={sendMessage} style={{ padding: '8px 12px' }}>Send</button>
    </div>
    );
}

export default SocketIOChat;

To use these components, import them into your App.jsx and render them.


Real-time Features Examples


Considerations for Real-time Applications

WebSockets open up a new dimension for web applications, enabling rich, interactive, and truly real-time experiences. While they add complexity, for applications demanding instant communication, they are an indispensable tool. In our final class, we'll discuss the critical topic of testing web applications.