Web Socket #

HTTP was designed for the request-response pattern: the client asks, the server answers, the connection closes. This pattern works well for static web pages, but not for applications that need continuously flowing data — scoreboards that change every second, real-time notifications, collaborative editors, or trading interfaces. Every time a client wants to know if there’s an update, it has to send a new request. WebSocket solves this: after an initial handshake over HTTP, the connection is upgraded into a fully two-way channel that stays open. The server can send data to the client at any time without waiting for a request — and vice versa. This article covers how the WebSocket protocol works, its implementation with three approaches in Java (Jakarta EE, Spring Boot, Jetty), broadcasting to all clients, heartbeats to keep connections alive, and when WebSocket is the right choice.

How WebSocket Works #

WebSocket isn’t a standalone protocol — it starts as HTTP and then gets upgraded. This process is called the WebSocket handshake.

The HTTP to WebSocket Handshake #

Client → Server (HTTP Upgrade Request):
  GET /ws HTTP/1.1
  Host: example.com
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
  Sec-WebSocket-Version: 13

Server → Client (HTTP 101 Switching Protocols):
  HTTP/1.1 101 Switching Protocols
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

After a successful handshake, the same TCP connection stays open and both sides can send frames at any time.

WebSocket Frame Types #

FrameCodeUse
Text0x1Text messages (UTF-8)
Binary0x2Binary data
Close0x8Close the connection with a code and reason
Ping0x9Check whether the connection is still alive
Pong0xAReply to a ping
sequenceDiagram
    participant B as Browser / Client
    participant S as Server

    B->>S: HTTP GET /ws (Upgrade: websocket)
    S->>B: HTTP 101 Switching Protocols
    Note over B,S: WebSocket connection open (full-duplex)
    B->>S: Text frame: "Hello server"
    S->>B: Text frame: "Hello client!"
    S->>B: Text frame: "Data update: 42" (server push, without being asked)
    B->>S: Ping frame
    S->>B: Pong frame
    B->>S: Close frame
    S->>B: Close frame
    Note over B,S: Connection closed

WebSocket vs HTTP Polling #

AspectHTTP PollingWebSocket
ConnectionNew for every requestOne persistent connection
LatencyHigh (HTTP header overhead)Low (send frames directly)
Server push✗ Not possible✓ Anytime
Server loadHigh (many requests)Low
Best caseData rarely changesData changes often / real-time

The Jakarta EE WebSocket API #

Jakarta EE (formerly Java EE) provides a standard WebSocket API through the jakarta.websocket package. This approach uses annotations to define endpoints and event handlers. It works well with application servers like Tomcat, WildFly, or Payara.

Dependencies #

<!-- Maven — for development (runtime is provided by the server) -->
<dependency>
    <groupId>jakarta.websocket</groupId>
    <artifactId>jakarta.websocket-api</artifactId>
    <version>2.1.0</version>
    <scope>provided</scope>
</dependency>

Annotation-Based Server Endpoint #

import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

@ServerEndpoint("/ws/chat")
public class ChatEndpoint {

    // A set of all active sessions — CopyOnWriteArraySet is safe for multithreading
    private static final Set<Session> activeSessions = new CopyOnWriteArraySet<>();

    @OnOpen
    public void onOpen(Session session) {
        activeSessions.add(session);
        System.out.println("Connected: " + session.getId());
        broadcast("A new user joined. Total: " + activeSessions.size(), null);
    }

    @OnMessage
    public void onMessage(String message, Session sender) {
        System.out.println("[" + sender.getId() + "] " + message);
        // Broadcast the message to all clients except the sender
        broadcast(message, sender);
    }

    @OnClose
    public void onClose(Session session, CloseReason reason) {
        activeSessions.remove(session);
        System.out.println("Disconnected: " + session.getId() + " — " + reason.getReasonPhrase());
        broadcast("A user left. Total: " + activeSessions.size(), null);
    }

    @OnError
    public void onError(Session session, Throwable error) {
        System.err.println("Error on session " + session.getId() + ": " + error.getMessage());
        activeSessions.remove(session);
    }

    private void broadcast(String message, Session except) {
        for (Session s : activeSessions) {
            if (s.equals(except)) continue;
            if (s.isOpen()) {
                try {
                    s.getBasicRemote().sendText(message);
                } catch (IOException e) {
                    System.err.println("Failed to send to " + s.getId() + ": " + e.getMessage());
                }
            }
        }
    }
}

