尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
若依框架集成WebSocket:后台实时推送的完整实战指南
这段时间一直在折腾若依框架权限、菜单、用户体系这套基础能力确实省心但真到做具体业务模块的时候有个东西基本每次都要自己补服务端主动给浏览器推数据。站内信要实时出红点、运维监控要往大屏上推告警、客服要跟页面访客开在线会话这些场景靠前端定时轮询接口撑得了一时撑不住用户量和实时性的双重压力。把 WebSocket 集成进若依成了很多做后台项目的开发者迟早要面对的一关。不少同学第一次搞这块直接搜个 demo 就往上抄结果不是握手一直 404就是连接管理漏了 session 清理再不然被项目的登录鉴权拦在半路上。这篇文章我把若依里集成 WebSocket 的完整过程、选型理由和实际踩过的坑从头到尾捋一遍项目里需要实时推送的可以照着改不需要重复造轮子。1. 先说结论为什么若依项目里需要 WebSocket1.1 哪些业务场景非它不可先说几个我在实际项目里遇到过的场景。第一个就是站内通知后台系统里经常有提交审批、审批通过、有人回复了你这一类消息以前的实现方式很原始前端每隔十秒或者三十秒拉一次未读消息接口看到有新的就弹提示、刷新红点。用户量小的时候这方案能用但接口被轮询打得太难看而且消息到达的体验是延迟的用户可能提交完在页面等半天才有反馈。第二个场景是数据大屏和监控面板。大屏上要展示实时成交金额、设备状态、在线人数这些数据如果靠页面自己在定时器里刷新整个屏幕会频繁整块重绘动画不连贯不说后端接口压力也大。用 WebSocket 之后服务端一旦发现数据变化就主动推给大屏页面整个页面可以做到秒级甚至毫秒级更新大屏体验完全不一样。第三个场景是客服聊天和一键双工交互。用户在 H5 页面找客服客服在后台接待两个人之间需要一条双向通道用户发一句客服立刻收到客服回复用户那边马上显示。HTTP 轮询在这种场景下体验是断断续续的哪怕做到每秒拉一次也像在用对讲机。WebSocket 建立后就是一条长连接消息随发随达。这些场景说到底都是同一个诉求服务端要能主动找客户端说话。传统 HTTP 请求-响应模型里客户端不发请求服务端就没机会开口WebSocket 就是专门解决这个问题的。1.2 轮询、长轮询、SSE 和 WebSocket 怎么选很多人在方案讨论阶段会纠结到底用轮询还是上 WebSocket能不能用 SSE服务端推送事件凑合。我整理过一个简单的对比放在团队里讨论方案时直接用。方案连接方式实时性服务端压力实现复杂度适用场景短轮询反复请求由轮询间隔决定高请求量巨大最低低频非实时临时功能长轮询请求挂住等响应较好中等连接占用多中旧系统改造兼容要求高SSE单向长连接好较低服务端推较低仅需要服务端单向推送WebSocket双向长连接最好低一条连接双工中高双向实时交互、在线状态等我的结论很直接服务端要推的是一对多的广播通知、或者纯展示类数据SSE 是可以考虑的但只要涉及双向交互、在线状态维护、用户和用户之间点对点通信就直接上 WebSocket。虽然编码量比 SSE 多一个拦截器和 session 管理的活儿但后面扩展聊天、在线客服、多人协作这类功能时WebSocket 底座是现成的不用再推倒重来。另外提一句性能上的体感。以前用短轮询做站内通知一千个在线用户每 30 秒轮询一次相当于每分钟多出 2000 个请求后端接口和数据库压力是实打实的换成 WebSocket 之后同样一千个在线用户只有一千条长连接挂在服务端每秒心跳和业务消息加起来不过几十条压力几乎可以忽略。长连接占的是连接数和内存但对普通后台系统来说远没有轮询的 QPS 冲击来得猛。2. 集成方案选型与整体设计思路2.1 为什么用 Spring WebSocket 而不是 STOMP 或 Netty若依基于 Spring Boot所以选择上天然有三条路直接用 Spring 自带的 WebSocket、用 STOMP 协议子协议栈、或者引 Netty 自己搭一套链路。我的建议是直接用 Spring 的org.springframework.web.socket这套原生 API不建议一上来就搞 STOMP。原因有三一是原生 API 足够简单配置一个 handler、一个拦截器就能跑起来没有多余的概念二是 STOMP 虽然提供了订阅、广播这些高级语义但调试复杂度也上去了前端要引 STOMP 客户端库后端要配消息代理对若依这种单机部署也能跑的后台系统来说属于杀鸡用牛刀三是项目的登录鉴权基于 Spring Security 或 Shiro原生的 WebSocket 握手过程可以自己写拦截器把用户身份从 token 里解析出来整个过程自己能完全控制STOMP 多了协议转换的一层反而更容易出认知偏差。Netty 就更不用考虑了。Netty 适合做高并发网关、IM 服务这种独立的长连接服务而若依是典型的 Spring MVC 后台管理系统。在 Spring Boot 项目里再引入一套 Netty 长连接服务等于同时维护两套应用服务端口、部署、监控都要单独处理对绝大多数项目是负收益。2.2 代码放在哪个模块最合适用过若依的都清楚它典型的分模块结构一般长这样ruoyi-admin是启动入口和 Controller 层ruoyi-framework放框架配置ruoyi-common放通用工具ruoyi-system放系统业务模块。WebSocket 代码我建议新建一个子包放在ruoyi-framework下比如com.ruoyi.framework.websocket。这样放的逻辑是WebSocket 的握手过程要依赖登录鉴权相关的类比如若依分离版的TokenService而TokenService本来就属于框架层同时ruoyi-system里的业务 Service 在消息发生时要调用推送能力依赖方向是业务层往下依赖框架层这是若依原本就有的依赖关系。如果项目已经改成单模块结构了直接建一个com.company.project.websocket包也行核心是让连接管理类Session 池和业务推送方法能被任意业务模块调用路径上不要彼此绕弯路。有一点需要留意WebSocket 配置类上加了Configuration之后Spring 扫描要能扫到它所在的包。若依启动类扫描的根包通常是com.ruoyi只要你不把包建到根包外面一般没问题。但如果你在若依的某些衍生版本里改了启动类扫描路径这个小细节很容易让人排查半天。3. 若依集成 WebSocket 核心代码实战按我现在的做法整个过程分六步走加依赖、写配置类、写 Handler、写握手拦截器、维护 Session 池、打通登录放行。下面按顺序过一遍都基于若依比较常见的分离版Spring Security JWT来写Shiro 版本的差异我放在后面提醒。3.1 添加 Maven 依赖第一步是在pom.xml里引入 Spring Boot 的 WebSocket starter。若依项目的父工程已经统一管理了 Spring Boot 版本所以这个依赖一般不需要写version。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency这里有个不少人犯过的迷糊以为 WebSocket 是 Spring Web 的一部分项目里已经引了spring-boot-starter-web就不用管了。实际不是spring-boot-starter-web只包含 Spring MVC 那一套WebSocket 需要额外这个 starter。不加依赖直接写EnableWebSocket应用启动时不会立即报错但容器初始化到 WebSocket 注册的 bean 时会报NoClassDefFoundError这类错误出现在启动后半段比编译期报错难受得多。3.2 编写 WebSocket 配置类加完依赖后新建一个配置类实现WebSocketConfigurer接口把 Handler 和拦截器注册到指定路径上。Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { private final WebSocketHandler webSocketHandler; private final WebSocketHandshakeInterceptor handshakeInterceptor; public WebSocketConfig(WebSocketHandler webSocketHandler, WebSocketHandshakeInterceptor handshakeInterceptor) { this.webSocketHandler webSocketHandler; this.handshakeInterceptor handshakeInterceptor; } Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(webSocketHandler, /ws) .addInterceptors(handshakeInterceptor) .setAllowedOriginPatterns(*); } }路径/ws是前端连接 WebSocket 时用的端点后面 Security 或 Shiro 的放行路径也得跟着它写。addInterceptors把握手拦截器挂上去这样每次前端连接 WebSocket 时都会先经过拦截器做身份校验。setAllowedOriginPatterns(*)这个配置在新版本 Spring 里很关键旧版本用的是setAllowedOrigins(*)但那套对跨域来源的处理比较死板一旦带Origin头就容易出问题新版本更推荐setAllowedOriginPatterns。在前端页面和后端不在同一域名的时候没有这个配置会直接握手失败。3.3 实现消息处理器 HandlerHandler 是 WebSocket 的核心处理类负责连接建立、收发消息、连接关闭这些生命周期事件。我写的是继承TextWebSocketHandler专门处理文本消息。Component public class WebSocketHandler extends TextWebSocketHandler { Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { Long userId (Long) session.getAttributes().get(userId); if (userId ! null) { WebSocketSessionPool.addSession(userId, session); // 这里可以给用户推一条欢迎消息或者标记在线 } } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload message.getPayload(); // 和前端约定好 ping/pong 心跳 if (ping.equals(payload)) { session.sendMessage(new TextMessage(pong)); } } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { Long userId (Long) session.getAttributes().get(userId); if (userId ! null) { WebSocketSessionPool.removeSession(userId); } } Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { Long userId (Long) session.getAttributes().get(userId); if (userId ! null) { WebSocketSessionPool.removeSession(userId); } session.close(CloseStatus.SERVER_ERROR); } }连接建立后我从session.getAttributes()里把userId取出来这个值是在握手拦截器里放进去的。这里要强调一个新手特别容易搞混的点WebSocketSession是 Spring 封装的会话接口不是 Java 自带的javax.websocket.Session。第一次写的时候很容易把javax.websocket.Session引进来然后发现它根本没有getAttributes()方法翻半天 API 找不到人。只要 import 的是org.springframework.web.socket.*这些方法才存在。handleTransportError我习惯在里面做一次清理并关闭连接。网络异常、服务端主动断开、客户端异常拔网线这些情况都会走到这个方法不在里面清理的话Session 池里会遗留大量已失效连接推消息时永远报MessageDeliveryException。3.4 实现握手拦截器绑定登录用户握手拦截器的核心作用是在 WebSocket 连接建立之前拿到当前用户信息并把它放进 WebSocket 会话的属性里。这样后面的 Handler 才能知道这个连接是谁。Component public class WebSocketHandshakeInterceptor implements HandshakeInterceptor { Resource private TokenService tokenService; Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, MapString, Object attributes) throws Exception { if (request instanceof ServletServerHttpRequest servletRequest) { String token servletRequest.getServletRequest().getParameter(token); LoginUser loginUser tokenService.getLoginUser(token); if (loginUser null) { response.setStatusCode(HttpStatus.UNAUTHORIZED); return false; } attributes.put(userId, loginUser.getUserId()); attributes.put(username, loginUser.getUsername()); return true; } return false; } Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { } }这段代码里的TokenService是若依框架里现成的登录用户服务getLoginUser方法会根据前端传的 token 解析出登录用户。如果 token 无效直接返回false握手失败连接不会建立。在若依这种带登录鉴权的系统里前端在连接 WebSocket 时把 token 放在 URL 查询参数上是最简单的做法。也有人会把 token 放在请求头里再写 JS 拦截器去处理但 WebSocket 握手的头信息在很多前端环境里不好自定义直接用 URL 参数最简单直接。需要注意一句URL 参数方式意味着 token 会出现在日志里如果对安全敏感可以改成先把 token 换成一个短时有效的随机 code 再拼到 URL 里后端用 code 换用户信息这里我不展开细讲。Shiro 版本的若依没有TokenService思路可以换成把/ws加入匿名访问后在握手拦截器里获取前端传入的业务凭证再用项目的自定义认证逻辑解析出用户 ID。原理一样都是握手必须确定连接归属。3.5 管理在线连接的 Session 池Session 池是 WebSocket 集成里的关键组件。因为项目里要给指定用户推送就必须把用户 ID 和 WebSocket 会话的对应关系存起来。我直接用一个静态的ConcurrentHashMap来存。public class WebSocketSessionPool { private static final MapLong, WebSocketSession SESSIONS new ConcurrentHashMap(); private WebSocketSessionPool() { } public static void addSession(Long userId, WebSocketSession session) { WebSocketSession oldSession SESSIONS.put(userId, session); if (oldSession ! null oldSession.isOpen()) { try { oldSession.close(); } catch (IOException e) { // 忽略关闭异常 } } } public static void removeSession(Long userId) { SESSIONS.remove(userId); } public static WebSocketSession getSession(Long userId) { return SESSIONS.get(userId); } public static void sendToUser(Long userId, String message) { WebSocketSession session SESSIONS.get(userId); if (session ! null session.isOpen()) { try { session.sendMessage(new TextMessage(message)); } catch (IOException e) { removeSession(userId); } } } public static void sendToAll(String message) { for (Map.EntryLong, WebSocketSession entry : SESSIONS.entrySet()) { WebSocketSession session entry.getValue(); if (session ! null session.isOpen()) { try { session.sendMessage(new TextMessage(message)); } catch (IOException e) { removeSession(entry.getKey()); } } } } }比如用户重新登录、或者在两个浏览器标签页打开了同一个系统会出现同一个userId发来多条连接的情况。我在addSession里做了个处理新连接进来时把旧连接直接关掉保证一个用户最多只有一个活跃连接。这个设计在推送通知时很重要否则一条消息推两次前端界面上会出现重复弹窗。关掉旧连接这个操作要放在finally或者确保SESSIONS.put执行成功后再做避免新连接还没放进去旧连接已经被关了造成空档期。3.6 打通登录鉴权Spring Security / Shiro 放行如果只写代码不去改鉴权配置前端会一直握手失败因为若依的 Spring Security 会拦截所有请求未经过认证的 WebSocket 握手请求会被弹回登录页。分离版若依里我都是找到SecurityConfig里配置放行路径的地方在permitAll列表里加上 WebSocket 的端点.permitAll() .antMatchers(/login, /ws/**).permitAll()注意这里的细节如果你的端点注册路径是/ws建议写成/ws/**。如果你注册的是/ws/{userId}这种多级路径一定要把子路径的形式覆盖到。有些项目里路径匹配规则没统一就漏了一个层级的放行结果连前端浏览器控制台都报 404。Shiro 版本不在这套配置里。Shiro 版要改的是ShiroConfig中的ShiroFilterFactoryBean在filterChainDefinitionMap里把/ws/**配置为anon也就是匿名可访问同时实际的身份校验放到握手拦截器里做。这里要提醒一句anon是放行给拦截器自研校验的意思不是安全漏洞因为你已经在beforeHandshake里做了 token 校验握手不通过的人是建立不了连接的。3.7 业务侧如何主动推送消息连接管理和身份绑定都齐了之后最后一步就是在业务代码里调用推送方法。举个例子发布通知公告时要给所有在线用户推一条消息。Service public class NoticeService { public void publishNotice(Notice notice) { // 省略原有的业务处理逻辑 MapString, Object pushData new HashMap(); pushData.put(type, NOTICE); pushData.put(noticeId, notice.getId()); pushData.put(title, notice.getTitle()); pushData.put(time, System.currentTimeMillis()); WebSocketSessionPool.sendToAll(JSON.toJSONString(pushData)); } }如果是给指定用户推消息就换成WebSocketSessionPool.sendToUser(userId, JSON.toJSONString(pushData))。像审批流这种场景审批人提交后当前审批人的配置单发生变化在负责人节点判断出下一步处理人时直接把待办消息推过去用户页面不用任何请求操作就能看到列表更新体验非常顺。我项目里通常会把推送的内容约定成一个统一的 JSON 结构至少包含消息类型type、业务 ID、标题、时间这几个字段前端收到后根据 type 决定是弹提示、刷新某个列表还是跳转页面。不要让前端去猜这个推送是干嘛的结构清晰比省几个英文字符重要。4. 前端页面对接、心跳与断线重连4.1 前端 JS 建立连接后端全部就绪前端这边其实很简单。若依的前端一般是 Vue axios而 WebSocket 不用 axios浏览器原生支持。下面是前端连接的核心代码。let ws null let heartbeatTimer null let url function connectWebSocket() { const token getToken() const protocol location.protocol https: ? wss:// : ws:// url protocol location.host /ws?token token ws new WebSocket(url) ws.onopen function () { console.log(WebSocket 连接成功) startHeartbeat() } ws.onmessage function (event) { const data JSON.parse(event.data) handlePushMessage(data) } ws.onclose function () { console.log(WebSocket 连接关闭) stopHeartbeat() setTimeout(connectWebSocket, 3000) } ws.onerror function (error) { console.error(WebSocket 异常, error) ws.close() } } function handlePushMessage(data) { // 根据 data.type 决定页面行为 // 例如 type NOTICE 时刷新未读消息数量并弹提示 }通过location.protocol自动区分ws://和wss://这个写法很实用。部署在 HTTPS 环境下的系统浏览器会强制要求使用wss://如果前端还写死ws://连接会被浏览器直接拦掉控制台提示混合内容错误。4.2 心跳保活机制WebSocket 连接有个特点长期不传数据中间的网络设备比如 Nginx、云负载均衡有可能把连接闲置断掉而服务端和客户端可能都是假活状态谁都没感知。最稳妥的做法是客户端定期发一个心跳包服务端回一个包双方都确认连接是真的活着。我这里约定的是文本协议里最简单的一种客户端每 30 秒发送ping服务端在handleTextMessage里收到ping就回pong。前端代码如下function startHeartbeat() { stopHeartbeat() heartbeatTimer setInterval(function () { if (ws ws.readyState WebSocket.OPEN) { ws.send(ping) } }, 30000) } function stopHeartbeat() { if (heartbeatTimer) { clearInterval(heartbeatTimer) heartbeatTimer null } }如果超过一定时间没有收到服务端的任何消息前端也可以认为连接断了主动触发onclose重连逻辑。通常我在前端约定的是60 秒内既没有收到消息也没有收到 pong就主动ws.close()让重连机制接管。服务端这边还有一个配置可以配合使用在注册 Handler 时可以设置空闲会话超时。registry.addHandler(webSocketHandler, /ws) .addInterceptors(handshakeInterceptor) .setAllowedOriginPatterns(*);如果要控制服务端空闲断开时间可以在 Handler 里调用session.setMaxSessionIdleTimeout(60000);不过我不建议把这个值设置得太小因为如果心跳间隔 30 秒、空闲超时 60 秒网络稍微抖动一下连接就可能被服务端主动断开前端还没反应过来就被重连了。一般设置在 120 秒到 300 秒之间比较安全。5. 高频问题排查与避坑实录5.1 连接一直 404 或握手失败WebSocket 连接失败最常见的现象是浏览器 Network 面板里请求状态是 404或者 101 没出现。这个问题我从两个方向查。第一优先查鉴权放行。确认若依的 Spring Security 或 Shiro 配置里/ws/**确实已经在放行列表里。这是个很容易忽略的点因为 WebSocket 握手请求本身是一次普通的 HTTP GET 请求会被项目的安全过滤器拦截。改了配置之后注意要重启服务开发者工具里的缓存经常让人误判。第二查 Nginx 配置。如果项目是 Nginx 反向代理过的光有后端放行还不够Nginx 默认不会转发 WebSocket 的升级请求。要在 Nginx 的 server 或 location 配置里加上下面这段location /ws/ { proxy_pass http://你的后端服务地址; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }proxy_read_timeout和proxy_send_timeout我习惯设到 3600 秒也就是一小时避免代理层因为长时间没有数据传输把连接断掉。心跳能续命但代理层超时是硬配置超了照样断。第三查握手拦截器。如果beforeHandshake返回了false后端日志里会存在安全校验失败的记录。这种情况前端看到的可能不是 404而是一个 403 或 401。可以临时在拦截器里打一行日志看是否走到了这个方法快速定位是哪一环拒绝的。5.2 连上了但消息推不过去连接能建立说明握手和鉴权都没问题了这时候推消息失败十有八九出在 Session 管理上。最常见的坑是Handler 注册成多例了。如果WebSocketHandler上用Scope(prototype)每个连接进来都是新实例那afterConnectionEstablished里调用 Session 池的代码没问题但业务侧调用sendToUser时用的池是同一个静态池理论上也不冲突。真正常见的冲突点是连接建立后没往池里放或者放进去的 session 已经关闭了。排查方法很简单在sendToUser里先判断session.isOpen()再做发送发送失败后立即从池里移除。上面代码里我已经这么写了如果你自己排查可以先看池里的 session 数量和实际在线人数是否一致不一致就是清理逻辑漏了。还有一种情况多个后端实例部署。如果你用 Nginx 负载均衡搭了两台后端服务用户 A 的连接挂在实例 1业务推送请求命中实例 2实例 2 的内存池里根本没有用户 A 的 session于是推送成功但实际消息丢了。这是单机内存 Session 池的天然限制。解决思路有两个方向一是简单粗暴把 WebSocket 的连接固定到某个实例上或者 Nginx 按 IP 哈希分配二是引入 Redis 的发布订阅业务推送发到 Redis 频道所有实例订阅这个频道谁内存里有这个 session 谁就负责推给客户端。后者是分布式场景的正路这里不展开完整实现但可以明确告诉你会遇到这个问题。5.3 页面刷新后连接没有及时清掉刷新页面时旧的 WebSocket 连接会关闭服务端afterConnectionClosed会被调用池里的 session 也会被移除。但有一种情况让人很头疼用户直接拔网线、电脑休眠、或者关掉笔记本盖TCP 连接并不会立刻感知网络断了服务端的afterConnectionClosed可能要等很久才触发。这也是我为什么要在handleTransportError里也做清理。除此之外Service 层推送时如果发现 session 已经关闭就顺手清掉池里的记录也是一种兜底。另外心跳是把双刃剑服务端也可以定期扫描池里的 session检查isOpen()关掉的直接移除防止内存慢慢被死连接占满。5.4 同用户多开页面的消息重复推送用户开两个标签页都登录了系统前后端都建立了 WebSocket。如果不做控制同一个业务推送会同时到两个页面用户看到两条一样的通知。我在addSession里用同用户只保留最新连接的办法处理了这个问题同一个userId的新连接建立后把之前的连接关掉。这样设计的取舍是用户在两台设备上同时登录时消息只会推到最后连接的那台设备另一台收不到。如果你的业务要求多个端同时收消息那就不能这样处理Session 池要从MapLong, WebSocketSession改成MapLong, ListWebSocketSession每条连接维护独立的 session。业务上怎么选要提前想清楚上线后再改代价会稍微大一点。5.5 服务端集群问题前面已经提到了集群部署下内存 Session 池的局限。我再补充一点实操建议如果是简单项目直接让 Nginx 对 WebSocket 连接做 IP 哈希就行因为同一个用户一般不会横跳两台服务器。但业务推送如果可能被调度到任意一台后端节点这个问题依旧存在除非所有推送请求也按同样的哈希策略打到同一条 WebSocket 连接所在的节点上。更工程化的做法是引入 Redis 消息广播业务模块推送消息时先把消息写入 Redis 的 pub/sub 频道所有后端实例订阅这个频道收到消息后在自己的内存 Session 池里查找目标用户谁找到了归谁推。这样处理之后集群节点数随便扩推送逻辑不用动。代价是维护多一个 Redis 订阅通道但对于已经用了 Redis 做缓存的项目来说成本很低。6. 集成后的一些实操体会整套集成走下来我最大的感受是WebSocket 本身不难难的是跟已有的鉴权体系、部署架构融合好。很多项目翻车都是翻在这些地方编码层面的 Handler 反而只是一小段代码。我习惯在项目里建立一套统一的消息推送规范字段包含 type、业务 ID、标题、内容、时间前端只要实现一个handlePushMessage统一处理这些结构后面任何新业务要推送后端直接往 Session 池发即可前端根本不用每次都改。另一个小习惯是把推送动作从业务逻辑里解耦出来比如发布公告后不要直接在 Service 里写一堆推送代码而是抽一个专门的推送服务这样以后要扩展消息类型、增加推送记录历史都不用动原有业务代码。最后劝一句做长连接功能一定要把心跳和断线重连当成必须实现的部分而不是可选项。上线之前最好模拟弱网环境测试一下把网线拔了再插上看看客户端能不能自动恢复、服务端池里有没有残留死连接。这些小细节提前处理好后面运维阶段能省掉大量半夜被叫起来处理用户收不到消息的麻烦。
RELATED

相关推荐

大模型微调如何选?Freeze、P-tuning、LoRA、QLoRA显存与效果全解析

大模型微调如何选?Freeze、P-tuning、LoRA、QLoRA显存与效果全解析

简介:这是一份聚焦大规模语言模型微调技术的PDF文档,共1个PDF文件,大小898KB,内容面向具备机器学习基础的研究人员和技术开发者,尤其适合算力有限但需要定制LLM的小型团队与个人。文档系统对比了Freeze、P-tuning V1/V…

📅 2026/10/17 16:35:03
异质图上的关系感知注意力:HetSANN原理与工程设计实践

异质图上的关系感知注意力:HetSANN原理与工程设计实践

做图神经网络做了几年,我最大的感触是:一到异质图,很多经典方法就开始失灵。明明GCN、GAT在同质图上表现稳定,可一旦图里有多类节点和多类边,要么把不同类型强行混在同一语义空间里算,要么依赖元路径人工指…

📅 2026/10/17 16:35:03
数据处理和存储系统建设方案:从采集到查询的完整链路设计

数据处理和存储系统建设方案:从采集到查询的完整链路设计

简介:这份《10数据处理和存储系统建设方案》面向智能化管控平台的项目规划人员、系统架构师及信息化建设从业者,围绕用户规模测算、数据计算、存储容量与传输带宽四大维度,给出可落地的资源估算与设备选型思路。方案以TPC-C基准推算数据库服务…

📅 2026/10/17 16:30:02
MORE NEWS

更多资讯

📰

CTF实战:Python实现数字水印提取与RC4解密全解析

1. 先说结论:这两道题为什么值得自己动手复现先交代一下背景。我今年备赛蓝桥杯网络安全赛的时候,把前几年的真题从头到尾过了一遍,其中“数字水印”和“RC4解密”这两道题让我印象最深。原因很简单:它们都属于“看起来高大上&…

📰

电商商品属性驱动架构:从PPT设计到可运行系统落地

简介:本资源是一份面向计算机专业本科生、电商系统开发初学者及课程设计实践者的学术型教学课件,聚焦“商品属性驱动的电子商务系统设计与优化”这一核心问题,系统阐述属性规范化、分类组织、精准搜索与个性化推荐四大设计原则及其实现路径。…

📰

蒙哥马利模乘原理与C语言实现:用移位代替除法加速RSA/ECC大整数运算

简介:《消除除法运算的蒙哥马利模乘》是一份经典算法文献,面向密码学、计算机数学方向的学生及工程开发者,尤其适合毕业设计阶段需要理解高效模乘原理的读者。蒙哥马利模乘通过引入与N互质的基数R,将模N乘法转换为无需试除的REDC运…

📰

MySQL查询结果加序号:窗口函数与用户变量的完整实践

MySQL查询结果加序号:从临时需求到窗口函数的完整实践干数据库开发的人,十有八九都遇到过这种需求:报表上要加一列序号,前端展示的时候要按顺序标上1、2、3……或者领导突然说“这个列表我要看排名”。接到这种需求时,…

📰

sqli-lab 38关之后:堆叠注入与ORDER BY注入实战拆解

从 Less-38 开始,sqli-lab 给我的感觉一下子就变了。前 37 关基本是在一条单查询里折腾:联合注入、报错、盲注、宽字节,方法没变,变的是过滤和闭合。到了 38 关,注入点后面第一次出现了分号。这个分号,让注…

📰

结构监测中频响函数漂移与时变结构分析:从异常数据到实用方法

做结构监测这些年,我遇到过不少一开始让人怀疑人生的数据。有一回在厂区设备基座上做振动测试,同样的测点、同样的设备、同样的激励方式,凌晨记录到的第一阶固有频率是6.85Hz,到了下午,同一测点居然变成了7.02Hz。一开…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