当前位置: 代码网 > it编程>编程语言>Java > SpringBoot集成WebSocket实现双屏实时消息互推功能

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

2026年02月12日 Java 我要评论
前言在项目开发中,实时消息推送是高频需求,比如双屏联动、大屏监控、在线聊天、订单状态推送等场景。websocket 作为 html5 的核心特性,实现了浏览器与服务器的全双工双向通信,相比传统的轮询

前言

在项目开发中,实时消息推送是高频需求,比如双屏联动、大屏监控、在线聊天、订单状态推送等场景。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 基础开发环境

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

  • jdk:1.8(兼容性最好,无版本问题)
  • springboot:2.7.10(本文统一版本,避免依赖冲突)
  • maven:3.6.0+
  • 开发工具:idea/eclipse(推荐 idea,自带 maven 管理)
  • 测试工具:浏览器、websocket 在线测试工具

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 实现动态判断,自动适配两种部署方式。

  • jar 包部署(内嵌 tomcat):需要手动创建serverendpointexporter bean;
  • war 包部署(外部 tomcat):由容器自身初始化 websocket,手动创建会导致 bean 冲突。

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 兼容)→工具类封装(连接管理 + 消息发送)→服务端端点实现(事件处理)→测试接口开发→前端测试,一步一步实现了双屏实时消息互推的功能,同时解决了项目开发和部署中的常见问题。
本文的代码具有以下特点:

  • 高可用性:完善的异常处理、session 有效性判断、失效连接清理,避免内存泄漏;
  • 高扩展性:工具类和服务端端点解耦,可根据业务需求自定义消息处理逻辑;
  • 高兼容性:支持 jar 包和 war 包两种部署方式,无需手动修改代码;
  • 线程安全:使用 concurrenthashmap 和 concurrenthashset 保证多线程下的连接管理安全。

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

以上就是springboot集成websocket实现双屏实时消息互推功能的详细内容,更多关于springboot websocket双屏消息互推的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com