WebSockets

Real-time, bidirectional communication between client and server

← Back to Index

What are WebSockets?

WebSocket is a communication protocol that provides full-duplex (two-way) communication channels over a single, long-lived TCP connection. It represents a fundamental shift from HTTP's request-response model to enable true real-time communication on the web.

WebSockets were standardized in 2011 (RFC 6455) to solve a problem that HTTP couldn't address well: server-initiated communication. With HTTP, the server can only respond to client requests—it can never proactively send data to the client. This limitation made building real-time applications like chat, gaming, and live dashboards difficult and inefficient.

The Problem WebSockets Solve

Before WebSockets, developers used workarounds to simulate real-time communication:

WebSockets provide a clean solution: a single persistent connection where both sides can send messages at any time.

Real-World Examples

You encounter WebSockets frequently, even if you don't realize it:

Key Difference from HTTP:

  • HTTP: Client asks, server responds. The server can NEVER initiate communication. Each request requires a new connection setup (in HTTP/1.1 without keep-alive).
  • WebSocket: Both parties can send messages anytime over a persistent connection. The server can push data to clients without being asked. Low overhead per message.
// HTTP: Request-Response (Half-duplex)
Client ──Request──> Server
Client <──Response── Server
Client ──Request──> Server
Client <──Response── Server
// Client must always initiate!

// WebSocket: Full-duplex
Client <────────────> Server
       ↑            ↑
       │            └── Server can push anytime
       └─────────────── Client can send anytime

// Perfect for: Chat, gaming, live updates, notifications

WebSocket vs HTTP

Aspect HTTP WebSocket
Communication Request-Response Bidirectional
Connection New connection per request Persistent connection
Initiation Client only Either side
Overhead Headers on every request Minimal frame overhead
Latency Higher (connection setup) Lower (persistent)
Use Case CRUD operations, static content Real-time updates, streaming

How WebSockets Work

// WebSocket Lifecycle

┌──────────────────────────────────────────────────────────────────┐
│                      1. HANDSHAKE (HTTP Upgrade)                  │
├──────────────────────────────────────────────────────────────────┤
│                                                                   │
│  Client Request:                                                  │
│  GET /chat HTTP/1.1                                              │
│  Host: server.example.com                                        │
│  Upgrade: websocket            ← Request protocol upgrade         │
│  Connection: Upgrade                                             │
│  Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==                    │
│  Sec-WebSocket-Version: 13                                       │
│                                                                   │
│  Server Response:                                                 │
│  HTTP/1.1 101 Switching Protocols                                │
│  Upgrade: websocket                                              │
│  Connection: Upgrade                                             │
│  Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=             │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│                   2. CONNECTION ESTABLISHED                       │
├──────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌─────────┐           WebSocket           ┌─────────┐           │
│  │ Client  │ <═══════════════════════════> │ Server  │           │
│  └─────────┘      Bidirectional            └─────────┘           │
│                                                                   │
│  - Both can send messages anytime                                │
│  - Low overhead per message (2-14 bytes)                         │
│  - Connection stays open                                         │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│                      3. DATA EXCHANGE                             │
├──────────────────────────────────────────────────────────────────┤
│                                                                   │
│  Client: {"type": "message", "text": "Hello!"}                   │
│  Server: {"type": "message", "from": "john", "text": "Hello!"}   │
│  Server: {"type": "notification", "count": 5}                    │
│  Client: {"type": "typing", "user": "jane"}                      │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│                      4. CONNECTION CLOSE                          │
├──────────────────────────────────────────────────────────────────┤
│                                                                   │
│  Either side sends close frame                                   │
│  Other side acknowledges                                         │
│  TCP connection terminates                                       │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘

WebSocket in JavaScript (Client)

// Basic WebSocket connection
const socket = new WebSocket('wss://api.example.com/ws');

// Connection opened
socket.onopen = function(event) {
    console.log('Connected to WebSocket!');

    // Send a message
    socket.send(JSON.stringify({
        type: 'join',
        room: 'general'
    }));
};

// Listen for messages
socket.onmessage = function(event) {
    const data = JSON.parse(event.data);
    console.log('Received:', data);

    if (data.type === 'message') {
        displayMessage(data);
    } else if (data.type === 'notification') {
        showNotification(data);
    }
};

