别再只写服务端了!Spring Boot WebSocket 完整双端通信与自动重连保姆级教程
从零构建Spring Boot WebSocket双端通信:在线协作白板实战与自动重连机制
想象一下这样的场景:你和团队成员正在远程协作设计一个产品原型,每个人都能实时看到其他人的修改,光标移动、图形绘制同步呈现,就像在同一块物理白板前工作。这种实时协作体验的核心,正是基于WebSocket的双向通信技术。本文将带你从零开始,用Spring Boot构建一个完整的在线协作白板系统,重点解决实际开发中最棘手的自动重连问题。
1. 项目初始化与环境搭建
1.1 创建Spring Boot项目
首先使用Spring Initializr创建项目,选择以下依赖:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>对于客户端项目,额外添加Java-WebSocket客户端库:
<dependency> <groupId>org.java-websocket</groupId> <artifactId>Java-WebSocket</artifactId> <version>1.5.3</version> </dependency>1.2 基础配置
服务端需要启用WebSocket支持,在启动类添加注解:
@SpringBootApplication @EnableWebSocket public class CollaborativeWhiteboardApplication { public static void main(String[] args) { SpringApplication.run(CollaborativeWhiteboardApplication.class, args); } }创建WebSocket配置类注册端点:
@Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }2. 服务端实现:协作白板核心逻辑
2.1 WebSocket端点设计
我们设计一个白板端点,处理绘图指令和用户连接:
@ServerEndpoint("/whiteboard") @Component public class WhiteboardEndpoint { private static final Logger logger = LoggerFactory.getLogger(WhiteboardEndpoint.class); private Session session; private static Set<Session> sessions = Collections.synchronizedSet(new HashSet<>()); @OnOpen public void onOpen(Session session) { this.session = session; sessions.add(session); logger.info("新连接加入: {}", session.getId()); } @OnClose public void onClose() { sessions.remove(session); logger.info("连接关闭: {}", session.getId()); } @OnMessage public void onMessage(String message, Session session) { // 广播绘图指令给所有客户端 broadcast(message); } private void broadcast(String message) { sessions.forEach(s -> { try { s.getBasicRemote().sendText(message); } catch (IOException e) { logger.error("消息发送失败", e); } }); } }2.2 白板指令协议设计
定义简单的JSON协议表示绘图动作:
{ "type": "draw", "data": { "x1": 100, "y1": 200, "x2": 150, "y2": 250, "color": "#FF0000", "width": 2 } }3. 客户端实现:Spring Boot整合WebSocket
3.1 客户端连接管理
创建带自动重连的客户端管理器:
public class WhiteboardClient extends WebSocketClient { private static final Logger logger = LoggerFactory.getLogger(WhiteboardClient.class); private final AtomicBoolean shouldReconnect = new AtomicBoolean(true); private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); public WhiteboardClient(URI serverUri) { super(serverUri); } @Override public void onOpen(ServerHandshake handshakedata) { logger.info("连接已建立"); // 初始化白板UI initWhiteboardUI(); } @Override public void onMessage(String message) { // 处理服务端发来的绘图指令 handleDrawingInstruction(message); } @Override public void onClose(int code, String reason, boolean remote) { logger.warn("连接关闭: {}", reason); if (shouldReconnect.get()) { executor.schedule(this::reconnect, 3, TimeUnit.SECONDS); } } @Override public void onError(Exception ex) { logger.error("连接错误", ex); } private void reconnect() { try { logger.info("尝试重新连接..."); this.reconnectBlocking(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }3.2 与Spring Boot集成
将客户端作为Spring Bean管理:
@Component public class WhiteboardClientInitializer { private static final String WS_URI = "ws://localhost:8080/whiteboard"; @PostConstruct public void init() { try { WhiteboardClient client = new WhiteboardClient(new URI(WS_URI)); client.connect(); } catch (URISyntaxException e) { throw new RuntimeException("无效的WebSocket URI", e); } } }4. 高级功能:增强型自动重连机制
4.1 指数退避重连策略
改进重连逻辑,避免频繁重试:
private void scheduleReconnect() { long delay = (long) Math.min(30, Math.pow(2, reconnectAttempts.get())) * 1000; logger.info("将在{}秒后尝试重连", delay/1000); executor.schedule(this::doReconnect, delay, TimeUnit.MILLISECONDS); reconnectAttempts.incrementAndGet(); }4.2 心跳检测机制
添加心跳保持连接活跃:
private void startHeartbeat() { executor.scheduleAtFixedRate(() -> { if (isOpen()) { try { send("{\"type\":\"heartbeat\"}"); } catch (Exception e) { logger.warn("心跳发送失败", e); } } }, 0, 30, TimeUnit.SECONDS); }4.3 网络状态感知
监听网络变化触发重连:
private void setupNetworkListener() { Runtime.getRuntime().addShutdownHook(new Thread(() -> { shouldReconnect.set(false); executor.shutdown(); })); // 简单模拟网络状态检测 executor.scheduleAtFixedRate(() -> { if (!isOpen() && !isConnecting() && shouldReconnect.get()) { scheduleReconnect(); } }, 1, 1, TimeUnit.MINUTES); }5. 前端集成与完整协作体验
5.1 简单HTML5白板界面
<!DOCTYPE html> <html> <head> <title>协作白板</title> <style> #whiteboard { border: 1px solid #000; cursor: crosshair; } </style> </head> <body> <canvas id="whiteboard" width="800" height="600"></canvas> <script> const canvas = document.getElementById('whiteboard'); const ctx = canvas.getContext('2d'); const socket = new WebSocket('ws://localhost:8080/whiteboard'); // 绘图逻辑和WebSocket事件处理 </script> </body> </html>5.2 消息处理与同步
socket.onmessage = function(event) { const instruction = JSON.parse(event.data); if (instruction.type === 'draw') { const {x1, y1, x2, y2, color, width} = instruction.data; ctx.strokeStyle = color; ctx.lineWidth = width; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); } };6. 部署与性能考量
6.1 服务端配置优化
调整WebSocket相关参数:
# application.properties server.servlet.context-path=/api spring.websocket.max-text-message-buffer-size=512KB spring.websocket.max-binary-message-buffer-size=512KB6.2 负载均衡与集群
在多个实例间同步白板状态:
@Configuration @EnableScheduling public class WhiteboardSyncConfig { @Autowired private SimpMessagingTemplate messagingTemplate; @Scheduled(fixedRate = 5000) public void syncSessions() { messagingTemplate.convertAndSend("/topic/sessionSync", getActiveSessions()); } }6.3 监控与日志
添加WebSocket事件监听:
@Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); container.setMaxSessionIdleTimeout(600000L); container.setAsyncSendTimeout(5000L); return container; }在实际项目中,我们还需要考虑消息压缩、二进制传输优化等问题。对于大规模协作场景,可以采用分层广播策略,只将变更发送给相关用户而非全量广播。
