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 #
| Frame | Code | Use |
|---|---|---|
| Text | 0x1 | Text messages (UTF-8) |
| Binary | 0x2 | Binary data |
| Close | 0x8 | Close the connection with a code and reason |
| Ping | 0x9 | Check whether the connection is still alive |
| Pong | 0xA | Reply 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 closedWebSocket vs HTTP Polling #
| Aspect | HTTP Polling | WebSocket |
|---|---|---|
| Connection | New for every request | One persistent connection |
| Latency | High (HTTP header overhead) | Low (send frames directly) |
| Server push | ✗ Not possible | ✓ Anytime |
| Server load | High (many requests) | Low |
| Best case | Data rarely changes | Data 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,@OnErrordeclaratively define the endpoint lifecycle.CopyOnWriteArraySetfor the active session list — thread-safe and safe to iterate while additions or removals happen from other threads.- Use
getAsyncRemote()for broadcasting —getBasicRemote()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.