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
@@ -0,0 +1,129 @@
package dev.coph.nextusweb.server.websocket;
import dev.coph.nextusweb.server.json.JsonMapper;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.AttributeKey;
import io.netty.util.CharsetUtil;
import tools.jackson.core.JacksonException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class WebSocketSession {
static final AttributeKey<WebSocketSession> SESSION_KEY =
AttributeKey.valueOf("nexusweb.ws.session");
private final Channel channel;
private final String id;
private final String path;
private final Map<String, String> pathParams;
private final Map<String, Object> attributes = new ConcurrentHashMap<>();
WebSocketSession(Channel channel, String path, Map<String, String> pathParams) {
this.channel = channel;
this.id = UUID.randomUUID().toString();
this.path = path;
this.pathParams = pathParams;
}
public String id() {
return id;
}
public String path() {
return path;
}
public String pathParam(String name) {
return pathParams.get(name);
}
public boolean isOpen() {
return channel.isActive();
}
public String remoteAddress() {
SocketAddress addr = channel.remoteAddress();
if (addr instanceof InetSocketAddress inet) {
return inet.getAddress().getHostAddress();
}
return addr == null ? null : addr.toString();
}
public Channel channel() {
return channel;
}
public WebSocketSession attribute(String name, Object value) {
if (value == null) attributes.remove(name);
else attributes.put(name, value);
return this;
}
@SuppressWarnings("unchecked")
public <T> T attribute(String name) {
return (T) attributes.get(name);
}
public ChannelFuture send(String text) {
if (!channel.isActive()) return channel.newSucceededFuture();
return channel.writeAndFlush(new TextWebSocketFrame(text));
}
public ChannelFuture sendJson(Object value) {
try {
byte[] bytes = JsonMapper.MAPPER.writeValueAsBytes(value);
if (!channel.isActive()) return channel.newSucceededFuture();
ByteBuf buf = channel.alloc().buffer(bytes.length).writeBytes(bytes);
return channel.writeAndFlush(new TextWebSocketFrame(true, 0, buf));
} catch (JacksonException e) {
throw new RuntimeException("JSON serialization failed", e);
}
}
public ChannelFuture sendBinary(byte[] data) {
if (!channel.isActive()) return channel.newSucceededFuture();
ByteBuf buf = channel.alloc().buffer(data.length).writeBytes(data);
return channel.writeAndFlush(new BinaryWebSocketFrame(buf));
}
public ChannelFuture ping() {
if (!channel.isActive()) return channel.newSucceededFuture();
return channel.writeAndFlush(new PingWebSocketFrame());
}
public ChannelFuture close() {
return close(1000, "");
}
public ChannelFuture close(int code, String reason) {
if (!channel.isActive()) return channel.newSucceededFuture();
return channel.writeAndFlush(new CloseWebSocketFrame(code, reason))
.addListener(ChannelFutureListener.CLOSE);
}
static ChannelFuture sendRaw(Channel channel, String text) {
if (!channel.isActive()) return channel.newSucceededFuture();
ByteBuf buf = channel.alloc().buffer();
buf.writeCharSequence(text, CharsetUtil.UTF_8);
return channel.writeAndFlush(new TextWebSocketFrame(true, 0, buf));
}
static ChannelFuture sendRawBinary(Channel channel, byte[] data) {
if (!channel.isActive()) return channel.newSucceededFuture();
ByteBuf buf = channel.alloc().buffer(data.length).writeBytes(Unpooled.wrappedBuffer(data));
return channel.writeAndFlush(new BinaryWebSocketFrame(buf));
}
}