// Connection closed
socket.onclose = function(event) {
    if (event.wasClean) {
        console.log('Connection closed cleanly');
    } else {
        console.log('Connection died');
    }
    console.log('Code:', event.code, 'Reason:', event.reason);
};

// Connection error
socket.onerror = function(error) {
    console.error('WebSocket error:', error);
};

// Send message function
function sendMessage(text) {
    if (socket.readyState === WebSocket.OPEN) {
        socket.send(JSON.stringify({
            type: 'message',
            text: text,
            timestamp: Date.now()
        }));
    }
}

// Close connection
function disconnect() {
    socket.close(1000, 'User logged out');
}

WebSocket with Reconnection

class ReconnectingWebSocket {
    constructor(url) {
        this.url = url;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 5;
        this.reconnectDelay = 1000;
        this.connect();
    }

    connect() {
        this.socket = new WebSocket(this.url);

        this.socket.onopen = () => {
            console.log('Connected');
            this.reconnectAttempts = 0;  // Reset on successful connection
        };

        this.socket.onclose = (event) => {
            if (!event.wasClean) {
                this.reconnect();
            }
        };

        this.socket.onerror = (error) => {
            console.error('WebSocket error', error);
        };

        this.socket.onmessage = (event) => {
            this.onMessage(JSON.parse(event.data));
        };
    }

    reconnect() {
        if (this.reconnectAttempts < this.maxReconnectAttempts) {
            this.reconnectAttempts++;
            const delay = this.reconnectDelay * this.reconnectAttempts;

            console.log(`Reconnecting in ${delay}ms... (attempt ${this.reconnectAttempts})`);

            setTimeout(() => this.connect(), delay);
        } else {
            console.error('Max reconnection attempts reached');
        }
    }

    send(data) {
        if (this.socket.readyState === WebSocket.OPEN) {
            this.socket.send(JSON.stringify(data));
        }
    }

    onMessage(data) {
        // Override this in subclass or set externally
        console.log('Received:', data);
    }
}

WebSocket in Spring Boot (Server)

Dependencies

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

Basic WebSocket Handler

// 1. WebSocket Configuration
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private ChatWebSocketHandler chatHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatHandler, "/ws/chat")
            .setAllowedOrigins("https://myapp.com");  // CORS for WebSocket
    }
}

