Spring Boot+Thymeleaf多用户页面自动刷新/棋步实时同步方案咨询
实现国际象棋游戏多用户实时棋步同步方案
我开发的小型国际象棋游戏,当前移动棋子后,其他用户必须手动刷新页面才能看到棋盘变化。希望实现多用户页面自动刷新或直接同步棋步的功能,在调用service.move()后需要触发实时刷新效果。我是首次使用Spring Boot和HttpServlet,不清楚可行的实现方案。
我的WebController代码如下:
package com.example.handandbrainchess; import com.example.handandbrainchess.Service.ApplicationService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @Controller public class WebController { private final ApplicationService service; private Integer moveRow1; private Integer moveRow2; private Integer moveColumn1; private Integer moveColumn2; public WebController(ApplicationService service) { this.service = service; } @GetMapping("/") public String index(Model m){ m.addAttribute("Feld",service.getFeld()); return "start"; } @GetMapping("/move") public String move(Model m, @RequestParam("idrow") int idrow, @RequestParam("idcolumn") int idcolumn){ // System.out.println("Reihe: " + idrow + " Spalte: "+ idcolumn); // m.addAttribute("Feld",service.getFeld()); if(moveRow1 == null){ moveRow1 = idrow; } else if(moveRow2 == null){ moveRow2 = idrow; } if(moveColumn1 == null){ moveColumn1 = idcolumn; }else if(moveColumn2 == null){ moveColumn2 = idcolumn; } if(moveRow2 != null && moveColumn2 != null){ service.move(moveRow1,moveColumn1,moveRow2,moveColumn2); //after this refresh moveRow1 = null; moveRow2 = null; moveColumn1 = null; moveColumn2= null; } return "redirect:/"; } @PostMapping("/") public String init(){ service.init(); return "redirect:/"; } }
可行解决方案
1. WebSocket(推荐,实时双向通信)
这是实现实时同步最理想的方案,支持服务器主动向客户端推送消息,适合需要双向交互的场景。
步骤1:添加WebSocket依赖
Maven项目在pom.xml中加入:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
步骤2:配置WebSocket端点
创建WebSocket配置类:
package com.example.handandbrainchess.config; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); // 消息代理前缀,用于推送消息给客户端 config.setApplicationDestinationPrefixes("/app"); // 客户端发送消息的前缀 } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/chess-websocket").withSockJS(); // 支持SockJS降级,兼容老旧浏览器 } }
步骤3:修改WebController,发送同步消息
注入WebSocket消息模板,在棋步完成后推送最新棋盘状态:
import org.springframework.messaging.simp.SimpMessagingTemplate; @Controller public class WebController { private final ApplicationService service; private final SimpMessagingTemplate messagingTemplate; private Integer moveRow1; private Integer moveRow2; private Integer moveColumn1; private Integer moveColumn2; // 构造方法注入SimpMessagingTemplate public WebController(ApplicationService service, SimpMessagingTemplate messagingTemplate) { this.service = service; this.messagingTemplate = messagingTemplate; } // ... 原有代码不变 ... @GetMapping("/move") public String move(Model m, @RequestParam("idrow") int idrow, @RequestParam("idcolumn") int idcolumn){ // ... 原有选择棋子逻辑不变 ... if(moveRow2 != null && moveColumn2 != null){ service.move(moveRow1,moveColumn1,moveRow2,moveColumn2); // 向所有订阅客户端推送最新棋盘 messagingTemplate.convertAndSend("/topic/chess-update", service.getFeld()); // 重置选择参数 moveRow1 = null; moveRow2 = null; moveColumn1 = null; moveColumn2= null; } return "redirect:/"; } // ... 其他代码不变 ... }
步骤4:前端页面添加WebSocket监听
在start.html中引入Stomp和SockJS,编写监听逻辑:
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script> <script> var stompClient = null; function connect() { var socket = new SockJS('/chess-websocket'); stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { // 订阅棋盘更新消息 stompClient.subscribe('/topic/chess-update', function(chessUpdate) { var newFeld = JSON.parse(chessUpdate.body); updateChessBoard(newFeld); }); }); } function updateChessBoard(feld) { // 自定义棋盘更新逻辑,比如遍历数组修改对应格子的棋子 for (let row = 0; row < feld.length; row++) { for (let col = 0; col < feld[row].length; col++) { const cell = document.getElementById(`${row}-${col}`); cell.textContent = feld[row][col]; // 假设棋子用字符表示 } } } // 页面加载时建立连接 window.onload = connect; </script>
2. Server-Sent Events(SSE,轻量单向推送)
如果只需要服务器向客户端推送数据,SSE是更轻量的选择,无需双向通信支持。
步骤1:创建SSE端点
在WebController中添加SSE连接管理和推送逻辑:
import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; import java.util.ArrayList; import java.util.List; @Controller public class WebController { // 保存所有活跃的SSE连接 private final List<SseEmitter> emitters = new ArrayList<>(); // ... 其他注入和代码不变 ... @GetMapping(value = "/chess-sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter sse() { SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); emitters.add(emitter); // 连接关闭/超时后移除 emitter.onCompletion(() -> emitters.remove(emitter)); emitter.onTimeout(() -> emitters.remove(emitter)); return emitter; } // 修改move方法推送更新 @GetMapping("/move") public String move(Model m, @RequestParam("idrow") int idrow, @RequestParam("idcolumn") int idcolumn){ // ... 原有逻辑不变 ... if(moveRow2 != null && moveColumn2 != null){ service.move(moveRow1,moveColumn1,moveRow2,moveColumn2); // 向所有SSE连接推送最新棋盘 for (SseEmitter emitter : emitters) { try { emitter.send(service.getFeld()); } catch (IOException e) { emitter.completeWithError(e); emitters.remove(emitter); } } // 重置参数 moveRow1 = null; moveRow2 = null; moveColumn1 = null; moveColumn2= null; } return "redirect:/"; } }
步骤2:前端监听SSE
在start.html中添加监听:
<script> const eventSource = new EventSource('/chess-sse'); eventSource.onmessage = function(event) { const newFeld = JSON.parse(event.data); updateChessBoard(newFeld); // 复用之前的updateChessBoard函数 }; function updateChessBoard(feld) { // 自定义棋盘更新逻辑 } </script>
3. 定时轮询(最简单,快速上手)
如果不想引入新的技术栈,定时轮询是最低成本的实现方式,缺点是实时性较差。
步骤1:前端添加定时请求
在start.html中设置定时请求:
<script> // 每2秒请求一次最新棋盘 setInterval(() => { fetch('/api/chess-board') .then(res => res.json()) .then(feld => updateChessBoard(feld)); }, 2000); function updateChessBoard(feld) { // 自定义棋盘更新逻辑 } </script>
步骤2:新增棋盘状态API
创建RestController提供棋盘数据接口:
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api") public class ChessApiController { private final ApplicationService service; public ChessApiController(ApplicationService service) { this.service = service; } @GetMapping("/chess-board") public Object getChessBoard() { return service.getFeld(); } }
方案选择建议
- 追求最佳实时体验:选WebSocket,支持双向交互,后续扩展功能更灵活。
- 轻量单向推送:选SSE,代码实现简单,无需处理双向通信逻辑。
- 快速测试验证:选定时轮询,无需引入新依赖,开发成本最低。
内容的提问来源于stack exchange,提问作者DomePB
相关产品推荐
相关产品推荐

