☰
Spring Boot实战:Bot管理、匹配系统与WebSocket实时通信
2026/9/29 18:58:23 网站建设 项目流程

做过后端的人都知道,CRUD是基本功,但能把CRUD做得顺手、把实时通信接得稳,才是实战里真正拉开差距的地方。这篇是Spring Boot开发实战手册的第十二篇,讲我在做一个Bot对战平台时,从零实现Bot管理前端页面、匹配系统前置准备、WebSocket集成配置的完整过程。Bot这里说的不是聊天机器人,而是用户提交的对战AI程序——用户可以在页面上编写代码、启用自己的Bot,之后Bot会进入匹配池,系统根据评分自动撮合对战,整个过程通过WebSocket实时推送到前端。这条链路覆盖了"用户提交策略、服务端撮合、实时结果推送"三个典型场景,对做游戏平台、OJ判题、实时撮合类系统的同学都有参考价值。

1. 先理清楚:Bot管理、匹配系统、WebSocket三者是什么关系

1.1 从业务链路看这一篇的定位

如果你只是单独学CRUD或者单独学WebSocket,很容易陷入"会写接口但不知道用在哪"的尴尬。我在做这个Bot对战平台时,业务链路是这样的:用户在网页上维护自己的Bot代码,可以新增、编辑、启用或禁用;启用的Bot会进入匹配池;匹配系统每隔一段时间扫描匹配池,把分数接近的Bot配成一对;配对成功后,系统通过WebSocket把"你已匹配到对手"以及后续的对战过程实时推送给前端。

所以这三块内容是咬合在一起的:前端页面的Bot CURD负责管理资源,匹配系统负责撮合资源,WebSocket负责通知结果。少了任何一环,链路都是断的。比如只做CURD不做匹配,那用户维护的Bot就是一堆死代码;只做匹配不做WebSocket,用户就只能靠轮询刷新页面来等匹配结果,体验会大打折扣。

1.2 技术选型的底层逻辑

先说CURD,我用了Spring Boot + MyBatis-Plus + Vue 3 + Element Plus的组合,没有引入太重的框架。原因很简单:Bot管理本质就是一张表的增删改查,但有几个不简单的点——代码字段是大文本、需要做用户归属校验、Bot有启用/禁用状态、列表要分页搜索。这些点让"简单CURD"变成了"带业务规则的CURD",恰恰是实战和面试里常被追问的地方。

匹配系统在第一期我没有直接上Redis或者消息队列,而是先用内存队列 + 定时任务的方式跑通。很多教程一上来就谈分布式匹配、Redis有序集合,但对一个单体应用来说,前期用ConcurrentLinkedQueue + @Scheduled完全够用,先把业务逻辑跑通,后面再迁移到Redis不迟。这里的关键是接口要抽象好,别把匹配逻辑和存储方式耦合死,后面换存储才是小事。

WebSocket选型上,我用的是Spring Boot自带的spring-boot-starter-websocket,没有上STOMP协议。原因在于这个场景只是服务端向指定客户端推送匹配结果和对战过程,不需要复杂的订阅-发布模型,原生WebSocket + JSON消息体反而更轻、更好控制。

2. Bot前端页面的CURD实现

2.1 数据模型:Bot实体与表结构

Bot表看起来简单,但字段设计上有几个坑。我最终的表结构大致是这样:

CREATE TABLE bot ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL COMMENT '所属用户ID', name VARCHAR(100) NOT NULL COMMENT 'Bot名称', description VARCHAR(255) DEFAULT '' COMMENT '描述', code TEXT NOT NULL COMMENT 'Bot代码', rating INT DEFAULT 1500 COMMENT '积分/段位', status TINYINT DEFAULT 1 COMMENT '状态:0禁用 1启用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );

这里有几个设计考量值得单独说一下。

第一,user_id必须单独建索引,因为所有查询都是"我的Bot列表",不带用户维度的查询在真实业务里几乎没有意义。如果没有这个索引,用户量上来之后列表查询会越来越慢。