// 2. WebSocket Handler
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {

    private final Set<WebSocketSession> sessions =
        ConcurrentHashMap.newKeySet();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        sessions.add(session);
        System.out.println("New connection: " + session.getId());
    }

    @Override
    protected void handleTextMessage(WebSocketSession session,
                                       TextMessage message) throws Exception {
        String payload = message.getPayload();
        System.out.println("Received: " + payload);

        // Parse the message
        ObjectMapper mapper = new ObjectMapper();
        ChatMessage chatMessage = mapper.readValue(payload, ChatMessage.class);

        // Broadcast to all connected clients
        for (WebSocketSession s : sessions) {
            if (s.isOpen()) {
                s.sendMessage(new TextMessage(payload));
            }
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session,
                                       CloseStatus status) {
        sessions.remove(session);
        System.out.println("Connection closed: " + session.getId());
    }

    @Override
    public void handleTransportError(WebSocketSession session,
                                      Throwable exception) {
        System.err.println("Error: " + exception.getMessage());
        sessions.remove(session);
    }

    // Send message to specific session
    public void sendToSession(String sessionId, String message) {
        sessions.stream()
            .filter(s -> s.getId().equals(sessionId))
            .findFirst()
            .ifPresent(s -> {
                try {
                    s.sendMessage(new TextMessage(message));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
    }
}

STOMP over WebSocket (Recommended)

STOMP (Simple Text Oriented Messaging Protocol) is a messaging protocol that runs over WebSocket, providing features like message routing, subscriptions, and better organization.

Server Configuration

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // Enable simple in-memory message broker
        // Client subscribes to /topic/* or /queue/*
        config.enableSimpleBroker("/topic", "/queue");

        // Prefix for messages FROM client TO server
        config.setApplicationDestinationPrefixes("/app");

        // Prefix for user-specific messages
        config.setUserDestinationPrefix("/user");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // WebSocket endpoint - clients connect here
        registry.addEndpoint("/ws")
            .setAllowedOrigins("https://myapp.com")
            .withSockJS();  // Fallback for old browsers
    }
}

Controller for Handling Messages

@Controller
public class ChatController {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    // Handle messages sent to /app/chat
    @MessageMapping("/chat")
    @SendTo("/topic/messages")  // Broadcast to all subscribers
    public ChatMessage handleMessage(ChatMessage message,
                                       Principal principal) {
        message.setSender(principal.getName());
        message.setTimestamp(LocalDateTime.now());
        return message;  // Automatically sent to /topic/messages
    }

    // Send to specific user
    @MessageMapping("/private")
    public void handlePrivateMessage(PrivateMessage message,
                                       Principal principal) {
        message.setSender(principal.getName());

        // Send to specific user's queue
        messagingTemplate.convertAndSendToUser(
            message.getRecipient(),
            "/queue/private",
            message
        );
    }

    // Send from anywhere in the application
    public void sendNotification(String userId, Notification notification) {
        messagingTemplate.convertAndSendToUser(
            userId,
            "/queue/notifications",
            notification
        );
    }

    // Broadcast to all
    public void broadcastAnnouncement(Announcement announcement) {
        messagingTemplate.convertAndSend("/topic/announcements", announcement);
    }
}

JavaScript Client with STOMP

// Using stompjs library
// npm install @stomp/stompjs sockjs-client

import { Client } from '@stomp/stompjs';
import SockJS from 'sockjs-client';

const client = new Client({
    // Use SockJS for fallback support
    webSocketFactory: () => new SockJS('https://api.myapp.com/ws'),

    // Or native WebSocket
    // brokerURL: 'wss://api.myapp.com/ws',

    reconnectDelay: 5000,

    onConnect: (frame) => {
        console.log('Connected!');

        // Subscribe to public topic
        client.subscribe('/topic/messages', (message) => {
            const chatMessage = JSON.parse(message.body);
            displayMessage(chatMessage);
        });

        // Subscribe to user-specific queue
        client.subscribe('/user/queue/private', (message) => {
            const privateMessage = JSON.parse(message.body);
            displayPrivateMessage(privateMessage);
        });

        // Subscribe to notifications
        client.subscribe('/user/queue/notifications', (message) => {
            const notification = JSON.parse(message.body);
            showNotification(notification);
        });
    },

    onDisconnect: () => {
        console.log('Disconnected');
    },

    onStompError: (frame) => {
        console.error('STOMP error:', frame.headers['message']);
    }
});

// Activate connection
client.activate();

// Send message to /app/chat
function sendMessage(text) {
    client.publish({
        destination: '/app/chat',
        body: JSON.stringify({
            type: 'message',
            text: text
        })
    });
}

// Send private message
function sendPrivateMessage(recipient, text) {
    client.publish({
        destination: '/app/private',
        body: JSON.stringify({
            recipient: recipient,
            text: text
        })
    });
}

// Disconnect
function disconnect() {
    client.deactivate();
}

WebSocket Authentication

// 1. Handshake Interceptor - Authenticate during connection
@Component
public class AuthHandshakeInterceptor implements HandshakeInterceptor {

    @Autowired
    private JwtTokenProvider tokenProvider;

    @Override
    public boolean beforeHandshake(ServerHttpRequest request,
                                     ServerHttpResponse response,
                                     WebSocketHandler handler,
                                     Map<String, Object> attributes) {

        // Get token from query parameter or header
        String token = extractToken(request);

        if (token != null && tokenProvider.validateToken(token)) {
            String userId = tokenProvider.getUserId(token);
            attributes.put("userId", userId);
            return true;  // Allow connection
        }

        return false;  // Reject connection
    }

    private String extractToken(ServerHttpRequest request) {
        // From query parameter: /ws?token=xxx
        String query = request.getURI().getQuery();
        if (query != null && query.contains("token=")) {
            return query.split("token=")[1].split("&")[0];
        }
        return null;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request,
                                ServerHttpResponse response,
                                WebSocketHandler handler,
                                Exception ex) { }
}

// 2. Register the interceptor
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Autowired
    private AuthHandshakeInterceptor authInterceptor;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
            .addInterceptors(authInterceptor)
            .setAllowedOrigins("*")
            .withSockJS();
    }
}

// 3. Channel Interceptor - Authenticate STOMP messages
@Component
public class AuthChannelInterceptor implements ChannelInterceptor {

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor =
            StompHeaderAccessor.wrap(message);

        if (StompCommand.CONNECT.equals(accessor.getCommand())) {
            String token = accessor.getFirstNativeHeader("Authorization");
            // Validate token and set principal
        }

