WebSocket

Real-Time Bidirectional Communication

← Back to Index

What is WebSocket?

Think of WebSocket like a phone call vs regular mail:

WebSocket provides full-duplex communication over a single TCP connection. Once established, both client and server can send messages anytime without requesting.

Use cases: Chat apps, live notifications, multiplayer games, stock tickers, collaborative tools

HTTP vs WebSocket

// HTTP - Request/Response (one-way)
Client → Request → Server
Client ← Response ← Server
// Connection closes. Need new request for more data.

// WebSocket - Persistent Connection (two-way)
Client ←→ Server
// Connection stays open. Both can send anytime!

Simple WebSocket Server

import jakarta.websocket.*;
import jakarta.websocket.server.*;

@ServerEndpoint("/chat")  // ws://localhost:8080/myapp/chat
public class ChatEndpoint {

    @OnOpen  // Called when client connects
    public void onOpen(Session session) {
        System.out.println("New connection: " + session.getId());
    }

    @OnMessage  // Called when message received
    public String onMessage(String message, Session session) {
        System.out.println("Received: " + message);
        return "Echo: " + message;  // Send back to sender
    }

    @OnClose  // Called when connection closes
    public void onClose(Session session, CloseReason reason) {
        System.out.println("Connection closed: " + reason.getReasonPhrase());
    }

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

JavaScript Client

// Connect to WebSocket
const socket = new WebSocket("ws://localhost:8080/myapp/chat");

// Connection opened
socket.onopen = function(event) {
    console.log("Connected!");
    socket.send("Hello Server!");
};

// Message received
socket.onmessage = function(event) {
    console.log("Server says: " + event.data);
};

// Connection closed
socket.onclose = function(event) {
    console.log("Disconnected");
};

// Error occurred
socket.onerror = function(error) {
    console.error("Error: ", error);
};

Broadcasting to All Clients

import java.util.*;
import java.util.concurrent.*;

@ServerEndpoint("/chat")
public class ChatRoom {

    // Store all connected sessions
    private static Set<Session> sessions =
        Collections.newSetFromMap(new ConcurrentHashMap<>());

    @OnOpen
    public void onOpen(Session session) {
        sessions.add(session);
        System.out.println("User joined. Total users: " + sessions.size());

        // Notify all users
        broadcast("A new user joined the chat!");
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        // Send message to all connected users
        broadcast(session.getId() + ": " + message);
    }

    @OnClose
    public void onClose(Session session) {
        sessions.remove(session);
        System.out.println("User left. Total users: " + sessions.size());

        broadcast("A user left the chat");
    }