Sending Messages to a Specific Client #

@OnMessage
public void onMessage(String message, Session sender) {
    // Send to the sender only (synchronous)
    try {
        sender.getBasicRemote().sendText("Echo: " + message);
    } catch (IOException e) {
        e.printStackTrace();
    }

    // Send asynchronously — more efficient for servers with many clients
    sender.getAsyncRemote().sendText("Async: " + message);

    // Send binary data
    byte[] data = message.getBytes();
    try {
        sender.getBasicRemote().sendBinary(java.nio.ByteBuffer.wrap(data));
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Endpoints with Path Parameters #

import jakarta.websocket.server.PathParam;

// URL: /ws/room/general or /ws/room/coding
@ServerEndpoint("/ws/room/{roomName}")
public class RoomEndpoint {

    @OnOpen
    public void onOpen(Session session, @PathParam("roomName") String room) {
        System.out.println("Joined room: " + room);
        // store the room in the session's user properties
        session.getUserProperties().put("room", room);
    }

    @OnMessage
    public void onMessage(String message, Session sender) {
        String room = (String) sender.getUserProperties().get("room");
        System.out.println("[" + room + "] " + message);
        // broadcast only to clients in the same room
    }
}

Spring Boot WebSocket #

Spring Boot simplifies WebSocket setup with auto-configuration and STOMP (Simple Text Oriented Messaging Protocol) support — a messaging protocol on top of WebSocket that adds the concepts of channels and subscriptions.

Dependencies #

<!-- Maven -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-websocket'

Simple WebSocket (Without STOMP) #

The most direct approach: a handler that processes text messages one by one.

import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

public class ChatWebSocketHandler extends TextWebSocketHandler {

    private static final Set<WebSocketSession> activeSessions = new CopyOnWriteArraySet<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        activeSessions.add(session);
        System.out.println("Connected: " + session.getId());
    }

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

        // Broadcast to everyone except the sender
        for (WebSocketSession s : activeSessions) {
            if (s.isOpen() && !s.getId().equals(sender.getId())) {
                s.sendMessage(new TextMessage(text));
            }
        }
    }

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

    @Override
    public void handleTransportError(WebSocketSession session, Throwable error) throws Exception {
        System.err.println("Error: " + error.getMessage());
        activeSessions.remove(session);
        session.close(CloseStatus.SERVER_ERROR);
    }
}

WebSocket Configuration #

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.*;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry
            .addHandler(new ChatWebSocketHandler(), "/ws/chat")
            .setAllowedOrigins("*"); // in production, restrict to specific domains
    }
}

WebSocket with STOMP and SockJS #

STOMP adds a channel abstraction (topics and queues) on top of WebSocket. SockJS is a fallback — if a browser doesn’t support WebSocket, it automatically switches to polling. This approach is the de facto standard in Spring Boot applications.

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;

@Configuration
@EnableWebSocketMessageBroker
public class StompConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // WebSocket handshake endpoint, with SockJS as a fallback
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // Prefix for messages sent from clients to the server
        registry.setApplicationDestinationPrefixes("/app");

        // Prefix for broadcast topics — clients subscribe to /topic/xxx
        registry.enableSimpleBroker("/topic", "/queue");
    }
}
import org.springframework.messaging.handler.annotation.*;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;

@Controller
public class ChatController {

    private final SimpMessagingTemplate template;

    public ChatController(SimpMessagingTemplate template) {
        this.template = template;
    }

    // Clients send to /app/message → this controller processes it
    @MessageMapping("/message")
    @SendTo("/topic/general-room") // broadcast the result to all subscribers of this topic
    public String handleMessage(String message) {
        return "Server: " + message;
    }

    // Send a message to a topic programmatically (e.g., from a scheduled job)
    public void sendNotification(String notification) {
        template.convertAndSend("/topic/notifications", notification);
    }

    // Send to one specific user
    public void sendPrivate(String username, String message) {
        template.convertAndSendToUser(username, "/queue/message", message);
    }
}

JavaScript Client (Browser) #

// Connect to the Spring Boot WebSocket server
const socket = new WebSocket('ws://localhost:8080/ws/chat');