        return message;
    }
}

Real-World Example: Live Chat

// Message DTOs
public record ChatMessage(
    String id,
    String sender,
    String content,
    String room,
    LocalDateTime timestamp
) {}

public record JoinMessage(
    String username,
    String room
) {}

// Chat Controller
@Controller
public class ChatController {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    private final Map<String, Set<String>> roomUsers = new ConcurrentHashMap<>();

    @MessageMapping("/chat.join")
    public void joinRoom(JoinMessage joinMessage, SimpMessageHeaderAccessor headerAccessor) {
        String sessionId = headerAccessor.getSessionId();
        String room = joinMessage.room();
        String username = joinMessage.username();

        // Store user in room
        roomUsers.computeIfAbsent(room, k -> ConcurrentHashMap.newKeySet()).add(username);

        // Store in session attributes
        headerAccessor.getSessionAttributes().put("username", username);
        headerAccessor.getSessionAttributes().put("room", room);

        // Notify room that user joined
        messagingTemplate.convertAndSend(
            "/topic/chat/" + room,
            new ChatMessage(
                UUID.randomUUID().toString(),
                "SYSTEM",
                username + " joined the chat",
                room,
                LocalDateTime.now()
            )
        );

        // Send updated user list
        messagingTemplate.convertAndSend(
            "/topic/chat/" + room + "/users",
            roomUsers.get(room)
        );
    }

    @MessageMapping("/chat.send")
    public void sendMessage(ChatMessage message) {
        ChatMessage enrichedMessage = new ChatMessage(
            UUID.randomUUID().toString(),
            message.sender(),
            message.content(),
            message.room(),
            LocalDateTime.now()
        );

        messagingTemplate.convertAndSend(
            "/topic/chat/" + message.room(),
            enrichedMessage
        );
    }

    // Handle disconnection
    @EventListener
    public void handleSessionDisconnect(SessionDisconnectEvent event) {
        SimpMessageHeaderAccessor headerAccessor =
            SimpMessageHeaderAccessor.wrap(event.getMessage());

        String username = (String) headerAccessor.getSessionAttributes().get("username");
        String room = (String) headerAccessor.getSessionAttributes().get("room");

        if (username != null && room != null) {
            roomUsers.getOrDefault(room, Set.of()).remove(username);

            messagingTemplate.convertAndSend(
                "/topic/chat/" + room,
                new ChatMessage(
                    UUID.randomUUID().toString(),
                    "SYSTEM",
                    username + " left the chat",
                    room,
                    LocalDateTime.now()
                )
            );
        }
    }
}

WebSocket vs Alternatives

Technology Direction Connection Use Case
WebSocket Bidirectional Persistent Chat, gaming, collaboration
Server-Sent Events Server → Client Persistent News feeds, notifications
Long Polling Simulated push Repeated requests Legacy support
HTTP/2 Push Server → Client Request-based Resource preloading
When to Use WebSocket
  • Real-time bidirectional communication needed
  • Low latency is critical
  • Frequent small messages
  • Gaming, chat, live collaboration

When NOT to use: Simple one-way notifications (use SSE), infrequent updates (use polling), RESTful CRUD operations (use HTTP)

Best Practices

DO:

  • Use WSS (WebSocket Secure) - Always encrypt in production
  • Implement heartbeat/ping-pong - Detect dead connections
  • Handle reconnection - Network can be unreliable
  • Use STOMP for complex apps - Better message routing
  • Authenticate connections - Validate tokens during handshake
  • Limit message size - Prevent DoS attacks
  • Use message queues for scale - RabbitMQ, Redis Pub/Sub

DON'T:

  • Don't use WebSocket for everything - REST is fine for CRUD
  • Don't trust client messages - Always validate on server
  • Don't forget error handling - Connections can fail
  • Don't ignore backpressure - Handle slow consumers
  • Don't store sensitive data in messages - Use references

Summary

  • WebSocket: Full-duplex communication protocol over TCP
  • Handshake: HTTP upgrade to WebSocket protocol
  • Persistent: Connection stays open for continuous communication
  • STOMP: Messaging protocol that runs over WebSocket
  • Topics: Pub/sub for broadcasting to multiple clients
  • Queues: Point-to-point for user-specific messages
  • Use cases: Chat, gaming, notifications, live updates