Gumdrop provides a standalone WebSocket service and client, built on top of the HTTP transport but presenting a clean, message-oriented programming model. WebSocket connections begin as HTTP upgrade requests but, once established, operate as raw bidirectional message channels with no further HTTP semantics.
The same WebSocketEventHandler interface is used on both the server
and client side, making it straightforward to write applications that operate in
either role or both.
The WebSocket Protocol (RFC 6455) provides full-duplex communication over a single TCP connection. Unlike HTTP’s request–response model, either side can send messages at any time once the connection is open. This makes it ideal for real-time applications such as chat, live feeds, collaborative editing, and IoT telemetry.
Gumdrop’s WebSocket implementation is structured around a clean separation of concerns:
Service
interface and knows nothing about HTTP.HTTPListener and encapsulates the HTTP-to-WebSocket upgrade
handshake. All HTTP machinery is confined to this layer.WebSocketEventHandler
interface as the server.
To create a WebSocket server, extend WebSocketService and
implement the createConnectionHandler method. This method is called
for each incoming WebSocket connection and must return a
WebSocketEventHandler (or null to reject the
connection).
public class EchoService extends WebSocketService {
@Override
protected WebSocketEventHandler createConnectionHandler(
String requestPath, Headers upgradeHeaders) {
return new DefaultWebSocketEventHandler() {
@Override
public void textMessageReceived(WebSocketSession session,
String message) {
try {
session.sendText("Echo: " + message);
} catch (IOException e) {
// handle error
}
}
};
}
}
The service receives the request path and upgrade headers, which can be used for routing or authentication decisions:
@Override
protected WebSocketEventHandler createConnectionHandler(
String requestPath, Headers upgradeHeaders) {
// Route based on path
if ("/chat".equals(requestPath)) {
return new ChatHandler();
} else if ("/notifications".equals(requestPath)) {
return new NotificationHandler();
}
// Reject unknown paths
return null;
}
The WebSocketEventHandler interface provides five callbacks:
opened(WebSocketSession session) – connection established;
the session is ready for sending messagestextMessageReceived(WebSocketSession session, String message)
– a text message was received from the peerbinaryMessageReceived(WebSocketSession session, ByteBuffer data)
– a binary message was received from the peerclosed(int code, String reason) – the connection was
closederror(Throwable cause) – an error occurred
DefaultWebSocketEventHandler provides empty implementations of all
methods for convenience.
The WebSocketSession interface is provided to the handler in
each callback. It does not need to be stored between calls:
sendText(String message) – send a text messagesendBinary(ByteBuffer data) – send binary datasendPing(ByteBuffer payload) – send a ping frame (peer
responds with pong automatically)close() – close with normal status (1000)close(int code, String reason) – close with a specific
status codeisOpen() – check if the session is still active
Standard close codes are defined in WebSocketConnection.CloseCodes:
1000 – Normal closure1001 – Going away (server shutdown or client navigating away)1002 – Protocol error1003 – Unsupported data type1009 – Message too big1011 – Internal server error
WebSocket supports application-level subprotocol negotiation via the
Sec-WebSocket-Protocol header. Override
selectSubprotocol on the service to participate:
@Override
protected String selectSubprotocol(Headers upgradeHeaders) {
String requested = upgradeHeaders.getValue("Sec-WebSocket-Protocol");
if (requested != null) {
// Client may request multiple protocols, comma-separated
if (requested.contains("graphql-ws")) {
return "graphql-ws";
}
if (requested.contains("mqtt")) {
return "mqtt";
}
}
return null; // no subprotocol
}
RFC 6455 §9 defines an extension negotiation mechanism via the
Sec-WebSocket-Extensions header. Extensions can augment the
protocol — for example by compressing message payloads.
Gumdrop implements a general-purpose extension framework through the
WebSocketExtension interface. Each extension declares which RSV
bits it uses, participates in the opening handshake negotiation, and provides
encode/decode transforms applied to message payloads.
The built-in PerMessageDeflateExtension implements
RFC 7692, compressing
WebSocket messages with DEFLATE. It is enabled by default on both
WebSocketListener (server) and WebSocketClient (client).
When a client offers permessage-deflate and the server accepts,
all data messages are transparently compressed.
The RSV1 bit is set on the first frame of each compressed message. The
trailing empty DEFLATE block marker (0x00 0x00 0xFF 0xFF) is
stripped from compressed output and re-appended before decompression, per
RFC 7692 §7.2.1.
Supported negotiation parameters:
server_no_context_takeover – server resets LZ77 window per messageclient_no_context_takeover – client resets LZ77 window per messageserver_max_window_bits – max server LZ77 window size (8–15)client_max_window_bits – max client LZ77 window size (8–15)To disable permessage-deflate on the server:
WebSocketListener listener = new WebSocketListener(); listener.setDeflateEnabled(false);
To disable it on the client:
WebSocketClient client = new WebSocketClient("example.com", 443);
client.setDeflateEnabled(false);
Implement the WebSocketExtension interface to create a custom
extension. Key methods:
getName() – the extension token (e.g. "permessage-deflate")usesRsv1(), usesRsv2(), usesRsv3() – RSV bit claimsacceptOffer(params) – server-side negotiationgenerateOffer() – client-side offer parametersacceptResponse(params) – client-side acceptance of server responseencode(payload) / decode(payload) – payload transforms
RFC 6455 §7.4.1 defines close code 1009 (Message Too Big) for when a
received message exceeds the endpoint’s capacity. Gumdrop enforces a
configurable maximum message size on each WebSocketConnection:
// Set on the connection (e.g. from an event handler) // 0 = unlimited (default) connection.setMaxMessageSize(1024 * 1024); // 1 MB limit
When a single-frame message or a fragmented message assembly exceeds this limit, the connection is proactively closed with code 1009. This prevents memory exhaustion from malicious or misbehaving peers.
RFC 6455 §7.4 defines rules for close codes that appear on the wire. Codes 1004, 1005, 1006, and 1015 are reserved and MUST NOT appear in close frames; codes outside the 1000–4999 range are also invalid. Gumdrop now validates received close codes and rejects invalid ones with a protocol error (close code 1002):
The WebSocketClient class provides a high-level facade for
connecting to WebSocket servers. It uses the same
WebSocketEventHandler interface as the server, so handler code can
be reused in both roles.
WebSocketClient client = new WebSocketClient("echo.example.com", 443);
client.setSecure(true);
client.connect("/ws", new DefaultWebSocketEventHandler() {
@Override
public void opened(WebSocketSession session) {
try {
session.sendText("Hello from Gumdrop!");
} catch (IOException e) {
// handle error
}
}
@Override
public void textMessageReceived(WebSocketSession session,
String message) {
System.out.println("Received: " + message);
}
@Override
public void closed(int code, String reason) {
System.out.println("Connection closed: " + code);
}
@Override
public void error(Throwable cause) {
cause.printStackTrace();
}
});
setSecure(boolean) – use TLS (wss:// scheme)setSSLContext(SSLContext) – provide a custom SSL contextsetSubprotocol(String) – request a specific subprotocol
during the handshake
Like the HTTP client, the WebSocket client can
be assigned to a specific SelectorLoop for efficient integration
with server-side code:
// Use the same event loop as the server connection
WebSocketClient client = new WebSocketClient(selectorLoop,
"upstream.example.com", 443);
client.setSecure(true);
client.connect("/events", handler);
This is useful for server-side scenarios where a Gumdrop service needs to maintain a WebSocket connection to an upstream service (for example, subscribing to a Nostr relay from within a server handler).
Nostr is a decentralised social protocol where clients exchange JSON events with relay servers over WebSocket. A Nostr relay is a natural fit for Gumdrop’s WebSocket service:
public class NostrRelayService extends WebSocketService {
private final NostrEventStore store = new NostrEventStore();
private final SubscriptionManager subscriptions =
new SubscriptionManager();
@Override
protected WebSocketEventHandler createConnectionHandler(
String requestPath, Headers upgradeHeaders) {
return new DefaultWebSocketEventHandler() {
private WebSocketSession session;
@Override
public void opened(WebSocketSession session) {
this.session = session;
}
@Override
public void textMessageReceived(WebSocketSession session,
String message) {
// Parse Nostr message: ["EVENT", ...], ["REQ", ...], ["CLOSE", ...]
NostrMessage msg = NostrMessage.parse(message);
try {
if (msg.isEvent()) {
store.save(msg.getEvent());
session.sendText("[\"OK\",\"" + msg.getEventId()
+ "\",true,\"\"]");
subscriptions.broadcast(msg.getEvent());
} else if (msg.isRequest()) {
subscriptions.subscribe(session,
msg.getSubscriptionId(),
msg.getFilters());
} else if (msg.isClose()) {
subscriptions.unsubscribe(session,
msg.getSubscriptionId());
}
} catch (IOException e) {
// handle send error
}
}
@Override
public void closed(int code, String reason) {
subscriptions.removeAll(session);
}
};
}
}
Push server-side metrics to browser clients in real time:
public class DashboardService extends WebSocketService {
private final Set<WebSocketSession> sessions =
Collections.synchronizedSet(new HashSet<WebSocketSession>());
@Override
protected WebSocketEventHandler createConnectionHandler(
String requestPath, Headers upgradeHeaders) {
return new DefaultWebSocketEventHandler() {
private WebSocketSession session;
@Override
public void opened(WebSocketSession session) {
this.session = session;
sessions.add(session);
}
@Override
public void closed(int code, String reason) {
sessions.remove(session);
}
};
}
/** Called periodically by a timer to push metrics. */
public void broadcastMetrics(String json) {
synchronized (sessions) {
for (WebSocketSession session : sessions) {
if (session.isOpen()) {
try {
session.sendText(json);
} catch (IOException e) {
// remove on next tick
}
}
}
}
}
}
Binary WebSocket messages are well suited for compact sensor data formats. Devices connect over WebSocket and push binary telemetry:
@Override
public void binaryMessageReceived(WebSocketSession session,
ByteBuffer data) {
SensorReading reading = SensorReading.decode(data);
telemetryPipeline.ingest(reading);
}
A basic chat server can be built by broadcasting text messages to all connected
sessions, following the same pattern as the dashboard example above with
an additional textMessageReceived handler that calls
broadcastMessage.
<service id="echo" class="com.example.EchoService">
<listener class="org.bluezoo.gumdrop.websocket.WebSocketListener">
<property name="port">8080</property>
</listener>
</service>
<service id="echo" class="com.example.EchoService">
<listener class="org.bluezoo.gumdrop.websocket.WebSocketListener">
<property name="port">443</property>
<property name="secure">true</property>
<property name="keystore-file">/etc/gumdrop/keystore.p12</property>
<property name="keystore-pass">changeit</property>
</listener>
</service>
A service can expose both plaintext and TLS listeners:
<service id="relay" class="com.example.NostrRelayService">
<listener class="org.bluezoo.gumdrop.websocket.WebSocketListener">
<property name="port">8080</property>
</listener>
<listener class="org.bluezoo.gumdrop.websocket.WebSocketListener">
<property name="port">8443</property>
<property name="secure">true</property>
<property name="keystore-file">/etc/gumdrop/keystore.p12</property>
<property name="keystore-pass">changeit</property>
</listener>
</service>
A complete gumdroprc for running a Nostr relay:
<gumdroprc>
<gumdrop workers="4"/>
<service id="nostr" class="com.example.NostrRelayService">
<property name="event-store-path">/var/lib/nostr/events</property>
<listener class="org.bluezoo.gumdrop.websocket.WebSocketListener">
<property name="port">443</property>
<property name="secure">true</property>
<property name="keystore-file">/etc/gumdrop/keystore.p12</property>
<property name="keystore-pass">changeit</property>
</listener>
</service>
</gumdroprc>
WebSocketListener extends HTTPListener and inherits
all of its properties:
port – listening port (required)secure – enable TLS (default: false)keystore-file – path to keystore for TLSkeystore-pass – keystore passwordkeystore-format – keystore format (default: PKCS12)need-client-auth – require client certificatesname – optional listener name for identification
When telemetry is configured on the parent HTTP listener,
WebSocketServerMetrics records the following OpenTelemetry metrics.
Access via WebSocketListener.getWebSocketMetrics().
| Metric | Type | Description |
|---|---|---|
websocket.server.connections | Counter | Total WebSocket connections |
websocket.server.session.duration | Histogram | Session duration (ms) |
websocket.server.messages.received | Counter | Messages received (by type) |
websocket.server.messages.sent | Counter | Messages sent (by type) |
websocket.server.frames.received | Counter | Frames received (by opcode) |
websocket.server.frames.sent | Counter | Frames sent (by opcode) |
websocket.server.errors | Counter | WebSocket errors |
If you are building an HTTP service with
the HTTP server handler API and want to
support WebSocket alongside regular HTTP requests, you can perform the upgrade
manually using HTTPResponseState.upgradeToWebSocket():
public class MyHandler extends DefaultHTTPRequestHandler {
@Override
public void headers(HTTPResponseState state, Headers headers) {
if (WebSocketHandshake.isValidWebSocketUpgrade(headers)) {
String protocol = headers.getValue("Sec-WebSocket-Protocol");
state.upgradeToWebSocket(protocol, new EchoWebSocketHandler());
} else {
// Handle as regular HTTP request
Headers response = new Headers();
response.status(HTTPStatus.OK);
response.add("content-type", "text/plain");
state.headers(response);
state.startResponseBody();
state.responseBodyContent(
ByteBuffer.wrap("Hello!".getBytes()));
state.endResponseBody();
state.complete();
}
}
}
This approach is useful when a single HTTP service needs to handle both regular
HTTP traffic and WebSocket connections on the same port. For dedicated WebSocket
services, the WebSocketService / WebSocketListener
model described above is preferred because the HTTP upgrade is handled
automatically.
The servlet container (ServletService)
provides its own WebSocket upgrade path via the standard
HttpServletRequest.upgrade() mechanism. Both the standalone
WebSocketService and the servlet container converge on the same
WebSocketEventHandler interface and share the same internal
WebSocket frame engine (WebSocketConnection). This means the
underlying protocol handling is identical regardless of how the upgrade was
initiated.
For applications using the servlet container, WebSocket support is available
through the standard Java WebSocket API (JSR 356) or by calling
HttpServletRequest.upgrade() directly. For applications that do
not need the servlet programming model, the standalone
WebSocketService provides a simpler, more direct API.
← Back to Main Page | HTTP Server & Client | Servlet Container | SMTP Server & Client | DNS Server | Telemetry
Gumdrop WebSocket Service & Client