You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 10:04:54