发布时间:2026/9/8 7:27:26
SpringBoot+WebSocket实现实时消息推送实战指南 简介SpringBootWebSocket Demo是一套面向SpringBoot开发者的实时通信入门示例适用于聊天室、在线通知、协作编辑等需要服务端主动推送消息的场景。资源以可运行工程为基础演示如何引入spring-boot-starter-websocket依赖、实现WebSocketHandler处理器并借助SockJS搭建简易前端页面完成消息收发与群发广播。压缩包包含106个文件以xml配置Maven工程与构建描述、java源码、class编译产物、html测试页、properties配置及少量js文件为主整体仅169KB结构紧凑便于快速对照学习。已有410人学习下载适合初涉WebSocket或想在不引入重型框架前提下快速验证实时通信逻辑的开发者。通过该Demo可以理清端点注册、会话管理、消息转发等核心流程并在此框架上继续扩展鉴权、消息编码或异常处理等生产级能力。 上周在项目里需要给后台管理系统加一个实时消息通知功能用户在前台提交了某个申请后台管理员要立刻看到弹窗提示不用手动刷新页面。接到需求后我第一反应就是SpringBootWebSocket。这套组合的好处是SpringBoot作为后端框架自带WebSocket支持不用额外引第三方组件几行配置就能跑起来。而且WebSocket和SpringBoot的契合度很好不像Netty那样要自己处理很多底层细节。这篇博文就基于我实际的开发过程把从依赖引入、Handler编写、握手拦截器、消息推送改造到最后部署时遇到的那些坑完整地梳理一遍。如果你也准备在自己的项目里用WebSocket做实时推送或者想搞明白这东西到底怎么和SpringBoot结合这篇应该能帮你少走不少弯路。1. 为什么后端推送非要选WebSocket先说说我为什么不用轮询。很多人遇到需要实时感知新数据的场景第一反应是前端定时发Ajax请求每5秒问一次后端“有没有新消息”。这种方式在小规模内部系统里不是不能用但问题很明显用户不操作页面的时候请求白发、服务器压力大、消息到达有延迟。之前我见过一个系统高峰期每分钟几千次无效轮询请求数据库连接池都被拖垮了。WebSocket解决的正是这个问题。它和HTTP一样工作在TCP之上但HTTP是“一问一答”WebSocket是一条长连接客户端和服务端可以随时主动往对方塞数据。连接建立之后服务端想推什么就推什么不用等前端来问。从技术角度说WebSocket和SpringBoot的集成点主要在这几块spring-boot-starter-websocketSpringBoot对WebSocket的自动配置整合帮我们把底层握手、消息分发的基本工作都包掉了。ServerEndpoint注解这是JSR-356标准定义的端点声明方式SpringBoot也支持把某个类标记成WebSocket的入口。WebSocketHandler接口这是Spring专门提供的一套抽象方便和SpringMVC的组件整合适合控制力要求更高的场景。Spring生态里实际有两种写法一种是直接用Java EE标准的ServerEndpoint把WebSocket端点当成一个独立组件来管理另一种是Spring自己封装的WebSocketHandler接口加上HandshakeInterceptor做握手拦截。两种我都试过刚开始图省事用的ServerEndpoint后来发现要注入Spring容器里的Service时总是动不动就是null得靠静态工具类绕弯。后面改成Spring的WebSocketHandler方案才顺过来依赖注入完全通畅拦截器也比注解配置直观。这篇里我就统一讲Spring原生的这套方案。一句话总结选型理由如果你的需求是服务端主动推送、客户端需要保持长连接实时收数据SpringBoot里用WebSocket就是最标准、最省事的做法。不需要上Netty这种重型框架也不要用轮询委屈自己。2. 先搞懂WebSocket握手的几个关键事实在写代码之前我觉得有必要把WebSocket的连接建立过程讲清楚因为后面排查问题全得靠这些知识。WebSocket的连接并不是直接“嗖”一下建立起来的它利用了我们已有的HTTP协议。客户端先发一个普通的HTTP请求请求头长这样GET /ws?userId1001 HTTP/1.1 Host: localhost:8080 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw Sec-WebSocket-Version: 13这段请求的关键是Upgrade: websocket这个头它的意思是“我不打算走普通HTTP流程了麻烦你把协议切换成WebSocket。”服务端看到这个头如果同意切换会返回101状态码HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWkSec-WebSocket-Accept的值是根据请求里的Sec-WebSocket-Key算出来的具体算法是RFC 6455规定的把Sec-WebSocket-Key加上GUID字符串然后做SHA-1哈希再Base64编码。这个细节有个很实际的作用我们调试后端时如果看到连接一直建立不起来可以先手动算算这个值是否正确确认是不是被某个中间代理改了头。握手成功的标志就是状态码101。之后这条TCP连接就升级成了WebSocket长连接客户端和服务端都可以往里面写数据帧。WebSocket的数据帧格式里有一个持久保持连接的机制叫心跳。每隔一段时间客户端或服务端可以发送一个Ping帧对方必须回一个Pong帧。很多长连接断开的问题其实不是网络真的断了而是中间的网络设备比如Nginx、云厂商的负载均衡把空闲连接给掐了。有了心跳连接就会一直被认定为活跃不会被回收。后面我讲配置和踩坑时都会围绕这个点展开。所以做WebSocket开发脑子里要时刻记住两个阶段握手阶段走的是HTTP协议语义连接建立之后走的是WebSocket自己的帧协议。这两个阶段各有一堆可排查的坑点。3. SpringBoot里如何写一个能用的WebSocket端点我先直接上一份完整的、能跑通的代码然后逐个拆解关键点。3.1 引入依赖SpringBoot项目只需要一个starter就够dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency这里有个小提示spring-boot-starter-websocket会自动把内嵌的Tomcat的WebSocket支持拉进来。你不需要额外引入Tomcat的websocket包免得版本冲突。之前见过有人画蛇添足手动又加了tomcat-embed-websocket结果Tomcat版本不一致直接启动报错。3.2 实现WebSocketHandlerSpring的WebSocketHandler接口定义了四个方法我们逐个说Component public class MyWebSocketHandler implements WebSocketHandler { private static final MapString, WebSocketSession SESSION_POOL new ConcurrentHashMap(); private static final ExecutorService PUSH_EXECUTOR Executors.newFixedThreadPool(8); Override public void afterConnectionEstablished(WebSocketSession session) { String userId (String) session.getAttributes().get(userId); SESSION_POOL.put(userId, session); // 这里可以记录一条日志连接建立成功 } Override public void handleMessage(WebSocketSession session, WebSocketMessage? message) { // 生产环境一般是收到客户端的Ping或者业务指令 } Override public void handleTransportError(WebSocketSession session, Throwable exception) { // 网络异常时触发需要在这里做清理 String userId findUserId(session); if (userId ! null) { SESSION_POOL.remove(userId); } } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) { String userId findUserId(session); if (userId ! null) { SESSION_POOL.remove(userId); } } Override public boolean supportsPartialMessages() { return false; } private String findUserId(WebSocketSession session) { return (String) session.getAttributes().get(userId); } }afterConnectionEstablished是连接建立成功后的回调这里适合做在线用户管理。我用了一个ConcurrentHashMap来存“userId - WebSocketSession”的映射。之所以用userId做key而不是sessionId是因为业务上我们根本不管底层连接是谁只关心消息要推给哪个用户这样在后面做“按用户推送”时会特别顺手。handleMessage是收到客户端消息时的入口。很多实际的Demo里这里都是空的因为服务端主动推送场景下客户端主要用来收偶尔发个Ping确认连接活着就完事了。不过如果你要做聊天室之类的双向交互逻辑就写在这里。注意我用了一个Executors.newFixedThreadPool(8)的线程池先说明这只是demo级的写法生产环境建议改为Spring的ThreadPoolTaskExecutor方便监控和动态调参。为什么会有这个线程池因为在Spring的WebSocketHandler里如果我们在处理消息时执行了比较耗时的任务比如查数据库、调外部接口会阻塞Tomcat处理WebSocket消息的线程影响同一条连接上其他消息的处理。把任务丢进线程池主线程可以立刻返回。3.3 注册WebSocket端点和握手拦截器有了Handler还需要把它暴露到指定的路径上。Spring用WebSocketConfigurer来做这件事Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Resource private MyWebSocketHandler myWebSocketHandler; Resource private AuthHandshakeInterceptor authHandshakeInterceptor; Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myWebSocketHandler, /ws) .addInterceptors(authHandshakeInterceptor) .setAllowedOrigins(*); } }有几个配置容易被忽略。setAllowedOrigins(*)如果你的前端页面和后端不在同一个域名或端口这个配置决定要不要拦截跨域请求。在SpringBoot 2.4之前写setAllowedOrigins(*)就行SpringBoot 2.4之后这个方法可能在部分版本不生效报错时会提示用setAllowedOriginPatterns。我碰上过一次前端页面在8080端口后端在8081端口WebSocket连接死活建不起来浏览器控制台报跨域错误。原因就是这里只设置了setAllowedOrigins后来换成下面的写法才通过registry.addHandler(myWebSocketHandler, /ws) .addInterceptors(authHandshakeInterceptor) .setAllowedOriginPatterns(*);addInterceptors不是必填项但很多时候我们必须在握手阶段校验用户是否已登录。HandshakeInterceptor里有beforeHandshake方法它能拿到当前的HTTP请求这样就能从请求参数、Header或者Cookie里取出鉴权信息Component public class AuthHandshakeInterceptor implements HandshakeInterceptor { Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, MapString, Object attributes) { // 从请求参数里取userId生产环境应该用token去redis里换用户信息 String userId request.getURI().getQuery() ! null ? extractUserIdFromQuery(request.getURI().getQuery()) : null; if (userId null) { return false; // 拒绝握手 } attributes.put(userId, userId); // 关键这里的attributes会传给Handler里的session.getAttributes() return true; } Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { // 握手完成后的回调一般用不上 } }这里有一个非常关键的坑beforeHandshake方法最后的attributes.put(userId, userId)这个attributesMap最终会被绑定到WebSocketSession的attributes属性上。也就是说afterConnectionEstablished里通过session.getAttributes().get(userId)拿到的就是这里放进去的值。如果忘记这一步后面在Handler里想要识别当前连接属于哪个用户就只能靠解析请求URL又要多写一堆麻烦代码。3.4 测试一下能不能通代码写完先用简单方式验证。找一个WebSocket在线测试工具网上很多搜websocket在线测试就能找到连接地址填ws://localhost:8080/ws?userId1001连接成功的话后端日志会打印afterConnectionEstablished里的记录。如果你用的是SpringBoot内置Tomcat默认http端口就是WebSocket端口不需要单独开端口。需要注意URL的scheme是ws不是http很多第一次接触的人在这里栽过跟头。4. 消息推送时踩过的“连接已关闭”坑连接建立起来了Handler也注册好了剩下的核心问题就是怎么把消息主动推给客户端。很多人的代码里会写类似这样的推送public void sendToUser(String userId, String message) { WebSocketSession session SESSION_POOL.get(userId); session.sendMessage(new TextMessage(message)); }如果测试时客户端一直在线这段代码没任何问题一切看起来都很顺利。但一旦用户关了浏览器或者手机切了飞行模式问题就来了session.sendMessage会抛IllegalStateException异常信息大概是“The remote endpoint was in state [TEXT_PARTIAL_WRITING]”或者“Connection closed”。如果你没有try-catch这条推送还会把上层业务逻辑打断。我真实的踩坑经历是这样的有一个给管理员推送待办消息的服务循环遍历所有在线的管理员连接并逐个推送。结果其中一个管理员的网络断了但服务端还没来得及执行afterConnectionClosed清理SESSION_POOL里还残留着他的session。推送时抛异常后后面的管理员全都收不到消息。排查日志时看到一大片异常堆栈才知道是单个脏连接引发的连锁反应。修复方案很简单也很必要——推送之前先判断session.isOpen()发送过程中包一层try-catch发送失败后立刻从池子里移除public void sendToUser(String userId, String message) { WebSocketSession session SESSION_POOL.get(userId); if (session ! null session.isOpen()) { try { synchronized (session) { session.sendMessage(new TextMessage(message)); } } catch (IOException e) { SESSION_POOL.remove(userId); // 记日志通知业务方该用户已掉线 } } }加synchronized (session)这个细节是后来才想明白的。Tomcat的WebSocketSession底层并发写是不安全的如果同时有两个线程往同一个session发送消息可能出现消息内容互相穿插的脏读。WebSocket官方文档里明确要求“一个连接同时只能有一个线程在写”所以用synchronized锁住session是最简单的保证方式。另一个坑是推送线程被阻塞。某个客户端如果接收得很慢sendMessage会一直阻塞在TCP的写缓冲区上。最开始我没处理这个问题测试时发现服务端推送一条大消息几MB时整个推送线程卡了好几十秒其他用户的推送全部排队等待。解决办法有两个方向一是给sendMessage加超时控制用session.setBinaryMessageTimeLimit这类API二是把推送任务丢进独立线程池不要阻塞业务主流程。我最后采用了线程池方案PUSH_EXECUTOR.execute(() - { sendToUser(userId, message); });线程池隔离的效果很明显某个慢客户端顶多拖垮池子里几个线程不会把整个业务线程拖死。但线程池的线程数要合理设置太小了推送吞吐不够太大了又浪费资源8只是demo环境的保守值生产环境可以根据活跃连接数和推送频率来调。5. 服务端主动推送的三种玩法很多人把WebSocket集成进去了但真正要用的时候反而有点懵“我Handler里连路径都配好了怎么从别的类里触发推送啊”这里我总结了三种实际项目里用得上的玩法。5.1 静态方法直接推老项目里常用这种玩法。把Session池和推送方法都写成静态的需要推送时直接用类名调用public class WebSocketSessionPool { private static final MapString, WebSocketSession POOL new ConcurrentHashMap(); public static void add(String userId, WebSocketSession session) { POOL.put(userId, session); } public static void remove(String userId) { POOL.remove(userId); } public static void sendToUser(String userId, String message) { WebSocketSession session POOL.get(userId); if (session ! null session.isOpen()) { try { session.sendMessage(new TextMessage(message)); } catch (IOException e) { POOL.remove(userId); } } } }好处是调用方便哪里都能用。坏处是静态Map在集群环境下变成每个节点的本地数据消息推送给哪个节点是个问题——这一点我后面单独讲。5.2 Spring事件驱动推送在SpringBoot项目里更符合框架习惯的做法是发内部事件。业务代码Post一个事件由监听器统一负责推送。这样业务逻辑和WebSocket推送逻辑解耦将来想换推送渠道比如再加一个短信通知也不用动原业务代码// 定义事件 public class MessagePushEvent extends ApplicationEvent { public MessagePushEvent(Object source, String userId, String content) { super(source); this.userId userId; this.content content; } } // 业务代码里发事件 applicationEventPublisher.publishEvent(new MessagePushEvent(this, userId, content)); // 监听器里做推送 EventListener public void onMessagePush(MessagePushEvent event) { WebSocketSessionPool.sendToUser(event.getUserId(), event.getContent()); }这种方式在多节点环境里还能搭配消息队列做跨节点广播业务代码往Redis的Pub/Sub或者RabbitMQ里发一条消息所有节点的监听器都收到通知让每个节点只推给本机持有的连接。这就是分布式的常规解法比直接调用静态方法优雅得多。5.3 定时任务推心跳顺便探活WebSocket长连接的本质是TCP连接存在被中间设备“静默掐断”的风险。设备本身没给你发TCP RST包所以你的服务端完全感知不到连接已经失效。这种情况下定时推一条Ping消息过来能维持链路的存活状态// 配合Spring的Scheduled任务 Scheduled(fixedRate 30000) public void heartbeatCheck() { for (String userId : WebSocketSessionPool.getOnlineUserIds()) { WebSocketSession session WebSocketSessionPool.getSession(userId); if (session ! null session.isOpen()) { try { session.sendMessage(new PingMessage(() - { ByteBuffer buffer ByteBuffer.allocate(1); buffer.put((byte) 0); return buffer; })); } catch (IOException e) { // 连接已经失效清理掉 WebSocketSessionPool.remove(userId); } } } }PingMessage是WebSocket协议里的心跳帧。服务端主动发送Ping客户端收到后不需要前端做什么特殊处理——浏览器会自动回一个Pong。如果客户端已经断开发送Ping会抛异常正好帮我们完成“探活清理”的双重任务。实测里这是最有效的清理失效连接手段比单纯依赖afterConnectionClosed回调稳得多因为有很多断开场景服务端是收不到关闭帧的。6. 前端配合和后端鉴权中的实用细节很多人做WebSocket Demo时只关注后端结果前端连不上就开始怀疑是后端写错了。其实前端也有几个关键点。6.1 前端WebSocket的写法如果用原生JavaScript连接和接收消息大概长这样let socket null; let heartbeatTimer null; function connectWebSocket(userId) { const protocol location.protocol https: ? wss : ws; socket new WebSocket(${protocol}://${location.host}/ws?userId${userId}); socket.onopen function() { // 连接成功开始心跳 heartbeatTimer setInterval(() { if (socket.readyState WebSocket.OPEN) { socket.send(ping); } }, 25000); }; socket.onmessage function(event) { const data JSON.parse(event.data); handlePushMessage(data); }; socket.onclose function() { clearInterval(heartbeatTimer); // 自动重连指数退避 setTimeout(connectWebSocket, 3000, userId); }; socket.onerror function(error) { // 错误多半会在onclose里触发这里记日志即可 }; }这里有几个可以直接落地的经验。心跳和重连是必须的心跳每隔25秒发一次防止中间设备把空闲连接断开服务端一般把空闲超时设置为60秒或更长所以心跳间隔必须小于服务端超时时间。重连要有退避策略否则服务端重启的瞬间所有客户端一起疯狂重连连接风暴直接打崩服务器。如果前端用的是Vue很多人喜欢配vueuse/core里的useWebSocket。这个组合式函数把连接状态、自动重连、消息收发都封装好了比手写原生WebSocket要省事尤其适合组件里需要响应式管理连接状态的场景。6.2 握手阶段鉴权的实践经验WebSocket的握手请求是HTTP请求所以我们可以在HandshakeInterceptor里做鉴权。但要注意浏览器WebSocket API没办法自定义请求头。如果我们要在握手时携带token只能通过以下三种方式之一URL参数ws://localhost:8080/ws?tokenxxx。简单直接但token会写在历史记录里有泄漏风险。Cookie握手时会自动带上同源的Cookie。后端从Cookie里取安全性和普通HTTP请求一致。前提是前后端同域或者CORS配置允许携带Cookie。子协议头Sec-WebSocket-Protocol可以用它偷渡token但会被一些网关和代理过滤不建议常规使用。我的习惯是如果做了登录系统就把token放在Cookie里走第二种方式临时demo就用URL参数图省事。一定要清楚一点你在beforeHandshake里返回false或者直接抛异常客户端看到的不会是HTTP 401而是WebSocket的onclose事件错误码通常是1006异常关闭。所以生成环境如果发现用户莫名其妙被断开记得在后端把握手失败的日志打详细一点不然前端很难排查。6.3 一个隐蔽问题分布式部署后的连接漂移如果你的SpringBoot应用将来要部署多个实例没有做负载均衡时两个节点各管各的那么用户A连的是节点1管理员B连的是节点2节点1往B推送时发现B的session不在自己的池子里推送失败——这就是分布式场景下的连接漂移问题。我在真实项目里的处理方式是引入Redis的Pub/Sub。思路不复杂每个节点把“当前节点持有哪些userId”上报到Redis。推送时先查Redis拿到userId对应的节点编号。如果命中本节点直接推送如果命中其他节点通过Redis的channel发一条消息让那个节点代为推送。这样就不需要引入Kafka或RabbitMQ这种重量级中间件也能在集群环境下把推送消息“投递”到正确的节点。Demo阶段可以先不考虑这个但它是一个真实项目迟早要面对的问题。如果你打算上云还要注意负载均衡器需要开启WebSocket支持通常表现为Upgrade和Connection这两个头的透传否则连接会在负载均衡层被截断。7. 关于配置文件和常见报错的小结SpringBoot的WebSocket基本不需要额外配置但有几个参数值得知道。server.websocket.timeout不是SpringBoot官方标准配置项不同版本的支持程度不一样。更常见的做法是通过实现WebSocketConfigurer时在底层容器层面做调整。如果你用的是内嵌Tomcat可以直接这样Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container new ServletServerContainerFactoryBean(); container.setMaxSessionIdleTimeout(60000L); // 空闲连接60秒超时 container.setMaxTextMessageBufferSize(1024 * 1024); // 单条文本最大1MB container.setMaxBinaryMessageBufferSize(1024 * 1024); return container; }这里有一个很容易踩的坑setMaxSessionIdleTimeout设的是整个连接的空闲超时如果我们的业务需要长时间保持连接但又不想一直被心跳打扰可以把这个值设大一些。但要注意如果设得太小比如30秒而前端心跳间隔是25秒就很容易出现“刚连上就被断开”的怪问题。我之前就碰过一次前端心跳发的是文本消息“ping”但我没有在handleMessage里处理它导致心跳虽然在传却被容器判定为“空闲”因为文本消息不算协议层的心跳帧结果连接照样被超时清理。后来把前端改成发PingMessage或者在后端handleMessage里做一个收到消息即视为活跃的处理问题才解决。顺手列几个常见的异常和对应的解决办法异常现象可能原因解决办法连接立即断开日志无任何记录握手拦截器返回了false在beforeHandshake里加日志确认是否鉴权失败前端报“Unexpected response code: 200”请求被拦截器或过滤器改写了或路径没匹配到确认/ws路径是否被权限框架如Shiro、Spring Security拦截广播时抛IllegalStateExceptionsession已经失效但仍在池里推送前检查isOpentry-catch后清理多个节点各推各的消息缺失分布式环境连接分散引入Redis Pub/Sub做跨节点转发握手跨域失败前后端不同源时未正确配置Origin用setAllowedOriginPatterns(*)启动时Bean创建失败忘了加EnableWebSocket在配置类上补注解其中“前端报200”那个问题排查思路是这样的浏览器地址栏输入http://localhost:8080/ws?userId1001如果返回的是JSON错误而不是101状态码说明请求被SpringMVC的拦截器或某个过滤器提前处理了。最常见的是项目里引入了Spring Security安全过滤器在握手之前就把请求拦截下来返回302跳转登录页。解决方法是放行WebSocket握手路径或者给该路径单独配置匿名访问权限。8. 一些真实项目里的思考再说说我当时上线这个功能时除了写代码之外还需要考虑的三件事这些都不会在常规教程里教你但对项目的稳定运行挺关键。第一个是消息可靠性。WebSocket推送本质上是尽力而为网络抖动、客户端崩溃、服务端重启都会导致消息丢失。如果业务要求“消息必须送达”那么光靠WebSocket是不够的需要自己实现补偿机制。比如推送前先把消息落库客户端收到消息后回一个ack服务端定期扫库找没有ack的消息重新推送。我当时的做法是在Redis里存一个待推送队列推送成功才移除定时任务每5分钟扫一次补推。第二个是连接数上限。WebSocket连接是长连接每一条连接都会占用一个文件描述符。默认情况下Linux系统单个进程能打开的文件描述符上限是1024如果你的在线用户数超过这个数字必须调整ulimit否则连接数到顶后新的连接直接失败。这是一个非常隐蔽的运维问题测试环境怎么都测不出来一上生产用户一多就出问题。第三个是优雅停机。用kill -9杀进程对WebSocket服务来说很残忍因为连接不会正常关闭客户端必须等到心跳超时才发现服务端已挂。更平滑的做法是配置Spring Boot的优雅停机server: shutdown: graceful这样在应用关闭时Spring会等待已建立的连接处理完当前任务再关闭WebSocket连接也会收到正常的关闭帧客户端可以及时发现并重连到其他节点。这个配置对于所有线上应用都值得开尤其是我们这种长连接服务。我这套代码跑下来之后最明显的体感是系统里的“在线状态”变得更加可信了管理员再也不用靠一遍遍刷新页面来找活干。如果你也准备在SpringBoot项目里接入WebSocket建议从最简单的单机Demo开始先把连接建立、消息推送、断开清理这三个环节跑通再逐步考虑集群、鉴权和消息可靠性的问题。每个阶段踩的坑都有不同解法但核心思路都是围绕连接状态和消息投递这两个维度来设计的。本文还有配套的精品资源点击获取

相关新闻

2026/9/8 7:27:25

VideoLineForJS:纯JS视频回放轴组件设计与海康设备对接实践

简介:面向海康威视等视频平台回放场景,此资源提供一款基于JavaScript的VideoLineForJS视频轴组件,适合Web前端开发者快速集成时间轴选择与视频回放联动功能。组件通过简洁的API回调将选中时间点返回给页面,示例代码演示了起止时间…

2026/9/8 7:22:23

Excel文件被覆盖如何恢复到几天前?Windows原生恢复机制实战

1. 问题场景与恢复思路先说个真实经历。我有一份记录了三个月项目支出的Excel表格,里面包含了供应商报价、采购明细、运费分摊这些核心数据。有天下午我想改一个表格里的透视表布局,顺手又调整了几个Sheet的列宽,结果手一抖把"数据汇总&…

2026/9/8 8:32:30

PHP+MySQL社区系统开发与宝塔面板部署实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/8 8:32:30

Spring Boot家装项目管理系统:从需求到远程调试的完整实战

做装修公司信息化这行快十年,见过太多工地上“人盯人”的管理方式了。项目经理翻着手机找聊天记录报进度,老板想看一眼各工地资金占用情况得等财务月底拉Excel,客户三天两头问“我家装到哪一步了”却得不到准确答复——这些都是装修公司项目管…

2026/9/8 8:32:30

SSM+Vue乐器销售管理系统毕设全攻略:从数据库设计到答辩

2026届的毕设题目下来得比往年早,很多同学开题就领到了“乐器销售管理系统”这个题目,技术栈指定ssmvue,要求论文和程序一起交付。说实话,这类题目属于经典的“管理系统”家族,网上能搜到的代码很多,但真正…

2026/9/8 8:32:30

FLIR T600系列热像仪全解析:从参数读懂到现场实拍

干这行久了,会发现一个很有意思的现象:每逢设备采购季,总有人抱着FLIR T600系列的参数表来问我,指着640x480分辨率、40mK热灵敏度这些数字,反复确认到底哪款够用。参数摆在那里,谁都能对比,可真…

2026/9/8 7:15:10

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/8 7:15:15

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/8 7:15:10

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/8 0:01:49

踩多轮坑才跑通|OpenClaw 3.1.0 双平台本地 AI 自动化搭建实操实录

🔹 工具简述 OpenClaw 是一款备受开发者与办公人群青睐的开源本地智能工具,凭借离线本地运行、可视化图形面板、全流程自主任务处理三大核心特点,积累了众多忠实用户。与普通对话类 AI 产品不同,它能够直接调用电脑的软硬件操作权…

2026/9/8 0:01:50

拒绝复杂命令行,Hermes Agent 一键包快速解锁智能办公能力

🔍前言 不少想要体验 Hermes Agent 办公能力的使用者,往往会被复杂的环境配置拦住使用脚步。手动下载匹配依赖、反复调整系统目录、处理命令行持续报错、修复权限异常、补全丢失核心文件等一系列操作,对普通使用者而言门槛较高,很…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/7 22:45:59

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…