前言
在项目开发中,实时消息推送是高频需求,比如双屏联动、大屏监控、在线聊天、订单状态推送等场景。websocket 作为 html5 的核心特性,实现了浏览器与服务器的全双工双向通信,相比传统的轮询 / 长轮询方式,大幅降低服务端压力,提升实时性和用户体验。
本文以springboot 2.7.x(最稳定版本,零基础友好)为基础,手把手教大家从 0 到 1 集成 websocket,实现左屏 / 右屏双端实时消息互推功能。全程代码可直接复制使用,兼顾jar 包内嵌 tomcat和war 包外部 tomcat两种部署方式,解决部署冲突问题,同时完善异常处理、连接管理、心跳检测等生产级细节,小白跟着步骤走就能跑通。
本文核心优势
- 零基础友好:代码全复制、步骤全拆解,无复杂配置,新手直接用;
- 部署无坑:自动适配 jar/war 包部署,无需手动修改代码,避免容器冲突;
- 生产级健壮:完善的异常处理、失效连接清理、心跳检测,防止内存泄漏;
- 支持多端登录:同一用户多设备连接,所有端都能收到消息,避免 session 覆盖;
- 双测试方式:在线工具快速验证 + 自定义 html 页面,前端后端全打通;
- 配套全补全:统一响应类、启动类改造等缺失代码全部补全,无需额外找依赖。
一、环境准备(新手必看)
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;
}
}
关键优化点:
- 把原有的map<string, session>改为map<string, set>,支持同一用户多端登录,所有连接都能收到消息;
- 增加session有效性判断(session.isopen()),避免向失效连接发送消息;
- 完善的异常捕获,发送消息失败时自动移除失效 session,防止内存泄漏;
- 提供精准移除(单 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());
}
}
核心注意点:
- @serverendpoint(“/connect/{userid}”):定义 websocket 的服务端连接地址,前端通过ws://ip:port/connect/left连接左屏,ws://ip:port/connect/right连接右屏;
- @component:必须添加,否则 spring 无法扫描到该端点,配合配置类的serverendpointexporter完成注册;
- @onerror方法不支持@pathparam注解:原代码中该注解会导致运行时参数解析异常,直接通过 session 遍历移除即可;
- 增加心跳检测:客户端定时发送&,服务端回执&,避免因长时间无交互导致连接被防火墙 / 服务器断开。
五、测试接口(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();
}
}
接口说明:
- 左屏推右屏:get http://ip:port/websocket/right?message=测试消息
- 右屏推左屏:get http://ip:port/websocket/left?message=测试消息
- 响应结果:成功返回{“code”:200,“msg”:“操作成功”,“data”:null},失败返回{“code”:500,“msg”:“操作失败”,“data”:null}。
六、前端测试(两种方式)
6.1 在线 websocket 测试工具(快速验证)
推荐使用在线工具:websocket 在线测试,无需编写前端代码,直接测试连接和消息推送。在线测试网站 https://wstool.js.org/
测试步骤:
- 打开两个浏览器窗口,分别访问在线测试工具;
- 第一个窗口连接地址填ws://localhost:8080/connect/left,点击连接,提示 “连接成功”;
- 第二个窗口连接地址填ws://localhost:8080/connect/right,点击连接,提示 “连接成功”;
- 左屏窗口发送消息hello 右屏,右屏窗口会收到左屏推送:hello 右屏;
- 右屏窗口发送消息hello 左屏,左屏窗口会收到右屏推送:hello 左屏;
- 调用 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 默认)
- pom.xml中打包方式为jar:
<packaging>jar</packaging >
- 执行 maven 命令打包:mvn clean package -dskiptests;
- 运行 jar 包:java -jar xxx.jar;
- 核心原理:内嵌 tomcat 环境,websocketautowired返回true,创建serverendpointexporter,websocket 正常注册。
7.2 war 包部署(外部 tomcat)
- 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>
- 修改启动类,继承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);
}
}
- 执行 maven 命令打包:mvn clean package -dskiptests;
- 将 war 包放入外部 tomcat 的webapps目录,启动 tomcat 即可;
- 核心原理:外部 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双屏消息互推的资料请关注代码网其它相关文章!
发表评论