第二,code用TEXT而不是VARCHAR,因为Bot代码动辄几百行,VARCHAR(255)肯定不够。用MyBatis-Plus时,TEXT字段在Java里对应String不会有问题,但要注意查询时不要默认把code字段全部查出来——列表页根本不需要代码内容,只在编辑时才需要,所以列表查询SQL里我手动排除了code字段,性能会好很多。

第三,rating字段是给匹配系统用的,初始值设为1500。这个数字我是从ELO评分体系里借过来的思路——新Bot从中间分起步,赢了对战加分,输了减分,这样匹配时就可以拿rating做撮合依据,保证新老Bot都有合适的对手。

对应的Java实体代码:

@TableName("bot") @Data public class Bot { @TableId(type = IdType.AUTO) private Integer id; private Integer userId; private String name; private String description; private String code; private Integer rating; private Integer status; private Date createTime; private Date updateTime; }

2.2 后端接口:RESTful API与权限控制

后端接口我按REST风格设计,一共五个:

方法路径说明
GET/api/bot/list分页查询当前用户的Bot
POST/api/bot/create新增Bot
PUT/api/bot/update修改Bot
DELETE/api/bot/delete/{id}删除Bot
POST/api/bot/change-status启用/禁用Bot

先说权限控制。因为涉及用户私有数据,所有接口都要从Token里解析出当前用户ID,而不是信任前端传过来的userId。我写了一个工具方法从JWT里拿userId,然后在service层强制设置:

@Override public Bot create(Bot bot) { Integer userId = UserContext.getUserId(); // 从当前登录态获取 bot.setUserId(userId); bot.setRating(1500); bot.setStatus(1); bot.setCreateTime(new Date()); bot.setUpdateTime(new Date()); botMapper.insert(bot); return bot; }

**这一点非常重要:如果直接把前端传的userId写进库,别人抓包改个参数就能往你名下塞数据,属于典型的越权漏洞。**我在项目里反复强调"后端永远不信任前端传的用户ID"。

再说更新操作。更新前必须先查一次,确认这条Bot确实属于当前用户,不然就是水平越权:

@Override public Bot update(Bot bot) { Bot old = botMapper.selectById(bot.getId()); if (old == null || !old.getUserId().equals(UserContext.getUserId())) { throw new RuntimeException("Bot不存在或无权操作"); } // 只更新允许修改的字段:name、description、code old.setName(bot.getName()); old.setDescription(bot.getDescription()); old.setCode(bot.getCode()); old.setUpdateTime(new Date()); botMapper.updateById(old); return old; }

有个小细节:更新时不要直接updateById(bot),因为前端可能只传了部分字段,直接把整个对象覆盖会导致rating、status等字段被意外清空。正确做法是先查出来再逐字段赋值,只更新允许用户改的字段。这个坑我自己踩过,线上用户报告"我的Bot积分变0了",查了半天就是更新时把rating覆盖掉了。

分页列表接口用MyBatis-Plus的分页插件实现,查询条件是user_id = 当前用户,可选加上name like搜索关键字,排序按update_time desc让用户最近改过的Bot排前面。分页参数用Page对象接收,返回给前端时带上total和records,前端拿这些数据渲染表格和页码。

2.3 前端页面:表格、表单、弹窗与校验

前端我用Vue 3 + Element Plus。整个页面分三个部分:搜索栏加操作按钮、表格、编辑弹窗。

搜索栏很简单,一个输入框加查询/重置按钮。表格列包括:ID、名称、描述、积分、状态、创建时间、更新时间、操作。操作列放"编辑""启用/禁用""删除"三个按钮。

新增和编辑共用一个弹窗表单,通过一个isEdit标志区分。表单字段就三个:name(必填,最长100字)、description(选填)、code(必填,用文本域,我给了一个500行的高度)。提交前做两个校验:名称不能为空;代码不能为空且不能超过一定长度。这些校验用Element Plus的rules配置就行。

这里分享一个我踩过的坑:**Bot代码是大文本,如果直接在表格里展示会撑爆布局。**我的做法是表格里只显示名称和描述,代码只在编辑弹窗里展示,并且用v-model绑定到textarea上。另外一个坑是删除操作一定要加二次确认弹窗,不然用户误点一下Bot就没了,找回的成本很高。

状态切换按钮是我后来加的。用户写完Bot后不一定会立刻投入使用,可能还在调试。所以状态字段设计成0/1两个值,前端按钮根据status动态显示"启用"或"禁用",点击后调/api/bot/change-status接口,后端判断当前状态做翻转。这个操作要"查出来再翻转写回",避免并发点击导致状态不一致。

3. 匹配系统准备:配对池、Bot状态与WebSocket的引入

3.1 匹配系统的核心需求

先把匹配系统的需求说清楚。用户启用了Bot之后,Bot不能立刻对战,而是要等系统给它"找对手"。这个找对手的过程就是匹配系统要解决的。

我定的需求很简单:一个启用的Bot提交匹配请求后进入等待队列;匹配系统持续扫描队列,把rating接近的两个Bot配对;配对成功后通知双方"匹配成功",随后进入对战流程。

这个需求里有两个点值得展开:什么样的Bot能进匹配池?如何保证匹配公平?

第一个问题:只有status=1(启用状态)的Bot才能进匹配池。这一点听起来是废话,但实现时容易漏。如果你在入池时不检查状态,用户禁用了Bot但Bot还在池子里等着被匹配,就会出现"幽灵Bot"。我处理的方式是:入池前查一次状态,同时提供"Bot状态变更时立即从匹配池移除"的逻辑。用户禁用Bot的那一刻,匹配池要同步把它踢出去,不能再参与撮合。

第二个问题:匹配公平性。最简单也最有效的方案就是基于rating的区间匹配。新Bot默认1500分,赢了对战加30分,输了减30分(K值取30,这是ELO体系里的常用参数)。匹配时先从队列里找出rating差值最小的两个Bot,如果差值在允许范围内(我设的是±100分,等待时间超过60秒后放宽到±200分),就配对成功。这个"等待越久、区间越宽"的策略是很多对战平台的通用做法,能避免玩家长时间匹配不到对手。

3.2 匹配池的代码实现

第一版匹配池我用的是ConcurrentLinkedQueue<Bot>,配合Spring的@Scheduled定时任务。为什么不用ArrayList?因为匹配池会同时被多个用户提交请求、被多个线程扫描配对,ConcurrentLinkedQueue是线程安全的,而且入队出队性能很好。核心逻辑大致是这样:

@Component public class MatchPool { private final ConcurrentLinkedQueue<Bot> queue = new ConcurrentLinkedQueue<>(); private final Map<Integer, Long> enterTimeMap = new ConcurrentHashMap<>(); // Bot进入匹配池 public synchronized boolean enter(Bot bot) { if (bot.getStatus() != 1) { return false; } if (queue.stream().anyMatch(b -> b.getId().equals(bot.getId()))) { return false; // 幂等判断,防止重复入队 } enterTimeMap.put(bot.getId(), System.currentTimeMillis()); return queue.offer(bot); } // Bot退出匹配池(禁用/删除/断开连接时调用) public void remove(Integer botId) { queue.removeIf(bot -> bot.getId().equals(botId)); enterTimeMap.remove(botId); } // 获取等待时长 public long waitTime(Integer botId) { Long enterTime = enterTimeMap.get(botId); return enterTime == null ? 0 : (System.currentTimeMillis() - enterTime) / 1000; } }

真正的匹配逻辑放在定时任务里,每3秒跑一次:

@Scheduled(fixedRate = 3000) public void doMatch() { List<Bot> bots = new ArrayList<>(queue); if (bots.size() < 2) return; // 按rating排序 bots.sort(Comparator.comparingInt(Bot::getRating)); // 找相邻rating差最小的一对 for (int i = 0; i < bots.size() - 1; i++) { Bot a = bots.get(i); Bot b = bots.get(i + 1); int diff = Math.abs(a.getRating() - b.getRating()); int limit = (matchPool.waitTime(a.getId()) > 60) ? 200 : 100; if (diff <= limit) { matchPool.remove(a.getId()); matchPool.remove(b.getId()); // 通知双方WebSocket客户端 notifyMatchSuccess(a, b); break; } } }

有两个细节值得注意。第一,doMatch里处理的是队列的快照副本,真正移除元素必须调用MatchPool的remove方法操作原队列,否则会出现并发修改问题。第二,waitTime超过60秒就放宽区间到±200,这个策略看起来简单,实际效果很好。我测试的时候发现如果不放宽区间,低分Bot在高分池里可能永远等不到对手,用户会一直在"匹配中"的状态里干等。

3.3 为什么匹配系统必须用WebSocket

现在问题来了:匹配成功的通知、对战时段的实时数据,应该怎么推给前端?

如果不用WebSocket,你有两个选择:前端轮询,或者SSE(Server-Sent Events)。轮询的问题是延迟和浪费——每3秒请求一次"我匹配到对手了吗",用户量大时对后端压力很大,而且匹配成功的瞬间用户最多要等3秒才能看到结果,体验很糟糕。SSE是单向的,服务端可以推送,但客户端不能通过同一条连接向服务端发消息,对战过程中如果需要频繁上报操作指令,SSE就不够用。

**WebSocket解决的是"全双工实时通信"的问题:客户端和服务端建立一条长连接,两边随时都能发消息。**在Bot对战场景里,匹配成功通知、对战状态推送、用户操作指令上报,全都需要双向实时交互,所以WebSocket是唯一合理的选择。

我接WebSocket时踩过一个认知上的坑:以为WebSocket必须引入STOMP、SockJS这些协议栈。其实对于"服务端向指定用户推送消息"这个需求,原生WebSocket完全够用。STOMP适合的是广播、订阅、群组这类复杂消息模型,引入它反而增加调试难度。

4. Spring Boot集成WebSocket实战

4.1 先搞懂WebSocket的基础概念

在写代码之前,先把WebSocket的协议模型搞清楚,不然后面调试会一头雾水。

WebSocket和HTTP的关系是:WebSocket连接通过HTTP的Upgrade握手来建立。客户端发一个普通的HTTP请求,带上Connection: Upgrade和Upgrade: websocket头,服务端如果同意,就返回101状态码,之后这条连接就从HTTP协议切换成WebSocket协议,双方开始全双工通信。

Spring Boot里的WebSocket支持,底层是把Spring的WebSocketHandler注册到Servlet容器(Tomcat)的WebSocket实现上。整个过程分三块:注册端点(Endpoint)、编写处理器(Handler)、可选地配置握手拦截器(HandshakeInterceptor)。

需要注意,Spring Boot 2.x和3.x在依赖坐标上略有差异,我用的是Spring Boot 2.7,依赖是:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>

4.2 注册端点与配置类

先写配置类,核心是实现WebSocketConfigurer接口,在registerWebSocketHandlers方法里注册Handler和路径:

@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Resource private MatchWebSocketHandler matchWebSocketHandler; @Resource private AuthHandshakeInterceptor authHandshakeInterceptor; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(matchWebSocketHandler, "/ws/match") .addInterceptors(authHandshakeInterceptor) .setAllowedOrigins("*"); } }

这里几个配置的含义要说清楚:

  • /ws/match是端点路径,前端连接时URL就是ws://域名/ws/match。
  • addInterceptors注册握手拦截器,用于身份认证和附加参数。
  • setAllowedOrigins("*")允许跨域连接。**生产环境建议收窄到自己的前端域名,不然别人网站也能连你的WebSocket端口。**这一点我吃过亏,后面常见问题里细说。

如果项目中配了Spring Security,WebSocket握手请求默认会被拦截,需要在SecurityConfig里放行/ws/**路径。这个坑很隐蔽,很多人WebSocket连不上,查了半天发现是Security把握手请求挡了。

4.3 握手拦截器与身份认证

WebSocket连接建立后,HTTP的session和常规请求的RequestContext就失效了,你没法在Handler里简单通过@RequestAttribute拿用户信息。所以身份认证要在握手阶段完成。

我的方案是:前端连接时在URL上带token参数,握手拦截器在beforeHandshake方法里解析token、校验身份、把userId存到WebSocketSession的attributes中:

@Component public class AuthHandshakeInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) { // 从URL参数中取token String token = getTokenFromRequest(request); if (!StringUtils.hasText(token)) { return false; // 拒绝握手 } // 解析JWT,获取userId Integer userId = JwtUtil.parseToken(token); if (userId == null) { return false; } // 将userId放入session attributes,后续Handler里可取 attributes.put("userId", userId); return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { // 握手完成后的回调,一般用不上 } }

这里有两个点值得注意。第一,token放在URL参数里存在被日志记录的风险,更稳妥的做法是放在Sec-WebSocket-Protocol头里,但浏览器和Spring的兼容性处理比较麻烦,我权衡后选择了URL参数。第二,握手拦截器返回false时,客户端会直接收到连接失败的错误,前端要针对这种情况做提示,而不是无限重连。

在Handler里,通过session.getAttributes().get("userId")就可以拿到当前用户ID,然后维护一个ConcurrentHashMap<Integer, WebSocketSession>存放"userId -> session"的映射,后续推送就从这个Map里找目标session:

@Component public class MatchWebSocketHandler extends TextWebSocketHandler { // 在线用户连接映射 private final Map<Integer, WebSocketSession> sessions = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) { Integer userId = (Integer) session.getAttributes().get("userId"); sessions.put(userId, session); sendMessage(session, Message.of("connected", Map.of("msg", "连接成功"))); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { Integer userId = (Integer) session.getAttributes().get("userId"); String payload = message.getPayload(); // 更新活跃时间,用于心跳检测 session.getAttributes().put("lastActive", System.currentTimeMillis()); // 根据消息type字段分发处理 dispatch(userId, payload); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { Integer userId = (Integer) session.getAttributes().get("userId"); sessions.remove(userId); // 注意:用户断开连接时,要把他的Bot从匹配池移除 matchPool.removeByUserId(userId); } }

afterConnectionClosed里那句matchPool.removeByUserId(userId)特别重要。**用户可能匹配到一半关了浏览器,如果不清掉匹配池里的Bot,就会出现配对成功后对面根本不存在的情况。**这个是我被测试同学提了bug之后才补上的。

4.4 消息协议、yml配置与心跳机制

WebSocket不像HTTP那样有天然的请求-响应模型,所以消息协议必须自己定义。我的做法是统一JSON格式,带一个type字段:

{ "type": "match_success", "data": { "opponentId": 10001, "opponentName": "mybot_2024", "gameId": "G001" } }

客户端发往服务端的消息类型主要有match_request(请求匹配)、match_cancel(取消匹配)、heartbeat(心跳)。服务端发往客户端的消息类型有connected、match_success、match_timeout、pong。类型字段加上之后,前后端各自做一个switch分发就行,代码清晰也好排查。

yml配置这块,很多教程会忽略,但实际部署时必须配。我在application.yml里加了这些:

server: tomcat: websocket: # WebSocket消息缓冲区大小,默认8KB,传输较大消息时要调大 buffer-size: 16384 spring: mvc: async: # 异步请求超时时间 request-timeout: 120000

这里的buffer-size如果太小,客户端发一条大消息(比如超过8KB的代码段)会被直接拒绝或者截断。我实际测试时发现,传十几KB的Bot代码片段,默认缓冲不够用,必须调大。

然后是心跳机制,这是WebSocket实战中绕不开的一环。WebSocket连接看起来还连着,但中间网络可能已经断了(比如用户切换网络、NAT超时),如果不做心跳,服务端永远不知道连接已经死了,会出现"假死连接"。

我实现的方案是服务端主动检测:每30秒扫描一次所有session,超过90秒没有收到任何消息的连接直接关闭:

@Component public class WebSocketHeartbeatTask { @Resource private MatchWebSocketHandler handler; // 每30秒检查一次 @Scheduled(fixedRate = 30000) public void heartbeatCheck() { long now = System.currentTimeMillis(); for (WebSocketSession session : handler.getAllSessions()) { long lastActive = (long) session.getAttributes().getOrDefault("lastActive", now); if (now - lastActive > 90_000) { try { session.close(CloseStatus.GOING_AWAY); } catch (Exception e) { // 关闭异常忽略 } handler.removeSession(session); } } } }

同时,handleTextMessage和afterConnectionEstablished里都要更新lastActive时间戳。客户端的ping建议每10秒发一次,服务端收到任何消息都会刷新lastActive,这样即使客户端不发心跳,只要它还在正常收发消息,连接也不会被误杀。

有一种简化的心跳方案是"服务端定时发送ping帧,客户端返回pong帧",利用WebSocket协议自带的Ping/Pong帧。**但浏览器端的WebSocket API没有直接暴露ping/pong帧的控制能力,所以前端只能用业务层的JSON心跳消息来模拟。**这个区别很多人搞混,这里特别说明一下。

5. 常见问题与排查技巧实录

5.1 WebSocket连接失败的高频原因

我接WebSocket时遇到过的连接失败原因,整理成一个速查表,遇到问题直接对号入座:

现象可能原因解决办法
握手401/403Spring Security拦截了/ws/**SecurityConfig中放行WebSocket路径
连接立即关闭握手拦截器返回false,token解析失败检查前端URL参数是否携带了正确的token
跨域报错setAllowedOrigins没配或者配错开发环境用*,生产环境配死前端域名
连接成功后马上断开服务端Handler抛异常,未做全局异常捕获在Handler方法内try-catch并记录日志
Nginx下连不上Nginx没有配置WebSocket升级配置Upgrade和Connection头,见下文

Nginx这个坑值得展开说。如果项目用Nginx做反向代理,默认配置是不支持WebSocket的,必须增加两行:

location /ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; }

proxy_read_timeout也要注意,默认60秒,WebSocket长连接很容易超过这个时间被Nginx掐断,至少要改成3600秒。

5.2 心跳超时与"假死连接"的完整处理

假死连接是最容易忽视的问题。我一开始没有做心跳,结果发现一个奇怪的现象:客户端明明已经关了页面,服务端的afterConnectionClosed却没被触发,session一直挂在内存里,越积越多,最后内存OOM。

排查过程很有意思:用netstat查看连接状态,发现TCP层面连接其实是ESTABLISHED状态,但应用层已经收不到任何数据了。原因可能是客户端进程被强杀、网络中间设备(如路由器NAT表项)超时静默断开。TCP本身有keep-alive机制,但默认参数(Linux下默认2小时)太长了,等它生效,假死连接已经堆积了很多。

我最终的方案就是前面说的:服务端30秒扫一次,90秒没活动就强制关闭。同时客户端要配合重连机制——浏览器端的WebSocket断开后会自动触发onclose事件,前端在这个事件里做指数退避重连,比如第一次等1秒、第二次等2秒、第四次等5秒,最多等5秒。不能无限快速重连,不然服务端会被冲垮。

5.3 匹配系统的并发与重复配对问题

匹配系统还有一个并发问题值得分享:如果定时任务每3秒跑一次,而用户同时点击了多个"请求匹配"按钮,同一个Bot可能被重复加入匹配池两次。这样Bot既可能和自己配到一起,也会导致一个Bot同时出现在两个配对里。

处理办法有两个:第一,前端在用户点击匹配按钮后立刻禁用按钮,防止重复提交;第二,后端在enter方法里做幂等判断,如果Bot已经在队列里就拒绝再次入队。我之前代码里的synchronized关键字就是为了防止"检查是否存在 + 入队"这个复合操作在高并发下被两个线程同时通过。这是一个很典型的"并发下的复合操作"问题,比单步操作复杂一个量级。

排查重复配对问题时,建议在日志里加上配对过程的traceId。每对成功的配对输出:时间、两个Bot的ID和rating、是否放宽区间。有了这些日志,线上出问题才能快速回溯。

最后的体会

我个人在把这些模块全部落地之后有个很深的体会:CRUD、匹配、WebSocket这三块单独看都不算难,真正花时间的是它们之间的衔接——Bot状态变了要同步踢出匹配池,WebSocket断了要清理匹配队列,匹配成功了要通过WebSocket精确推送给对应的用户。每一个衔接点都是在真实联调中踩坑踩出来的。

最后再分享一个小技巧:调试WebSocket时,不要每次都靠前端页面,直接装一个WebSocket测试客户端工具来手动连接、手动发JSON消息。我在联调阶段几乎所有连接问题都是靠它快速复现的,比反复改前端代码高效得多。下一篇我会继续讲匹配成功后的对战房间管理,如果这篇对你有帮助,欢迎在评论区聊聊你遇到的WebSocket踩坑经历。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询