Add WebSocket support with routing, origin validation, session management, and broadcasting

This commit is contained in:
CodingPhoenixx
2026-05-28 13:23:23 +02:00
parent b75e1e5c6e
commit 994e7fa80c
11 changed files with 799 additions and 4 deletions
+109
View File
@@ -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();
```