java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot WebSocket双屏消息互推

SpringBoot集成WebSocket实现双屏实时消息互推功能

作者:qq_29757467

在项目开发中,实时消息推送是高频需求,比如双屏联动、大屏监控、在线聊天、订单状态推送等场景,本文以SpringBoot 2.7.x(最稳定版本,零基础友好)为基础,手把手教大家从 0 到 1 集成 WebSocket,实现左屏 / 右屏双端实时消息互推功能,需要的朋友可以参考下

前言

在项目开发中,实时消息推送是高频需求,比如双屏联动、大屏监控、在线聊天、订单状态推送等场景。WebSocket 作为 HTML5 的核心特性,实现了浏览器与服务器的全双工双向通信,相比传统的轮询 / 长轮询方式,大幅降低服务端压力,提升实时性和用户体验。

本文以SpringBoot 2.7.x(最稳定版本,零基础友好)为基础,手把手教大家从 0 到 1 集成 WebSocket,实现左屏 / 右屏双端实时消息互推功能。全程代码可直接复制使用,兼顾Jar 包内嵌 Tomcat和War 包外部 Tomcat两种部署方式,解决部署冲突问题,同时完善异常处理、连接管理、心跳检测等生产级细节,小白跟着步骤走就能跑通。

本文核心优势

  1. 零基础友好:代码全复制、步骤全拆解,无复杂配置,新手直接用;
  2. 部署无坑:自动适配 Jar/War 包部署,无需手动修改代码,避免容器冲突;
  3. 生产级健壮:完善的异常处理、失效连接清理、心跳检测,防止内存泄漏;
  4. 支持多端登录:同一用户多设备连接,所有端都能收到消息,避免 Session 覆盖;
  5. 双测试方式:在线工具快速验证 + 自定义 HTML 页面,前端后端全打通;
  6. 配套全补全:统一响应类、启动类改造等缺失代码全部补全,无需额外找依赖。

一、环境准备(新手必看)

1.1 基础开发环境

无需高版本,基础环境即可运行,推荐搭配:

1.2 核心依赖

在pom.xml中引入 SpringBoot 官方的 WebSocket Starter 依赖,无需额外引入其他包,Spring 已做封装:

<!-- SpringBoot集成WebSocket核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 可选:SpringMVC基础依赖(项目已引入可忽略) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

二、核心配置类(解决 Jar/War 部署兼容)

SpringBoot 中使用@ServerEndpoint注解实现 WebSocket 时,必须注册ServerEndpointExporter 让 Spring 扫描并管理 WebSocket 端点,但内嵌 Tomcat(Jar 包)和外部 Tomcat(War 包) 对该 Bean 的要求不同:
Jar 包部署(内嵌 Tomcat):需要手动创建ServerEndpointExporter Bean;
War 包部署(外部 Tomcat):由容器自身初始化 WebSocket,手动创建会导致 Bean 冲突。
因此我们通过Spring 条件注解@Conditional 实现动态判断,自动适配两种部署方式。

2.1 自定义条件判断类

创建包com.tydt.framework.config,编写WebSocketAutoWired类,实现Condition接口,核心逻辑是判断是否为内嵌 Tomcat 环境:

/**
 * All rights reserved.
 */
package com.itl.framework.config;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ClassUtils;

/**
 * 类描述:WebSocket条件判断类,控制ServerEndpointExporter是否创建
 * jar包部署(内嵌Tomcat)返回true,war包部署(外部Tomcat)返回false
 * @author itl
 * @version 1.0
 * 
 * 修订历史:
 * 日期			修订者		修订描述
 * 2026-02-05	xxx		修复matches方法固定返回false问题,实现jar/war包部署动态判断
 */
public class WebSocketAutoWired implements Condition {

    /**
     * 核心判断方法:jar包部署(内嵌Tomcat)为true; war包部署(外部Tomcat)为false
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 判断类加载器中是否存在内嵌Tomcat核心类 → 存在=jar包部署,不存在=war包部署
        return ClassUtils.isPresent(
                "org.apache.catalina.startup.Tomcat",
                context.getClassLoader()
        );
    }
}

2.2 WebSocket 核心配置类

编写WebSocketConfig类,通过@Conditional关联上面的条件判断类,动态创建ServerEndpointExporter:

/**
 *
 * All rights reserved.
 */
package com.itl.framework.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * 类描述:WebSocket核心配置类
 * 动态创建ServerEndpointExporter,解决内嵌Tomcat/外部Tomcat部署兼容问题
 * @author itl
 * @version 1.0
 * 新增条件注解,适配内嵌/外部Tomcat
 */
@Configuration
public class WebSocketConfig {