    // Broadcast to all connected clients
    private static void broadcast(String message) {
        for (Session session : sessions) {
            if (session.isOpen()) {
                try {
                    session.getBasicRemote().sendText(message);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Path Parameters

@ServerEndpoint("/chat/{room}")  // ws://localhost:8080/myapp/chat/general
public class MultiRoomChat {

    @OnOpen
    public void onOpen(Session session,
                     @PathParam("room") String roomName) {
        System.out.println("User joined room: " + roomName);

        // Store room name in session
        session.getUserProperties().put("room", roomName);
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        String room = (String) session.getUserProperties().get("room");
        System.out.println("Message in " + room + ": " + message);
    }
}

Sending to Specific Client

@ServerEndpoint("/notification")
public class NotificationEndpoint {

    private static Map<String, Session> userSessions = new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(Session session) {
        // Get username from query parameter
        String username = session.getRequestParameterMap()
                                    .get("username")
                                    .get(0);

        userSessions.put(username, session);
        System.out.println(username + " connected");
    }

    // Send notification to specific user
    public static void notifyUser(String username, String message) {
        Session session = userSessions.get(username);
        if (session != null && session.isOpen()) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

// Usage from anywhere in your app
NotificationEndpoint.notifyUser("alice", "New message from Bob!");

JSON Messages

Message Class

public class ChatMessage {
    private String sender;
    private String content;
    private long timestamp;

    // Constructors, getters, setters
}

Endpoint with JSON

import jakarta.json.bind.*;

@ServerEndpoint("/chat")
public class JsonChatEndpoint {

    private static Jsonb jsonb = JsonbBuilder.create();

    @OnMessage
    public void onMessage(String jsonMessage) {
        // Parse JSON to object
        ChatMessage message = jsonb.fromJson(jsonMessage, ChatMessage.class);

        System.out.println(message.getSender() + ": " + message.getContent());

        // Create response
        ChatMessage response = new ChatMessage();
        response.setSender("Server");
        response.setContent("Message received!");
        response.setTimestamp(System.currentTimeMillis());

        // Send as JSON
        String jsonResponse = jsonb.toJson(response);
        broadcast(jsonResponse);
    }
}

JavaScript Client with JSON

const socket = new WebSocket("ws://localhost:8080/myapp/chat");

socket.onopen = function() {
    // Send JSON message
    const message = {
        sender: "Alice",
        content: "Hello everyone!",
        timestamp: Date.now()
    };
    socket.send(JSON.stringify(message));
};

socket.onmessage = function(event) {
    // Parse JSON response
    const message = JSON.parse(event.data);
    console.log(message.sender + ": " + message.content);
};

CDI Integration

@ServerEndpoint(
    value = "/chat",
    configurator = CdiAwareConfigurator.class  // Enable CDI
)
public class CdiChatEndpoint {

    @Inject  // Inject services!
    private MessageService messageService;

    @Inject
    private UserService userService;

    @OnMessage
    public void onMessage(String message, Session session) {
        // Use injected services
        messageService.saveMessage(message);

        String username = userService.getCurrentUser(session);
        broadcast(username + ": " + message);
    }
}

// Configurator to enable CDI
public class CdiAwareConfigurator extends ServerEndpointConfig.Configurator {
    @Override
    public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {
        return CDI.current().select(endpointClass).get();
    }
}

Timeouts and Idle Sessions

@ServerEndpoint("/chat")
public class TimeoutEndpoint {

    @OnOpen
    public void onOpen(Session session) {
        // Set max idle timeout (15 minutes)
        session.setMaxIdleTimeout(15 * 60 * 1000);  // milliseconds

        // Set max message size (1MB)
        session.setMaxTextMessageBufferSize(1024 * 1024);

        System.out.println("Session configured with timeout: " +
                           session.getMaxIdleTimeout());
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        // Reset idle timeout on activity
        session.setMaxIdleTimeout(15 * 60 * 1000);
    }
}

Ping/Pong (Heartbeat)

@ServerEndpoint("/heartbeat")
public class HeartbeatEndpoint {

    @OnMessage
    public void onPong(PongMessage pong, Session session) {
        System.out.println("Received pong from: " + session.getId());
    }

    // Send ping periodically to keep connection alive
    public static void sendPing(Session session) {
        if (session.isOpen()) {
            try {
                session.getBasicRemote().sendPing(ByteBuffer.wrap("ping".getBytes()));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Client-Side Heartbeat

const socket = new WebSocket("ws://localhost:8080/myapp/heartbeat");

// Send ping every 30 seconds
setInterval(() => {
    if (socket.readyState === WebSocket.OPEN) {
        socket.send("ping");
    }
}, 30000);

Best Practices

✅ DO:

  • Use JSON for structured data - Easier to parse and extend
  • Implement heartbeat/ping-pong - Detect dead connections
  • Set timeouts - Clean up idle connections
  • Handle errors gracefully - Connection can drop anytime
  • Use ConcurrentHashMap for sessions - Thread-safe
  • Close connections properly - Free resources
  • Validate messages - Don't trust client data
  • Use async sending for broadcasts - Better performance

❌ DON'T:

  • Don't use WebSocket for everything - REST is often simpler
  • Don't send huge messages - Split large data
  • Don't forget authentication - Secure your endpoints
  • Don't block the thread - Use async for long operations
  • Don't forget to remove closed sessions - Memory leaks
  • Don't store state in endpoint - Use session attributes

Summary

  • WebSocket provides real-time bidirectional communication
  • @ServerEndpoint: Marks WebSocket endpoint
  • @OnOpen: Called when client connects
  • @OnMessage: Called when message received
  • @OnClose: Called when connection closes
  • @OnError: Called on error
  • Session: Represents connection to a client
  • Broadcasting: Send to all connected clients
  • JSON support: Send/receive structured data
  • Use cases: Chat, live notifications, multiplayer games, real-time dashboards