From a24b061c0fed7c2241e66d6ec6c7f732e15d1a1b Mon Sep 17 00:00:00 2001 From: miaoqingshuai Date: Wed, 17 Jun 2026 20:04:36 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BA=A2=E4=B8=AD=E6=9C=BA=E5=99=A8=E4=BA=BA?= =?UTF-8?q?=20http=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/robot/mj/Config.java | 17 +- .../main/java/robot/mj/EXGameController.java | 112 ++++++++++- .../src/main/java/robot/mj/EXMainServer.java | 23 ++- .../main/java/robot/mj/RobotHttpServer.java | 175 ++++++++++++++++++ 4 files changed, 312 insertions(+), 15 deletions(-) create mode 100644 robots/majiang/robot_mj_hz/src/main/java/robot/mj/RobotHttpServer.java diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/Config.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/Config.java index cdfc9f4..c9e85e7 100644 --- a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/Config.java +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/Config.java @@ -29,6 +29,9 @@ public class Config { /** Web组加入房间协议 */ public static final String WEB_GROUP_JOIN_ROOM = "225"; + public static final String WEB_GROUP_JOIN_ROOM11 = "285"; + + /** Web组主动重连协议 */ public static final String WEB_GROUP_ACTIVE_RECONNECT = "226"; @@ -36,11 +39,11 @@ public class Config { /** 游戏服务器主机地址 */ /*public static final String GAME_SERVER_HOST = "8.134.76.43"; public static final String DEFAULT_GROUP_ID = "762479";*/ - public static final String DEFAULT_GROUP_ID = "383709"; - public static final String GAME_SERVER_HOST = "8.163.97.101"; + public static final String DEFAULT_GROUP_ID = "426149"; + public static final String GAME_SERVER_HOST = "8.138.162.178"; /** 游戏服务器端口 */ - public static final String GAME_SERVER_PORT = "26421"; + public static final String GAME_SERVER_PORT = "16421"; /** 默认密码 */ public static final String DEFAULT_PASSWORD = "123456"; @@ -48,5 +51,13 @@ public class Config { /** 默认PID */ public static final String DEFAULT_PID = "22"; + /** + * 机器人HTTP服务端口(接收 web_group 通过 HTTP 发送的 225 协议) + * 规则:TCP端口 + 1000(TCP=8722,HTTP=9722) + */ + public static final int HTTP_SERVER_PORT = 9722; + + /** 机器人HTTP服务路径 */ + public static final String HTTP_PATH_JOIN_ROOM = "/robot/joinRoom"; } \ No newline at end of file diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXGameController.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXGameController.java index 612521d..71550fc 100644 --- a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXGameController.java +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXGameController.java @@ -229,15 +229,43 @@ public class EXGameController extends GameController { } } + /** - * 接收来自web_group的加入房间协议 + * 接收来自web_group的加入房间协议(TCP 入口,保留原有行为) + * 核心逻辑已提取到 processWebGroupJoin,供 HTTP 入口复用 */ @ActionKey(value = Config.WEB_GROUP_JOIN_ROOM, validate = GameInterceptor.NOT_PLAYER) public void webGroup(Session session, ITObject params, int gid) { int robotId = params.getInt("robotid"); String roomId = params.getString("roomid"); int groupId = params.getInt("groupid"); - + + ITObject result = processWebGroupJoin(robotId, roomId, groupId, params); + + // 仅在"ID冲突"场景下回错误响应,其他场景保持原有静默行为不变 + int code = result.containsKey("code") ? result.getInt("code") : 0; + if (code == 1) { + ITObject errorResponse = TObject.newInstance(); + errorResponse.putString("status", "failed"); + errorResponse.putString("message", result.containsKey("message") ? result.getString("message") : "处理失败"); + MainServer.instance.sendResponse(gid, 1, errorResponse, session); + } + } + + /** + * 处理 web_group 加入房间请求 - 核心逻辑(与传输方式无关) + * 供 TCP 入口(webGroup)和 HTTP 入口(RobotHttpServer)共用 + * + * @param robotId 机器人ID + * @param roomId 房间ID + * @param groupId 群组ID + * @param params 原始参数 + * @return ITObject 处理结果,包含: + * code (0=成功, 1=ID冲突需告知调用方, 2=不同机器人冲突静默忽略) + * message (描述信息) + */ + public ITObject processWebGroupJoin(int robotId, String roomId, int groupId, ITObject params) { + ITObject result = TObject.newInstance(); String lockKey = "room_lock:" + roomId; synchronized (lockKey.intern()) { if (checkRobotInRoomRedis(roomId, String.valueOf(robotId))) { @@ -249,11 +277,9 @@ public class EXGameController extends GameController { } else { if (isPlayerIdConflictInRoom(roomId, robotId)) { log.warn("检测到机器人{"+robotId+"}与房间{"+roomId+"}中的真人玩家 ID 冲突,拒绝加入"); - ITObject errorResponse = TObject.newInstance(); - errorResponse.putString("status", "failed"); - errorResponse.putString("message", "机器人 ID 与房间内玩家冲突"); - MainServer.instance.sendResponse(gid, 1, errorResponse, session); - return; + result.putInt("code", 1); + result.putString("message", "机器人 ID 与房间内玩家冲突"); + return result; } } } @@ -281,7 +307,9 @@ public class EXGameController extends GameController { if (robotId != existingRobotId) { //不同机器人的冲突 log.warn("房间{"+roomId+"}中Redis已存在机器人{"+existingRobotId+"},当前机器人{"+robotId+"}不执行加入逻辑"); - return; + result.putInt("code", 2); + result.putString("message", "Redis已存在其他机器人,不执行加入逻辑"); + return result; } } } @@ -291,8 +319,76 @@ public class EXGameController extends GameController { joinRoomCommon(robotId, roomId, groupId, params); log.info("225 已进入房间准备成功:room:{"+roomId+"} robot:{"+robotId+"}"); } + result.putInt("code", 0); + result.putString("message", "success"); + return result; } + +// /** +// * 接收来自web_group的加入房间协议 +// */ +// @ActionKey(value = Config.WEB_GROUP_JOIN_ROOM, validate = GameInterceptor.NOT_PLAYER) +// public void webGroup(Session session, ITObject params, int gid) { +// int robotId = params.getInt("robotid"); +// String roomId = params.getString("roomid"); +// int groupId = params.getInt("groupid"); +// +// String lockKey = "room_lock:" + roomId; +// synchronized (lockKey.intern()) { +// if (checkRobotInRoomRedis(roomId, String.valueOf(robotId))) { +// log.info("机器人{"+robotId+"}已在房间{"+roomId+"}中(Redis 中存在),直接允许加入"); +// } else { +// RobotUser existingRobotUser = getRobotRoomInfo(String.valueOf(robotId)); +// if (existingRobotUser != null && existingRobotUser.getCurrentRoomId() == Integer.parseInt(roomId)) { +// log.info("机器人{"+robotId+"}已在房间{"+roomId+"}中(本地映射存在),直接允许加入"); +// } else { +// if (isPlayerIdConflictInRoom(roomId, robotId)) { +// log.warn("检测到机器人{"+robotId+"}与房间{"+roomId+"}中的真人玩家 ID 冲突,拒绝加入"); +// ITObject errorResponse = TObject.newInstance(); +// errorResponse.putString("status", "failed"); +// errorResponse.putString("message", "机器人 ID 与房间内玩家冲突"); +// MainServer.instance.sendResponse(gid, 1, errorResponse, session); +// return; +// } +// } +// } +// +// //检查Redis中该房间是否真的包含当前机器人 +// if (!checkRobotInRoomRedis(roomId, String.valueOf(robotId))) { +// //Redis中不存在该机器人 清理本地可能的错误映射 +// List robotUsers = getRobotUsersByRoomId(Integer.parseInt(roomId)); +// if (!robotUsers.isEmpty()) { +// synchronized (robotUsers) { +// RobotUser robotUser = robotUsers.get(0); +// log.warn("房间{"+roomId+"}中Redis未找到机器人{"+robotId+"},但本地映射存在{"+robotUser.getRobotId()+"},清理本地映射"); +// robotRoomMapping.remove(robotUser.getConnecId()); +// robotRoomMapping.remove(robotUser.getRobotId()); +// } +// } +// } else { +// //Redis中存在该机器人 检查是否是不同机器人的冲突 +// List robotUsers = getRobotUsersByRoomId(Integer.parseInt(roomId)); +// if (!robotUsers.isEmpty()) { +// synchronized (robotUsers) { +// RobotUser robotUser = robotUsers.get(0); +// int existingRobotId = Integer.parseInt(robotUser.getRobotId()); +// +// if (robotId != existingRobotId) { +// //不同机器人的冲突 +// log.warn("房间{"+roomId+"}中Redis已存在机器人{"+existingRobotId+"},当前机器人{"+robotId+"}不执行加入逻辑"); +// return; +// } +// } +// } +// } +// log.info("225 开始进房间:room:{"+roomId+"} robot:{"+robotId+"}"); +// //加入房间 +// joinRoomCommon(robotId, roomId, groupId, params); +// log.info("225 已进入房间准备成功:room:{"+roomId+"} robot:{"+robotId+"}"); +// } +// } + /** * 接收来自web_group的主动重连协议 */ diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXMainServer.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXMainServer.java index 2a9f209..bed482d 100644 --- a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXMainServer.java +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXMainServer.java @@ -17,19 +17,28 @@ import taurus.client.NetManager; import static robot.mj.EXGameController.robotRoomMapping; -/** - * 红中麻将机器人主服务器 - * TCP服务端接收robot_mgr的协议 同时作为客户端连接game_mj_cs处理AI逻辑 - */ + public class EXMainServer extends MainServer{ private static final Logger log = LoggerFactory.getLogger(EXMainServer.class); private static final RobotConnectionManager robotConnectionManager = new RobotConnectionManager(); + /** 机器人HTTP服务(接收 web_group 通过 HTTP 发送的 225 协议,替代TCP异步方式) */ + private RobotHttpServer robotHttpServer; + @Override public void onStart() { super.onStart(); + // 启动机器人HTTP服务(接收 web_group 通过 HTTP 发送的 225 协议) + // 替代原 TCP 异步多线程方式(TaurusClient+CompletableFuture),降低CPU占用 + try { + robotHttpServer = new RobotHttpServer(); + robotHttpServer.start(Config.HTTP_SERVER_PORT); + } catch (Exception e) { + log.error("启动 RobotHttpServer 失败,端口:" + Config.HTTP_SERVER_PORT, e); + } + //JVM关闭钩子 Runtime.getRuntime().addShutdownHook(new Thread(() -> { log.info("收到JVM关闭信号,开始优雅关闭..."); @@ -163,6 +172,12 @@ public class EXMainServer extends MainServer{ public void onStop() { super.onStop(); + // 停止机器人HTTP服务 + if (robotHttpServer != null) { + robotHttpServer.stop(); + robotHttpServer = null; + } + log.info("红中麻将机器人服务器已停止"); } } \ No newline at end of file diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/RobotHttpServer.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/RobotHttpServer.java new file mode 100644 index 0000000..519fd17 --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/RobotHttpServer.java @@ -0,0 +1,175 @@ +package robot.mj; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.ByteArrayOutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import com.taurus.core.entity.ITObject; +import com.taurus.core.entity.TObject; +import com.taurus.core.util.Logger; +import com.robot.Global; + + +public class RobotHttpServer { + private static final Logger log = Logger.getLogger(RobotHttpServer.class); + + /** 工作线程数(同时处理的并发请求数) */ + private static final int WORKER_THREADS = 8; + + /** HTTP 响应超时(用于优雅关闭,秒) */ + private static final int STOP_DELAY = 2; + + private HttpServer server; + private ExecutorService executor; + private final Gson gson = new Gson(); + + /** + * 启动 HTTP 服务 + * @param port 监听端口 + * @throws IOException 端口被占用等异常 + */ + public void start(int port) throws IOException { + server = HttpServer.create(new InetSocketAddress(port), 0); + server.createContext(Config.HTTP_PATH_JOIN_ROOM, new JoinRoomHandler()); + executor = Executors.newFixedThreadPool(WORKER_THREADS); + server.setExecutor(executor); + server.start(); + log.info("RobotHttpServer已启动,监听端口:" + port + ",路径:" + Config.HTTP_PATH_JOIN_ROOM + ",工作线程:" + WORKER_THREADS); + } + + /** + * 停止 HTTP 服务 + */ + public void stop() { + if (server != null) { + server.stop(STOP_DELAY); + server = null; + } + if (executor != null) { + executor.shutdown(); + try { + if (!executor.awaitTermination(STOP_DELAY, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + executor = null; + } + log.info("RobotHttpServer已停止"); + } + + /** + * 处理 /robot/joinRoom 请求的 Handler + */ + private class JoinRoomHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + // 仅接受 POST + if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) { + writeJsonResponse(exchange, 405, buildErrorResponse("Method Not Allowed", "只支持POST请求")); + return; + } + + Map respData = new HashMap<>(); + try { + // 读取请求体 + String body = readRequestBody(exchange); + log.info("HTTP 收到加入房间请求:" + body); + + // 解析 JSON + @SuppressWarnings("unchecked") + Map data = gson.fromJson(body, Map.class); + if (data == null) { + writeJsonResponse(exchange, 400, buildErrorResponse("Bad Request", "请求体为空或非合法JSON")); + return; + } + + Number robotIdNum = (Number) data.get("robotid"); + Object roomIdObj = data.get("roomid"); + Number groupIdNum = (Number) data.get("groupid"); + + if (robotIdNum == null || roomIdObj == null || groupIdNum == null) { + writeJsonResponse(exchange, 400, buildErrorResponse("Bad Request", "缺少必要参数 robotid/roomid/groupid")); + return; + } + + int robotId = robotIdNum.intValue(); + String roomId = String.valueOf(roomIdObj); + int groupId = groupIdNum.intValue(); + + // 构造 ITObject 参数(与原 TCP 入参保持一致) + ITObject params = TObject.newInstance(); + params.putInt("robotid", robotId); + params.putString("roomid", roomId); + params.putInt("groupid", groupId); + + // 调用公共处理逻辑(与 TCP webGroup 方法共用) + ITObject result = ((EXGameController) Global.gameCtr).processWebGroupJoin(robotId, roomId, groupId, params); + + int code = result.containsKey("code") ? result.getInt("code") : 0; + String message = result.containsKey("message") ? result.getString("message") : "success"; + respData.put("code", code); + respData.put("message", message); + } catch (Exception e) { + log.error("HTTP处理加入房间请求异常", e); + respData.put("code", 500); + respData.put("message", "服务器内部错误: " + e.getMessage()); + } + + writeJsonResponse(exchange, 200, respData); + } + + /** + * 读取请求体为字符串 + */ + private String readRequestBody(HttpExchange exchange) throws IOException { + InputStream in = exchange.getRequestBody(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int n; + while ((n = in.read(buf)) > 0) { + baos.write(buf, 0, n); + } + in.close(); + return new String(baos.toByteArray(), StandardCharsets.UTF_8); + } + + /** + * 写入 JSON 响应 + */ + private void writeJsonResponse(HttpExchange exchange, int httpCode, Map data) throws IOException { + String json = gson.toJson(data); + byte[] bytes = json.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=UTF-8"); + exchange.sendResponseHeaders(httpCode, bytes.length); + OutputStream out = exchange.getResponseBody(); + out.write(bytes); + out.close(); + } + + /** + * 构造错误响应 + */ + private Map buildErrorResponse(String error, String message) { + Map data = new HashMap<>(); + data.put("code", -1); + data.put("error", error); + data.put("message", message); + return data; + } + } +}