    /**
     * 注册WebSocket端点处理器,仅内嵌Tomcat(jar包)时创建
     * 外部Tomcat(war包)由容器自身初始化,无需手动创建
     */
    @Bean
    @Conditional(WebSocketAutoWired.class)
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

核心原理:项目启动时,Spring 会根据WebSocketAutoWired的matches方法返回值,动态决定是否创建ServerEndpointExporter Bean,从根本上解决 Jar/War 部署的冲突问题。

三、WebSocket 工具类(连接管理 + 消息发送)

创建工具类WebSocketUtils,用于统一管理客户端 Session 连接、发送消息、移除连接等操作,使用ConcurrentHashMap保证多线程下的线程安全,同时支持同一用户多端连接(避免 Session 被覆盖)。
包路径:com.itl.common.utils

/**
 * All rights reserved.
 */
package com.itl.common.utils;

import java.util.Map;
import java.util.Set;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.Session;

/**
 * 类描述:WebSocket工具类,管理客户端Session和消息发送
 * @author itl
 * 
 * 修订历史:
 * 日期			修订者		修订描述
 * 优化Session管理,支持单用户多连接;增加异常处理和Session有效性判断
 */
public class WebSocketUtils {

    // 存储客户端连接:key=用户ID,value=该用户的所有Session连接(支持多端登录)
    public static Map<String, Set<Session>> clients = new ConcurrentHashMap<>();

    /**
     * 添加客户端连接
     * @param userId 用户唯一标识
     * @param session 客户端会话
     */
    public static void add(String userId, Session session) {
        // 不存在则创建新的Set,存在则直接添加;ConcurrentHashMap.newKeySet()保证线程安全
        clients.computeIfAbsent(userId, k -> ConcurrentHashMap.newKeySet()).add(session);
    }

    /**
     * 处理客户端发送的消息(可根据业务自定义)
     * @param userId 发送消息的用户ID
     * @param message 消息内容
     */
    public static void receive(String userId, String message) {
        // 示例:双屏联动,左屏消息推右屏,右屏消息推左屏
        if ("left".equals(userId)) {
            sendMessage("right", "左屏推送:" + message);
        } else if ("right".equals(userId)) {
            sendMessage("left", "右屏推送:" + message);
        }
        System.out.println("收到用户[" + userId + "]的消息:" + message);
    }

    /**
     * 精准移除某用户的某一个Session连接(连接关闭/异常时调用)
     * @param userId 用户唯一标识
     * @param session 要移除的会话
     */
    public static void remove(String userId, Session session) {
        Set<Session> sessions = clients.get(userId);
        if (sessions != null) {
            sessions.remove(session);
            // 若该用户无任何连接,移除key,避免空集合占用内存
            if (sessions.isEmpty()) {
                clients.remove(userId);
            }
        }
    }

    /**
     * 移除某用户的所有连接
     * @param userId 用户唯一标识
     */
    public static void remove(String userId) {
        clients.remove(userId);
    }

    /**
     * 向指定用户发送消息
     * @param userId 接收消息的用户ID
     * @param message 消息内容
     * @return 成功发送的连接数
     */
    public static int sendMessage(String userId, String message) {
        Set<Session> sessions = clients.get(userId);
        // 无该用户连接,直接返回0
        if (sessions == null || sessions.isEmpty()) {
            return 0;
        }
        int successCount = 0;
        Iterator<Session> it = sessions.iterator();
        while (it.hasNext()) {
            Session session = it.next();
            // 判断Session是否有效(连接未关闭)
            if (!session.isOpen()) {
                it.remove(); // 移除失效Session,避免内存泄漏
                continue;
            }
            try {
                // 异步发送消息(推荐),同步发送使用session.getBasicRemote().sendText(message)
                session.getAsyncRemote().sendText(message);
                successCount++;
            } catch (Exception e) {
                it.remove(); // 发送失败,移除失效Session
                e.printStackTrace(); // 实际项目建议使用日志框架(如Logback/Log4j2)
            }
        }
        // 清理空集合
        if (sessions.isEmpty()) {
            clients.remove(userId);
        }
        return successCount;
    }
}

关键优化点:

  1. 把原有的Map<String, Session>改为Map<String, Set>,支持同一用户多端登录,所有连接都能收到消息;
  2. 增加Session有效性判断(session.isOpen()),避免向失效连接发送消息;
  3. 完善的异常捕获,发送消息失败时自动移除失效 Session,防止内存泄漏;
  4. 提供精准移除(单 Session)和批量移除(全 Session)两种方法,适配不同场景。

四、WebSocket 服务端端点(核心业务处理)

创建WebSocketService类,使用@ServerEndpoint注解定义 WebSocket 服务端地址,通过@OnOpen、@OnMessage、@OnClose、@OnError注解处理 WebSocket 的连接打开、接收消息、连接关闭、连接异常四大事件,同时通过@Component注解让 Spring 管理该 Bean。
包路径:com.itl.framework.web.service

/**
 * All rights reserved.
 */
package com.itl.framework.web.service;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import com.itl.common.utils.WebSocketUtils;

/**
 * 类描述:WebSocket服务端端点,处理客户端连接和事件回调
 * 服务端地址:/connect/{userId}
 * @author itl
 * 修复onError方法参数注解问题;优化连接管理,精准移除Session
 */
@ServerEndpoint("/connect/{userId}") // WebSocket连接地址,{userId}为用户唯一标识
@Component // 必须交给Spring管理,否则无法扫描
public class WebSocketService {

    /**
     * 连接打开事件(客户端首次连接时调用)
     * @param userId 路径参数中的用户ID
     * @param session 客户端会话
     */
    @OnOpen
    public void onOpen(@PathParam("userId") String userId, Session session) {
        System.out.println("【WebSocket】连接打开成功!");
        WebSocketUtils.add(userId, session);
        System.out.println("【WebSocket】用户" + userId + "上线,当前在线人数:" + WebSocketUtils.clients.size());
    }

    /**
     * 接收客户端消息事件
     * @param userId 发送消息的用户ID
     * @param message 客户端发送的消息
     * @return 服务端向客户端的回执消息
     */
    @OnMessage
    public String onMessage(@PathParam("userId") String userId, String message) {
        // 心跳检测(可选),客户端发送&时,服务端回执&,避免连接被断开
        if (message.equals("&")) {
            return "&";
        } else {
            // 调用工具类处理消息
            WebSocketUtils.receive(userId, message);
            return "【服务端回执】已收到消息:" + message;
        }
    }

    /**
     * 连接异常事件(网络中断、客户端崩溃等)
     * 注意:@OnError注解不支持@PathParam参数,会导致参数解析异常
     * @param session 异常的客户端会话
     * @param throwable 异常信息
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        // 遍历移除该失效的Session
        WebSocketUtils.clients.forEach((userId, sessions) -> {
            WebSocketUtils.remove(userId, session);
        });
        throwable.printStackTrace();
        System.out.println("【WebSocket】连接异常,已移除失效会话");
    }

    /**
     * 连接关闭事件(客户端主动关闭连接)
     * @param userId 断开连接的用户ID
     * @param session 关闭的客户端会话
     */
    @OnClose
    public void onClose(@PathParam("userId") String userId, Session session) {
        System.out.println("【WebSocket】连接关闭成功!");
        WebSocketUtils.remove(userId, session);
        System.out.println("【WebSocket】用户" + userId + "下线,当前在线人数:" + WebSocketUtils.clients.size());
    }
}

核心注意点:

  1. @ServerEndpoint(“/connect/{userId}”):定义 WebSocket 的服务端连接地址,前端通过ws://ip:port/connect/left连接左屏,ws://ip:port/connect/right连接右屏;
  2. @Component:必须添加,否则 Spring 无法扫描到该端点,配合配置类的ServerEndpointExporter完成注册;
  3. @OnError方法不支持@PathParam注解:原代码中该注解会导致运行时参数解析异常,直接通过 Session 遍历移除即可;
  4. 增加心跳检测:客户端定时发送&,服务端回执&,避免因长时间无交互导致连接被防火墙 / 服务器断开。

五、测试接口(HTTP 触发 WebSocket 消息推送)

创建 Controller,提供 HTTP 接口,用于通过后端接口触发 WebSocket 消息推送(比如业务系统调用接口向前端推送消息),实现左屏 / 右屏双端消息互推,同时使用AjaxResult返回统一的响应结果(SpringBoot 项目通用)。

import com.itl.common.utils.WebSocketUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * WebSocket测试控制器,双屏消息互推接口
 * @author itl
 * @date 2026-02-05
 */
@RestController
@RequestMapping("/websocket")
@Api(tags = "WebSocket测试接口")
public class WebSocketController {

