Add netty websocket game server with web client

This commit is contained in:
solver-app[bot]
2025-02-05 13:54:13 +00:00
committed by GitHub
parent b829ebc717
commit 5ad793e069
8 changed files with 521 additions and 2 deletions
@@ -0,0 +1,33 @@
package com.gameserver;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class GameMessage {
private final String type;
private final String payload;
@JsonCreator
public GameMessage(
@JsonProperty("type") String type,
@JsonProperty("payload") String payload) {
this.type = type;
this.payload = payload;
}
public String getType() {
return type;
}
public String getPayload() {
return payload;
}
@Override
public String toString() {
return "GameMessage{" +
"type='" + type + '\'' +
", payload='" + payload + '\'' +
'}';
}
}
@@ -0,0 +1,85 @@
package com.gameserver;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GameServer {
private static final Logger logger = LoggerFactory.getLogger(GameServer.class);
private final int port;
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
public GameServer(int port) {
this.port = port;
}
public void start() throws Exception {
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// HTTP codec for WebSocket handshake and static files
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
// Static file handler for serving the web client
pipeline.addLast(new HttpStaticFileHandler("src/main/resources/static"));
// WebSocket protocol handler
pipeline.addLast(new WebSocketServerProtocolHandler("/game", null, true));
// Game handler for WebSocket frames
pipeline.addLast(new GameWebSocketHandler());
}
});
Channel channel = bootstrap.bind(port).sync().channel();
logger.info("Game server started on port {}", port);
channel.closeFuture().sync();
} finally {
shutdown();
}
}
public void shutdown() {
if (bossGroup != null) {
bossGroup.shutdownGracefully();
}
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
logger.info("Game server shutdown complete");
}
public static void main(String[] args) {
int port = 8080;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
}
GameServer server = new GameServer(port);
try {
server.start();
} catch (Exception e) {
logger.error("Failed to start game server", e);
}
}
}
@@ -0,0 +1,52 @@
package com.gameserver;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GameWebSocketHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
private static final Logger logger = LoggerFactory.getLogger(GameWebSocketHandler.class);
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) {
if (frame instanceof TextWebSocketFrame) {
String text = ((TextWebSocketFrame) frame).text();
try {
GameMessage message = objectMapper.readValue(text, GameMessage.class);
logger.info("Received message: {}", message);
// Create response message
GameMessage response = new GameMessage("SERVER_RESPONSE", "Received: " + message.getType());
String responseJson = objectMapper.writeValueAsString(response);
ctx.channel().writeAndFlush(new TextWebSocketFrame(responseJson));
} catch (Exception e) {
logger.error("Error processing message: {}", e.getMessage());
ctx.channel().writeAndFlush(new TextWebSocketFrame("Error processing message"));
}
} else {
String message = "Unsupported frame type: " + frame.getClass().getName();
throw new UnsupportedOperationException(message);
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
logger.info("Client connected: {}", ctx.channel().remoteAddress());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
logger.info("Client disconnected: {}", ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.error("Channel exception: {}", cause.getMessage());
ctx.close();
}
}
@@ -0,0 +1,143 @@
package com.gameserver;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.stream.ChunkedFile;
import io.netty.handler.ssl.SslHandler;
import java.io.*;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import static io.netty.handler.codec.http.HttpHeaderNames.*;
import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
public class HttpStaticFileHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private final String staticPath;
public HttpStaticFileHandler(String staticPath) {
this.staticPath = staticPath;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
if (!request.decoderResult().isSuccess()) {
sendError(ctx, BAD_REQUEST);
return;
}
if (!HttpMethod.GET.equals(request.method())) {
sendError(ctx, METHOD_NOT_ALLOWED);
return;
}
final String uri = request.uri();
final String path = sanitizeUri(uri);
if (path == null) {
sendError(ctx, FORBIDDEN);
return;
}
File file = new File(staticPath + path);
if (!file.exists()) {
sendError(ctx, NOT_FOUND);
return;
}
if (file.isDirectory()) {
if (uri.endsWith("/")) {
file = new File(file, "index.html");
} else {
sendRedirect(ctx, uri + '/');
return;
}
}
if (!file.isFile()) {
sendError(ctx, FORBIDDEN);
return;
}
try {
RandomAccessFile raf = new RandomAccessFile(file, "r");
long fileLength = raf.length();
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
HttpUtil.setContentLength(response, fileLength);
setContentTypeHeader(response, file);
if (HttpUtil.isKeepAlive(request)) {
response.headers().set(CONNECTION, KEEP_ALIVE);
}
ctx.write(response);
ChannelFuture sendFileFuture = ctx.write(new ChunkedFile(raf, 0, fileLength, 8192),
ctx.newProgressivePromise());
ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!HttpUtil.isKeepAlive(request)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
} catch (FileNotFoundException fnfe) {
sendError(ctx, NOT_FOUND);
}
}
private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status);
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
response.content().writeBytes(status.toString().getBytes(StandardCharsets.UTF_8));
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
response.headers().set(LOCATION, newUri);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static String sanitizeUri(String uri) {
try {
uri = URLDecoder.decode(uri, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
try {
uri = URLDecoder.decode(uri, StandardCharsets.ISO_8859_1.name());
} catch (UnsupportedEncodingException ex) {
throw new Error(ex);
}
}
if (!uri.startsWith("/")) {
return null;
}
uri = uri.replace('/', File.separatorChar);
if (uri.contains(File.separator + '.') ||
uri.contains('.' + File.separator) ||
uri.startsWith(".") || uri.endsWith(".") ||
uri.contains("..")) {
return null;
}
return uri;
}
private static void setContentTypeHeader(HttpResponse response, File file) {
String name = file.getName().toLowerCase();
if (name.endsWith(".html") || name.endsWith(".htm")) {
response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
} else if (name.endsWith(".css")) {
response.headers().set(CONTENT_TYPE, "text/css; charset=UTF-8");
} else if (name.endsWith(".js")) {
response.headers().set(CONTENT_TYPE, "application/javascript; charset=UTF-8");
} else if (name.endsWith(".json")) {
response.headers().set(CONTENT_TYPE, "application/json; charset=UTF-8");
} else {
response.headers().set(CONTENT_TYPE, "application/octet-stream");
}
}
}