Add WebSocket support with routing, origin validation, session management, and broadcasting
This commit is contained in:
@@ -11,6 +11,7 @@ A lightweight, high-performance HTTP server library built on top of Netty. Nexus
|
||||
- **Middleware chain** — attach cross-cutting logic to all routes
|
||||
- **CORS support** — configurable origins, methods, headers, credentials, and preflight caching
|
||||
- **Rate limiting** — four algorithm implementations with per-IP, per-token, or custom key strategies
|
||||
- **WebSockets** — path-routed handlers with origin validation, idle timeout, frame size limits and permessage-deflate
|
||||
- **JSON I/O** — built-in Jackson integration for request parsing and response serialization
|
||||
|
||||
---
|
||||
@@ -265,6 +266,104 @@ Every response automatically includes:
|
||||
|
||||
---
|
||||
|
||||
## WebSockets
|
||||
|
||||
WebSocket routes are registered on a `WebSocketRouter` and attached to the server alongside the HTTP `Router`. Upgrade requests (`GET` + `Upgrade: websocket`) are intercepted before the HTTP router runs.
|
||||
|
||||
### Handler
|
||||
|
||||
Implement `WebSocketHandler`. All callbacks are optional.
|
||||
|
||||
```java
|
||||
public class ChatSocket implements WebSocketHandler {
|
||||
|
||||
private final WebSocketGroup room = new WebSocketGroup("chat");
|
||||
|
||||
@Override
|
||||
public void onOpen(WebSocketSession session) {
|
||||
room.add(session);
|
||||
session.send("{\"type\":\"welcome\",\"id\":\"" + session.id() + "\"}");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocketSession session, String message) {
|
||||
room.broadcastExcept(session, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(WebSocketSession session, int code, String reason) {
|
||||
room.remove(session);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Registration
|
||||
|
||||
```java
|
||||
WebSocketRouter wsRouter = new WebSocketRouter()
|
||||
.on("/ws/chat", new ChatSocket())
|
||||
.on("/ws/rooms/{room}", new RoomSocket());
|
||||
|
||||
WebSocketConfig wsConfig = WebSocketConfig.builder()
|
||||
.allowedOrigins("https://app.example.com")
|
||||
.maxFramePayloadLength(64 * 1024) // 64 KiB per frame
|
||||
.maxAggregatedMessageSize(1024 * 1024) // 1 MiB upgrade body cap
|
||||
.idleTimeout(Duration.ofSeconds(60)) // close idle peers
|
||||
.subprotocols("chat.v1")
|
||||
.compression(true) // permessage-deflate
|
||||
.build();
|
||||
|
||||
HttpServer.builder(8080, router)
|
||||
.withWebSockets(wsRouter, wsConfig)
|
||||
.start();
|
||||
```
|
||||
|
||||
Use `WebSocketConfig.defaults()` (or `.anyOrigin()` on the builder) only for local development — production deployments should always allow-list origins explicitly.
|
||||
|
||||
### Session API
|
||||
|
||||
```java
|
||||
session.id(); // stable UUID for this connection
|
||||
session.path(); // matched path
|
||||
session.pathParam("room"); // path parameter, e.g. from /ws/rooms/{room}
|
||||
session.remoteAddress(); // client IP
|
||||
session.attribute("userId", id); // attach state to the session
|
||||
session.attribute("userId"); // read it back
|
||||
|
||||
session.send("text"); // text frame
|
||||
session.sendJson(dto); // serialized via Jackson
|
||||
session.sendBinary(bytes); // binary frame
|
||||
session.ping(); // ping frame
|
||||
session.close(); // normal close (1000)
|
||||
session.close(1011, "internal"); // close with code + reason
|
||||
```
|
||||
|
||||
### Broadcasting
|
||||
|
||||
`WebSocketGroup` is a thin fluent wrapper around Netty's `ChannelGroup` — joining a session is cheap and removal happens automatically when the channel closes.
|
||||
|
||||
```java
|
||||
WebSocketGroup group = new WebSocketGroup("lobby")
|
||||
.add(sessionA)
|
||||
.add(sessionB);
|
||||
|
||||
group.broadcast("hello everyone");
|
||||
group.broadcastJson(eventDto);
|
||||
group.broadcastExcept(sessionA, "everyone but A");
|
||||
```
|
||||
|
||||
### Security & limits
|
||||
|
||||
| Concern | How it's handled |
|
||||
|---|---|
|
||||
| Cross-origin upgrades | `Origin` header validated against `WebSocketConfig.allowedOrigins(...)`; mismatched origins are rejected with `403` |
|
||||
| Memory exhaustion | `maxFramePayloadLength` caps a single frame; `maxAggregatedMessageSize` caps the upgrade request body |
|
||||
| Idle / zombie connections | `idleTimeout` triggers a server-side close when no read **and** no write happen within the window |
|
||||
| User code isolation | All callbacks dispatch onto Java virtual threads, never the Netty event loop |
|
||||
| Subprotocol negotiation | Server advertises the configured `subprotocols(...)` list; clients that ask for an unsupported subprotocol fail the handshake |
|
||||
|
||||
---
|
||||
|
||||
## Full Example
|
||||
|
||||
```java
|
||||
@@ -304,9 +403,19 @@ RateLimitConfig rlConfig = RateLimitConfig.builder()
|
||||
.build();
|
||||
RateLimitGate gate = new RateLimitGate(rlConfig);
|
||||
|
||||
// WebSockets
|
||||
WebSocketRouter wsRouter = new WebSocketRouter()
|
||||
.on("/ws/chat", new ChatSocket());
|
||||
|
||||
WebSocketConfig wsConfig = WebSocketConfig.builder()
|
||||
.allowedOrigins("https://app.example.com")
|
||||
.idleTimeout(Duration.ofSeconds(60))
|
||||
.build();
|
||||
|
||||
HttpServer.builder(8080, router)
|
||||
.withCorsHandler(cors)
|
||||
.withRateLimitGate(gate)
|
||||
.withWebSockets(wsRouter, wsConfig)
|
||||
.start();
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user