socket.onopen = () => {
    console.log('Connected!');
    socket.send('Hello from the browser');
};

socket.onmessage = (event) => {
    console.log('Received:', event.data);
};

socket.onclose = (event) => {
    console.log('Disconnected:', event.code, event.reason);
};

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

// Send a message
function send(message) {
    if (socket.readyState === WebSocket.OPEN) {
        socket.send(message);
    }
}

// Close the connection
function close() {
    socket.close(1000, 'User left');
}

Embedded Jetty WebSocket #

Jetty lets you build a WebSocket server embedded directly in a plain Java application — without needing an external application server. Good for internal tools, desktop applications, or lightweight microservices.

Dependencies #

<!-- Maven — Jetty 12 -->
<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-server</artifactId>
    <version>12.0.7</version>
</dependency>
<dependency>
    <groupId>org.eclipse.jetty.websocket</groupId>
    <artifactId>jetty-websocket-jetty-server</artifactId>
    <version>12.0.7</version>
</dependency>

Implementation with Jetty #

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.websocket.api.*;
import org.eclipse.jetty.websocket.api.annotations.*;
import org.eclipse.jetty.websocket.server.JettyWebSocketServlet;
import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory;

// WebSocket endpoint
@WebSocket
public class EchoSocket {

    @OnWebSocketOpen
    public void onOpen(Session session) {
        System.out.println("Connected: " + session.getRemoteAddress());
        session.setIdleTimeout(java.time.Duration.ofMinutes(5));
    }