    /**
     * 接收左屏消息并推送至右屏
     * @param message 消息内容
     * @return 推送结果(1=成功,0=失败)
     */
    @ApiOperation(value = "左屏推右屏", notes = "HTTP接口触发,向右屏推送消息")
    @ApiImplicitParam(name = "message", value = "推送的消息内容", required = true, dataType = "String")
    @GetMapping(value = "/right")
    public AjaxResult right(String message) {
        // toAjax:通用工具类,1=成功,0=失败
        return toAjax(WebSocketUtils.sendMessage("right", message));
    }

    /**
     * 接收右屏消息并推送至左屏
     * @param message 消息内容
     * @return 推送结果(1=成功,0=失败)
     */
    @ApiOperation(value = "右屏推左屏", notes = "HTTP接口触发,向左屏推送消息")
    @ApiImplicitParam(name = "message", value = "推送的消息内容", required = true, dataType = "String")
    @GetMapping(value = "/left")
    public AjaxResult left(String message) {
        return toAjax(WebSocketUtils.sendMessage("left", message));
    }

    /**
     * 通用响应结果封装(项目已实现可忽略)
     * @param rows 成功数
     * @return AjaxResult
     */
    private AjaxResult toAjax(int rows) {
        return rows > 0 ? AjaxResult.success() : AjaxResult.error();
    }
}

接口说明:

  1. 左屏推右屏:GET http://ip:port/websocket/right?message=测试消息
  2. 右屏推左屏:GET http://ip:port/websocket/left?message=测试消息
  3. 响应结果:成功返回{“code”:200,“msg”:“操作成功”,“data”:null},失败返回{“code”:500,“msg”:“操作失败”,“data”:null}。

六、前端测试(两种方式)

6.1 在线 WebSocket 测试工具(快速验证)

推荐使用在线工具:WebSocket 在线测试,无需编写前端代码,直接测试连接和消息推送。在线测试网站 https://wstool.js.org/
测试步骤:

  1. 打开两个浏览器窗口,分别访问在线测试工具;
  2. 第一个窗口连接地址填ws://localhost:8080/connect/left,点击连接,提示 “连接成功”;
  3. 第二个窗口连接地址填ws://localhost:8080/connect/right,点击连接,提示 “连接成功”;
  4. 左屏窗口发送消息Hello 右屏,右屏窗口会收到左屏推送:Hello 右屏;
  5. 右屏窗口发送消息Hello 左屏,左屏窗口会收到右屏推送:Hello 左屏;
  6. 调用 HTTP 接口http://localhost:8080/websocket/right?message=接口推右屏,右屏窗口会收到该消息。

6.2 自定义 HTML 测试页面(项目使用)

编写简单的 HTML 页面,通过原生 WebSocket API 实现连接和消息收发,可直接放入项目的resources/static目录下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>WebSocket双屏测试</title>
</head>
<body>
    <h3>WebSocket双屏联动测试(<span id="screenType">左屏</span>)</h3>
    <input type="text" id="msgInput" placeholder="请输入消息内容">
    <button onclick="sendMsg()">发送消息</button>
    <div id="msgList" style="margin-top: 20px; width: 500px; height: 300px; border: 1px solid #ccc; padding: 10px; overflow-y: auto;"></div>

    <script>
        // 定义用户ID,left=左屏,right=右屏
        const userId = "left";
        document.getElementById("screenType").innerText = userId === "left" ? "左屏" : "右屏";
        // WebSocket连接地址,替换为自己的服务端地址
        const ws = new WebSocket("ws://localhost:8080/connect/" + userId);

        // 连接成功回调
        ws.onopen = function() {
            addMsg("【系统提示】WebSocket连接成功!");
        };

        // 接收消息回调
        ws.onmessage = function(event) {
            addMsg("【收到消息】" + event.data);
        };

        // 连接关闭回调
        ws.onclose = function() {
            addMsg("【系统提示】WebSocket连接关闭!");
        };

        // 连接异常回调
        ws.onerror = function() {
            addMsg("【系统提示】WebSocket连接异常!");
        };

        // 发送消息
        function sendMsg() {
            const msg = document.getElementById("msgInput").value;
            if (!msg) {
                alert("请输入消息内容!");
                return;
            }
            ws.send(msg);
            addMsg("【发送消息】" + msg);
            document.getElementById("msgInput").value = "";
        }

        // 追加消息到页面
        function addMsg(content) {
            const msgList = document.getElementById("msgList");
            const div = document.createElement("div");
            div.style.margin = "5px 0";
            div.innerText = new Date().toLocaleString() + " - " + content;
            msgList.appendChild(div);
            // 滚动到底部
            msgList.scrollTop = msgList.scrollHeight;
        }

        // 心跳检测,每30秒发送一次&,防止连接断开
        setInterval(() => {
            ws.send("&");
        }, 30000);
    </script>
</body>
</html>

使用说明:

复制两份页面,分别修改userId为left和right,命名为left.html和right.html;
启动项目后,访问http://localhost:8080/left.html和http://localhost:8080/right.html;
两个页面可互相发送消息,同时支持后端接口推送。

七、部署方式说明

本文的配置已完美适配Jar 包内嵌 Tomcat和War 包外部 Tomcat两种部署方式,无需修改任何代码。

7.1 Jar 包部署(推荐,SpringBoot 默认)

  1. pom.xml中打包方式为jar:
<packaging>jar</packaging >
  1. 执行 Maven 命令打包:mvn clean package -DskipTests;
  2. 运行 Jar 包:java -jar xxx.jar;
  3. 核心原理:内嵌 Tomcat 环境,WebSocketAutoWired返回true,创建ServerEndpointExporter,WebSocket 正常注册。

7.2 War 包部署(外部 Tomcat)

  1. pom.xml中修改打包方式为war,并排除内嵌 Tomcat:
<packaging>war</packaging>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <!-- 排除内嵌Tomcat -->
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- 引入servlet-api依赖 -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>
  1. 修改启动类,继承SpringBootServletInitializer,重写configure方法:
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  1. 执行 Maven 命令打包:mvn clean package -DskipTests;
  2. 将 war 包放入外部 Tomcat 的webapps目录,启动 Tomcat 即可;
  3. 核心原理:外部 Tomcat 环境,WebSocketAutoWired返回false,不创建ServerEndpointExporter,由 Tomcat 容器自身初始化 WebSocket,避免冲突。

八、常见问题及解决方案

8.1 客户端连接报 404 错误

原因:未创建ServerEndpointExporter Bean,Spring 未扫描到@ServerEndpoint注解;
解决方案:检查配置类WebSocketConfig和条件判断类WebSocketAutoWired是否正确,Jar 包部署时确保matches方法返回true。

8.2 War 包部署到外部 Tomcat 启动报 Bean 冲突

原因:外部 Tomcat 环境下创建了ServerEndpointExporter Bean,与容器自身的 WebSocket 初始化冲突;
解决方案:确保条件判断类WebSocketAutoWired在 War 包部署时返回false,不创建该 Bean。

8.3 发送消息时报 IO 异常

原因:向失效的 Session(连接已关闭 / 网络中断)发送消息,或未做异常捕获;
解决方案:在sendMessage方法中增加session.isOpen()判断,同时捕获异常并移除失效 Session(本文工具类已实现)。

8.4 同一用户多端登录,只有最后一个连接能收到消息

原因:原代码使用Map<String, Session>存储连接,新连接会覆盖旧连接;
解决方案:改为Map<String, Set>存储,同一用户的所有连接都加入 Set(本文工具类已实现)。

8.5 长时间无交互,连接被断开

原因:防火墙 / 服务器会断开长时间无数据交互的 TCP 连接;
解决方案:实现心跳检测,客户端定时发送心跳包(如&),服务端回执,保持连接活跃(本文代码已实现)。

九、总结

本文详细讲解了 SpringBoot 集成 WebSocket 的全流程,从核心依赖引入→配置类编写(解决 Jar/War 兼容)→工具类封装(连接管理 + 消息发送)→服务端端点实现(事件处理)→测试接口开发→前端测试,一步一步实现了双屏实时消息互推的功能,同时解决了项目开发和部署中的常见问题。
本文的代码具有以下特点:

WebSocket 的应用场景非常广泛,除了双屏联动,还可以用于在线聊天、实时监控、订单推送、弹幕等场景,只需在本文代码的基础上,根据业务需求修改WebSocketUtils的receive方法和消息发送逻辑即可。

以上就是SpringBoot集成WebSocket实现双屏实时消息互推功能的详细内容,更多关于SpringBoot WebSocket双屏消息互推的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文