Skip to Content
Course content

145: WebSockets in Java

Click on the "Edit" button in the top corner of the screen to edit your slide content.

One of the first things you'll notice when moving from standard REST APIs to WebSockets is that the mental model shifts from "request-response" to "persistent connection." In a REST call, the server does its job and then forgets the client exists. With WebSockets, the server has to maintain a living relationship with every connected user. This is where things usually start breaking.

I recently saw a developer struggle with a real-time notification system. Their code looked perfect in isolation, but the moment they had more than two people connected, the server started throwing ConcurrentModificationException and IOException errors. Let's look at the broken code.

@ServerEndpoint("/notifications")
public class NotificationServer {
    // A simple list to keep track of who is connected
    private static List<Session> sessions = new ArrayList<>();

    @OnOpen
    public void onOpen(Session session) {
        sessions.add(session);
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        // Broadcast the message to everyone
        for (Session s : sessions) {
            s.getBasicRemote().sendText("User said: " + message);
        }
    }

    @OnClose
    public void onClose(Session session) {
        sessions.remove(session);
    }
}

The Broadcast Crash

If you run this with a single user, it works. But imagine this: the onMessage method is iterating through the sessions list to broadcast a message. While that loop is running, another user closes their browser tab, triggering onClose. The onClose method calls sessions.remove(session).

Because ArrayList is not thread-safe and doesn't allow modification while iterating, the server crashes with a ConcurrentModificationException. Even if you fixed the list, you'd eventually hit an IOException because you're trying to send a message to a Session that the client has already killed on their end. It's a classic "state management" nightmare.

Thread-Safe Session Management

To fix this, we need to handle the session list concurrently and verify the connection state before we attempt to push data. I prefer using a CopyOnWriteArrayList for small-to-medium sets of connections because it creates a fresh copy of the underlying array whenever the list is modified, making iteration safe from concurrent changes.

@ServerEndpoint("/notifications")
public class NotificationServer {
    // Use a thread-safe collection for session management
    private static final CopyOnWriteArrayList<Session> sessions = new CopyOnWriteArrayList<>();

    @OnOpen
    public void onOpen(Session session) {
        sessions.add(session);
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        for (Session s : sessions) {
            // ALWAYS check if the session is still open before sending
            if (s.isOpen()) {
                try {
                    s.getBasicRemote().sendText("User said: " + message);
                } catch (IOException e) {
                    // Log the error and potentially clean up the session
                    e.printStackTrace();
                }
            }
        }
    }

    @OnClose
    public void onClose(Session session) {
        sessions.remove(session);
    }
}

Understanding the Full-Duplex Flow

The key takeaway here is that @ServerEndpoint turns your Java class into a handler for a persistent pipe. Unlike an HTTP controller, the Session object is your lifeline. You can store user IDs in the session's user properties using session.getUserProperties().put("userId", id), which allows you to target specific users instead of just broadcasting to everyone.

One quick tip: getBasicRemote() is synchronous. If you're sending huge amounts of data or dealing with slow clients, it can block your execution thread. In high-throughput production systems, you'll want to look into getAsyncRemote(), which lets you send messages without waiting for the network ACK, though it requires a bit more care with callbacks.




📋 Practical Task

Build a Real-Time Stock Price Broadcaster

Your task is to create a WebSocket server that simulates a live stock ticker. Instead of waiting for a client message to trigger a response, the server should push updates autonomously.

  • Create a @ServerEndpoint("/stocks") class.
  • Maintain a thread-safe collection of active Session objects.
  • Implement a ScheduledExecutorService (or a separate background thread) that generates a random "price" for a stock (e.g., "AAPL: 150.25") every 2 seconds.
  • The background task must iterate through all connected sessions and push the current price update to every open session.
  • Ensure that when a user disconnects, they are removed from the broadcast list to prevent memory leaks.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.