    @OnWebSocketMessage
    public void onMessage(Session session, String message) {
        System.out.println("Received: " + message);
        try {
            session.getRemote().sendString("Echo: " + message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @OnWebSocketClose
    public void onClose(int statusCode, String reason) {
        System.out.println("Closed: " + statusCode + " — " + reason);
    }

    @OnWebSocketError
    public void onError(Throwable error) {
        System.err.println("Error: " + error.getMessage());
    }
}

// Servlet to register the endpoint
public class EchoServlet extends JettyWebSocketServlet {
    @Override
    protected void configure(JettyWebSocketServletFactory factory) {
        factory.register(EchoSocket.class);
    }
}
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;

public class JettyMain {
    public static void main(String[] args) throws Exception {
        Server server = new Server(8080);

        ServletContextHandler ctx = new ServletContextHandler();
        ctx.setContextPath("/");
        ctx.addServlet(new ServletHolder(new EchoServlet()), "/ws/*");

        server.setHandler(ctx);
        server.start();

        System.out.println("Jetty server running at ws://localhost:8080/ws/");
        server.join();
    }
}

Heartbeats — Keeping Connections Alive #

WebSocket connections can be silently cut off by proxies, load balancers, or firewalls that consider idle connections inactive. Heartbeats (ping/pong) prevent this by sending small frames periodically.

Heartbeat in Jakarta EE #

import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.*;

@ServerEndpoint("/ws/live")
public class LiveEndpoint {

    private ScheduledExecutorService scheduler;
    private ScheduledFuture<?> heartbeatTask;

    @OnOpen
    public void onOpen(Session session) {
        scheduler = Executors.newSingleThreadScheduledExecutor();

        // Send a ping every 30 seconds
        heartbeatTask = scheduler.scheduleAtFixedRate(() -> {
            if (session.isOpen()) {
                try {
                    // Ping frame — the server sends, the client automatically replies with a pong
                    session.getBasicRemote().sendPing(ByteBuffer.wrap("ping".getBytes()));
                } catch (IOException e) {
                    System.err.println("Ping failed: " + e.getMessage());
                }
            }
        }, 30, 30, TimeUnit.SECONDS);
    }

    @OnClose
    public void onClose(Session session) {
        if (heartbeatTask != null) heartbeatTask.cancel(true);
        if (scheduler != null) scheduler.shutdown();
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        // handle normal messages
    }
}

Heartbeat in Spring Boot #

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;

@Configuration
@EnableWebSocketMessageBroker
public class StompConfigWithHeartbeat implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // Heartbeat: the server sends every 10 seconds, expects the client to send every 10 seconds
        registry.enableSimpleBroker("/topic")
                .setHeartbeatValue(new long[]{10000, 10000});
        registry.setApplicationDestinationPrefixes("/app");
    }

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

Session Management and Security #

Storing User Information in a Session #

@ServerEndpoint("/ws/chat")
public class ChatEndpointWithAuth {

    @OnOpen
    public void onOpen(Session session,
                       @jakarta.websocket.server.PathParam("token") String token) {
        // Validate the token
        String username = validateToken(token);
        if (username == null) {
            try {
                session.close(new CloseReason(
                    CloseReason.CloseCodes.VIOLATED_POLICY,
                    "Invalid token"
                ));
            } catch (IOException e) {
                e.printStackTrace();
            }
            return;
        }

        // Store the user info in the session properties
        session.getUserProperties().put("username", username);
        System.out.println(username + " connected");
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        String username = (String) session.getUserProperties().get("username");
        if (username != null) {
            broadcast("[" + username + "]: " + message);
        }
    }

    private String validateToken(String token) {
        // JWT or session token validation implementation
        return token != null && !token.isBlank() ? "user-" + token : null;
    }

    private void broadcast(String message) {
        // ... broadcast to all sessions
    }
}

Common Anti-Patterns #

// ANTI-PATTERN 1: sending a message without checking whether the session is still open
session.getBasicRemote().sendText(message); // ✗ can throw if the session is already closed

// CORRECT: always check isOpen() before sending
if (session.isOpen()) {
    try {
        session.getBasicRemote().sendText(message);
    } catch (IOException e) {
        activeSessions.remove(session); // remove from the list if sending fails
    }
}

// ANTI-PATTERN 2: using getBasicRemote() on a server with many clients
// getBasicRemote() is blocking — one slow message blocks the others
for (Session s : activeSessions) {
    s.getBasicRemote().sendText(message); // ✗ blocking, slow

// CORRECT: use getAsyncRemote() for broadcasting
for (Session s : activeSessions) {
    if (s.isOpen()) {
        s.getAsyncRemote().sendText(message); // ✓ non-blocking
    }
}

// ANTI-PATTERN 3: storing a session in an instance variable (not thread-safe)
public class BadEndpoint {
    private Session session; // ✗ each instance is one session — this is actually ok
    // but if state is shared across sessions, it must be static + thread-safe
}

When to Use WebSocket #

Use WEBSOCKET when:
  ✓ Data must flow from the server to the client without the client asking (server push)
  ✓ Low latency is critical (games, trading, real-time collaboration)
  ✓ High update frequency (more than once per second)
  ✓ Continuous two-way communication is needed

Use regular HTTP when:
  ✗ Data is only needed occasionally (less than once per minute)
  ✗ Simple stateless operations (CRUD APIs)
  ✗ Client-side or CDN caching matters
  ✗ The team isn't familiar with stateful connections

Consider SSE (Server-Sent Events) when:
  → You only need one-way server push (notification streams, progress)
  → It's simpler than WebSocket because it's just regular HTTP
  → Automatic reconnect is built into browsers

Choose the implementation:
  → Jakarta EE WebSocket  : you already have an app server (Tomcat, WildFly, Payara)
  → Spring Boot WebSocket : Spring Boot projects, need STOMP or Spring integration
  → Embedded Jetty        : standalone applications without an app server

Summary #

  • WebSocket starts as HTTP and then gets upgraded — the handshake uses HTTP 101 Switching Protocols. After that, the same TCP connection is used for two-way WebSocket frames.
  • Full-duplex means the server can send anytime — no need to wait for a client request. This is what distinguishes WebSocket from HTTP polling.
  • Jakarta EE WebSocket uses annotations@ServerEndpoint, @OnOpen, @OnMessage, @OnClose, @OnError declaratively define the endpoint lifecycle.
  • CopyOnWriteArraySet for the active session list — thread-safe and safe to iterate while additions or removals happen from other threads.
  • Use getAsyncRemote() for broadcastinggetBasicRemote() is blocking. To send to many clients at once, getAsyncRemote() doesn’t block the thread.
  • Always check session.isOpen() before sending — sessions can close at any time. Sending to an already-closed session throws an exception.
  • Heartbeats prevent idle connection drops — proxies and firewalls often cut inactive connections. Send a ping every 30–60 seconds to keep connections alive.
  • Spring Boot + STOMP for enterprise applications: it adds the concepts of topics, queues, and subscriptions on top of WebSocket, making message routing more structured.

← Previous: Socket   Next: Web Server →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact