mirror of
https://github.com/tiennm99/try-solver.git
synced 2026-06-09 20:14:33 +00:00
Add netty websocket game server with web client
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user