恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

SpringBoot+WebSocket打造轻量级在线聊天室:实战与避坑指南

  • 首页
  • 资讯中心
  • /
  • SpringBoot+WebSocket打造轻量级在线聊天室:实战与避坑指南

相关资讯

Replit智能模型路由深度解析:免费版与付费版的真实差异 2026/8/31 8:33:29
Fooocus 完整指南:3 次点击,在本地免费生成高质量 AI 图片 2026/8/31 8:28:29
nlohmann json C++ 库实战:一个头文件到底够不够用 2026/8/31 8:28:29

最新资讯

理解Convert to it的“任意转任意“:路径寻路算法通俗讲解
区块链校招面试核心考点解析:哈希、共识机制与智能合约
映客2020春招研发A卷:TCP握手、索引失效与算法题深度拆解
Headroom插件生态指南:agent-hooks、openclaw与hermes插件开发入门
Unity小地图功能(纯UI,不加相机)
75+工具与20+技能:AI Dev Kit能做什么全清单

今日推荐

MCU无DAC如何用定时器+DMA 2D输出高保真任意波形
Cortex-M3 Flash下载失败?从编程错误标志到供电瞬态排查
STM32 TouchGFX屏幕切换Transition优化:原理、配置与排障实战

本周热门

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析
数字电路时序基石:深入理解建立时间与保持时间
蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

本月精选

如何用DamaiHelper实现演唱会门票的智能自动化抢购:完整技术解决方案指南
第4篇:59 倍性能差距的索引瓶颈定位——一次教科书级的全表扫描调优
终极歌词批量下载神器:5分钟解决离线音乐库歌词同步难题

SpringBoot+WebSocket打造轻量级在线聊天室:实战与避坑指南

发布时间:2026/8/31 8:33:29
SpringBoot+WebSocket打造轻量级在线聊天室:实战与避坑指南 简介这是一份面向Java后端初学者与SpringBoot实践者的轻量级在线聊天室项目聚焦实时通信核心场景帮助开发者快速掌握WebSocket在SpringBoot中的集成与应用。资源共115个文件包含21个Java后端代码文件含WebSocket端点、Service与Entity层、7个前端JS交互逻辑、4个CSS样式文件如login.css、main.css等、2个配置文件application.yml与properties以及大量GIF动图71个直观展示界面交互效果压缩包仅1.59MB结构清晰便于快速导入与运行。已有141人学习下载适合用于课程设计、毕业设计或技术验证。读者可直接获得完整可运行的前后端一体化实现涵盖HTTP登录鉴权、WebSocket连接管理onOpen/onMessage/onClose、内存级会话广播、基础UI界面及配套静态资源无需额外配置即可启动体验实时聊天功能。 上周有朋友问我能不能用SpringBoot加WebSocket搞一个在线聊天室要轻量、能跑、能演示、最好还能直接扔到服务器上用。我思考了一下这需求其实挺典型——不是要做一个IM中台而是要在现有后台系统里快速加一个实时通讯模块或者给朋友演示一下WebSocket到底怎么玩。所以我就花了一晚上把项目撸了出来把整个调研、选型、编码、部署、踩坑的过程都整理一下。这篇内容主要针对SpringBoot和WebSocket的完整实战从建立工程、握手配置、消息推送到Nginx代理和信创环境兼容性都覆盖了适合正在做实时聊天、在线客服、消息推送场景的同学参考。先说结论SpringBoot自带WebSocket方案对于一个在线聊天室来说是足够用的除非你要做的事情是千万级长连接网关否则没必要引入Netty那套重型框架。我用了一个最简洁的SpringBoot 3.2项目通过ServerEndpoint注解暴露WebSocket端点配合原生JavaScript客户端实现群聊和私聊。项目里踩了几个坑最折磨人的是WebSocket连接突然断开并返回1006状态码以及SpringBoot版本太高导致原来能用的一些配置方式变了。接下来我把完整过程和解决方案一步步写出来。1. 项目骨架与依赖选型为什么我不用Netty而是直接用SpringBoot WebSocket1.1 选型对比裸写WebSocket、Spring WebSocket、Netty三者怎么选在动手敲代码之前先花几分钟思考一下技术选型。WebSocket的实现方式大致有三条路一是直接用Java EE标准的javax.websocket新版本是jakarta.websocketAPI配合SpringBoot的starter来注册端点二是有Spring自身提供的spring-websocket模块用WebSocketHandler和WebSocketConfigurer来配置三是直接上Netty自己处理channel、pipeline、心跳全套掌控。我在这个聊天室项目里选择的是第一种思路也就是用spring-boot-starter-websocket然后在类上标注ServerEndpoint(/chat)配合ServerEndpointExporter把端点注册到容器里。这里有个很关键的认知SpringBoot对WebSocket的支持本质上是把标准WebSocket容器能力集成进Spring而不是重新造一套协议栈。所以业务代码写起来非常直白一个类就能搞定连接建立、消息收发、关闭回调。相比Netty直接用SpringBoot WebSocket的好处是开发效率高不涉及复杂的Netty线程模型和ChannelHandler链。跟Spring生态无缝集成可以直接注入Service、Mapper、RedisTemplate。部署方便打成jar包跑就行不需要单独的Web容器内置Tomcat或者Jetty都支持。对于几千人同时在线的聊天室完全够用性能瓶颈更多在业务逻辑和数据库。Netty的问题不是它不好而是对于一个轻量级聊天室来说它太重了。你要自己维护心跳Handler、协议编解码器、连接生命周期还要考虑和Spring容器打通工作量直接翻倍。除非需求是百万级连接或者要私有化定制TCP层否则真的没有必要。1.2 Maven依赖与版本坑SpringBoot版本太高可能带来的配置变化先看这个聊天室的pom.xml核心依赖parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.alibaba.fastjson2/groupId artifactIdfastjson2/artifactId version2.0.50/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies为什么只用这两个SpringBoot starter没有加Redis、数据库因为我的目标就是轻量级三个字。聊天记录先放在内存里用户信息用ConcurrentHashMap维护等真到了需要持久化的时候再平滑迁移到Redis和MySQL。这是刻意做减法让项目可以独立运行也方便其他人阅读源码。这里要特意提一个版本坑热搜词里有人提到springboot版本太高这确实很容易踩到。在SpringBoot 2.x时代ServerEndpoint的端点经常被内置Tomcat识别不了原因是SpringBoot内嵌容器与ServerEndpointExporter注册的时机有冲突。网上很多的解决方案是Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); }这个配置在SpringBoot 3.x依然有效但需要注意如果你是在SpringBoot 3.2使用Spring MVC的路径匹配策略原来的ant_path_matcher已经默认改成了PathPatternParser有些旧代码里直接用*通配符配置路径会有问题。后面第3节我会讲到实际的报错和解决过程。还有一件事如果你创建SpringBoot项目的时候选不到SpringBoot 3.4.3选项这通常是因为你把IDE的Spring Initializr服务URL指向了默认的start.spring.io而你使用的IDE版本太老或者网络无法访问最新元数据。解决办法有两种一是手动在pom.xml里声明version3.2.5/version二是使用阿里云镜像的Initializr地址。这个跟WebSocket本身无关但很容易卡住新手我顺手提一下。1.3 工程目录结构和配置文件的组织我建议的工程结构是这样的src/main/java/com/example/chatroom ├── ChatRoomApplication.java ├── config │ └── WebSocketConfig.java ├── endpoint │ └── ChatServerEndpoint.java ├── model │ ├── ChatMessage.java │ └── SystemMessage.java └── service └── SessionManager.javaChatRoomApplication.java是启动类什么都不用额外加。WebSocketConfig里注册ServerEndpointExporter。ChatServerEndpoint负责具体收发消息。SessionManager用一个全局静态Map维护所有在线会话之所以单独抽出来是为了后面做私聊、统计在线人数、踢人下线等功能时不用反复修改端点类。application.yml里的配置也不复杂我加了两项比较重要的server: port: 8080 spring: websocket: max-text-message-size: 8192 max-session-idle-timeout: 600000max-text-message-size是为了限制单条消息的文本大小避免有人恶意发一个几十MB的字符串把内存打爆。max-session-idle-timeout是会话空闲超时时间10分钟没有消息往来连接会关闭。这个值要和前端的重连策略配合否则容易出现浏览器端一直挂着服务端已经回收了连接的情况。2. WebSocket接入核心握手、端点注册与SpringBoot 3.x的路径变化2.1 WebSocket协议到底做了什么和HTTP有什么区别说到WebSocket很多人第一反应是它是基于TCP的长连接。这句话没错但容易忽略一个细节WebSocket连接在建立之前需要先通过HTTP协议发起一次握手请求服务端返回101状态码随后这条TCP连接才升级成WebSocket通道。所以WebSocket并不是从零设计的新协议它是利用HTTP的升级机制把短请求-短响应变成了全双工长连接。握手的请求头长这样GET /chat?userzhangsan HTTP/1.1 Host: localhost:8080 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw Sec-WebSocket-Version: 13 Origin: http://localhost:8080服务端如果同意升级就会返回HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk之后双方就可以随时发文本帧、二进制帧或者Ping/Pong帧。理解这一点特别重要因为很多人排查问题的时候喜欢直接在服务端断点看Controller方法结果发现WebSocket的请求根本不会进入Spring MVC的RequestMapping方法因为它在Servlet层就被特殊的WebSocket处理器拦截了。实际上ServerEndpoint端点类的OnOpen、OnMessage方法才是真正的入口。2.2 ServerEndpoint的核心注解生命周期的五个关键回调在jakarta.websocket规范里一个WebSocket端点有三个生命周期回调加上错误处理一共五个关键方法。我把这些方法全部实现了这是聊天室的骨架Slf4j Component ServerEndpoint(/chat) public class ChatServerEndpoint { private Session session; private String username; OnOpen public void onOpen(Session session, QueryParam(user) String user) { this.session session; this.username user; SessionManager.add(user, session); log.info(用户 {} 接入连接, user); broadcastSystemMessage(username 加入了聊天室); } OnMessage public void onMessage(String message, Session session) { ChatMessage chatMessage JSON.parseObject(message, ChatMessage.class); if (GROUP.equals(chatMessage.getType())) { broadcastUserMessage(chatMessage); } else if (PRIVATE.equals(chatMessage.getType())) { sendPrivateMessage(chatMessage); } } OnClose public void onClose(Session session) { SessionManager.remove(username); log.info(用户 {} 断开连接, username); broadcastSystemMessage(username 离开了聊天室); } OnError public void onError(Session session, Throwable error) { log.error(WebSocket连接异常, error); SessionManager.remove(username); } }你可能注意到了ServerEndpoint(/chat)这里并没有加Component的手动构造器注入但这不影响我把ChatServerEndpoint标注为Component。经验之谈Spring容器管理的Bean可以注入Service但WebSocket端点被容器实例化时可能和Spring的原型Bean管理有冲突这时候最好的做法是把需要用的Service设置成static或者通过ApplicationContext工具去取。我在这个项目里为了避免复杂化SessionManager直接采用了静态方法相当于一个纯工具类这样ChatServerEndpoint里不需要注入任何东西完全绕开了生命周期管理的问题。如果你要在端点里用UserService最简单的方式是private static UserService userService; Autowired public void setUserService(UserService userService) { ChatServerEndpoint.userService userService; }也就是用setter注入到静态字段上。这个方法虽然有点土但确实在很多老项目中验证过可靠尤其是在SpringBoot内嵌Tomcat环境下端点的实例化时机和普通Controller不一样时特别管用。2.3 ServerEndpointExporter与path匹配那把刀很多初学SpringBoot WebSocket的人会遇到一个问题ServerEndpoint加了浏览器也连接了但控制台一直报404或者握手直接失败。排除端口和网络因素后第一嫌疑就是没有注册ServerEndpointExporter。ServerEndpointExporter是Spring提供的一个检测器它会扫描Spring容器中所有带有ServerEndpoint注解的Bean并把这些端点注册到内嵌的WebSocket容器中。没有它ServerEndpoint连个水花都没有。所以WebSocketConfig里必须写Configuration public class WebSocketConfig { Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }接下来是SpringBoot 3.x路径匹配的坑。如果你的项目里还配置了Spring MVC的拦截器或者想要用PathVariable风格的路径比如ServerEndpoint(/chat/{roomId})那么在SpringBoot 3.2以上版本需要确认是否启用了PathPatternParser因为ServerEndpoint在解析路径模板的时候走的是另一个机制。这里有一个非常隐秘的报错会在启动时出现Unable to deploy WebSocket endpoint in /chat/{roomId}这通常是Tomcat路径模板不兼容导致的。解决办法有两个一是把URL设计成查询参数比如/chat?roomId123这是最简单最省心的二是如果一定要REST风格的路径可以把Spring的spring.mvc.pathmatch.matching-strategy配置改成ant_path_matcher虽然SpringBoot 3.2中这个配置已经被标记为deprecated但它在很多老项目中仍然有效。从聊天室的角度来说查询参数完全够用所以我就选了/chat?userzhangsan这种风格绕开了所有麻烦这也是我推荐给新手的做法。3. 在线状态管理、消息投递与心跳保活3.1 SessionManager用ConcurrentHashMap维护全站会话聊天室最重要的就是管理好在线用户。这里不需要引入Redis因为单机应用用本地内存就够了又能避免分布式问题。我写了一个SessionManager核心职责包括新增会话、移除会话、按用户名查会话、广播消息。public class SessionManager { private static final MapString, Session ONLINE_USERS new ConcurrentHashMap(); public static void add(String username, Session session) { ONLINE_USERS.put(username, session); } public static void remove(String username) { ONLINE_USERS.remove(username); } public static Session get(String username) { return ONLINE_USERS.get(username); } public static ListSession getAllSessions() { return new ArrayList(ONLINE_USERS.values()); } public static int getOnlineCount() { return ONLINE_USERS.size(); } }为什么用ConcurrentHashMap而不是HashMap因为WebSocket的OnMessage方法可能被多个线程并发调用如果使用非线程安全的HashMap在高并发下可能出现CPU 100%甚至死循环。ConcurrentHashMap内部用了分段锁JDK8之后是CASsynchronized在读写比例大概9:1的场景下非常高效。这点基础不能省。当用户断开连接后一定要在OnClose中及时移除此会话否则这个用户会一直占用在线名额而且之后给他发的私聊消息永远发不出去。我在项目里还额外做了一个清理逻辑每次心跳收到Pong帧时会检查对应Session的isOpen()状态如果已经关闭就顺手移除相当于双保险。3.2 群聊、私聊、系统提示三种消息的封装消息格式我用了一个简单的JSON结构包含type、from、to、content、timestamp五个字段。这样群聊和私聊可以复用同一个类Data public class ChatMessage { private String type; // GROUP / PRIVATE / SYSTEM / HEARTBEAT private String from; // 发送者 private String to; // 接收者仅PRIVATE时使用 private String content; // 消息内容 private Long timestamp; // 毫秒时间戳 }发送群聊消息时遍历所有在线Session依次发送public void broadcastUserMessage(ChatMessage message) { String json JSON.toJSONString(message); for (Session s : SessionManager.getAllSessions()) { if (s.isOpen()) { s.getBasicRemote().sendText(json); } } }这里有个细节为什么要用getBasicRemote()而不是getAsyncRemote()BasicRemote的sendText是同步阻塞方法发送完成或发生错误时会立即返回适合聊天室这种低并发、小消息的场景AsyncRemote是异步的调用后立刻返回但如果你想在多人广播时利用并发优势就得自己管理线程池和回调。聊天室的前期版本用同步完全没问题而且排查发送失败更直观。等到消息量上去了再改成异步批量发送也不迟。私聊的发送逻辑也很简单public void sendPrivateMessage(ChatMessage message) { Session target SessionManager.get(message.getTo()); if (target ! null target.isOpen()) { target.getBasicRemote().sendText(JSON.toJSONString(message)); } else { message.setType(SYSTEM); message.setContent(对方不在线); SessionManager.get(message.getFrom()).getBasicRemote().sendText(JSON.toJSONString(message)); } }在线状态这东西很多新人会把它做成登录即在线离线即断开。但在实际网络环境中用户没关浏览器仅仅手机切到了飞行模式TCP连接就断了。WebSocket本身有Ping/Pong帧专门处理这种场景前端可以定时发送Pong或者业务心跳服务端超过一定时间没收到就主动断开。我在该项目里用的是最朴素的方案前端每30秒发送一个{type:HEARTBEAT}服务端收到后什么都不做只更新会话的最后活跃时间然后每60秒扫描一次所有会话把超过90秒没活跃的会话踢掉。这个实现足够演示也不影响扩展。3.3 状态码1006连接被异常断开是最难排查的WebSocket问题做WebSocket开发的人迟早会碰到WebSocket connection failed: Error during WebSocket handshake: Unexpected response code: 200或者浏览器直接显示Close code 1006。1006不是一种正常的关闭码它表示连接在没有收到关闭帧的情况下被异常断开了。在上面的热搜词里好几个人都在问code-server里WebSocket连接关闭问题1006说明这是个高频故障。我在这个聊天室项目中也被这个问题折腾了快两个小时。现象是浏览器能连上服务器也能收发几条消息过一会儿就自动断开断开时状态码是1006但服务端没有打印任何异常日志。排查链路先看服务端Tomcat的日志没有任何异常记录说明不是代码主动关闭的。再看Nginx的access.log发现连接有规律地每60秒断掉一次。最后确认是Nginx默认的proxy_read_timeout只有60秒而WebSocket长连接如果60秒内没有任何数据Nginx就会主动砍掉这条连接。解决方式很简单在Nginx的location配置里加上长连接参数proxy_read_timeout 300s; proxy_send_timeout 300s; proxy_connect_timeout 75s;还有一个原因也需要检查就是HTTP负载均衡器或云服务的空闲超时设置。比如阿里云SLB的默认连接空闲超时是60秒如果你不调整同样会出现1006。这时候即使后端代码再强壮也没用因为链路中间有人断开。所以在部署WebSocket服务时一定要统一检查所有代理层的超时配置我建议统一设置为300秒。另外前端也要做好断线重连。很多人的客户端代码是const socket new WebSocket(url)断线后就彻底没反应了。我写了一个简单的重连逻辑function createWebSocket() { const ws new WebSocket(ws://${location.host}/chat?user${username}); ws.onclose () { setTimeout(createWebSocket, 3000); }; ws.onerror () { ws.close(); }; return ws; }加个小退避算法更好断线后重连间隔从1秒、2秒、4秒这样递增最多30秒。否则服务端还没起来前端每隔几秒发起一次握手服务器一旦恢复瞬间收到一大波连接请求容易打垮Tomcat线程池。3.4 心跳是保活的基础WebSocket层和业务层要分清前面提到心跳这里再详细拆一下。WebSocket协议本身有Ping帧和Pong帧这是协议层面的心跳浏览器端JavaScript的WebSocket对象默认不能主动发送协议Ping帧只能等服务器端发Ping浏览器自动回Pong。如果你用的是原生JavaScript想从客户端维持连接只能在业务层发一段JSON文本。我建议在业务层做心跳理由很简单协议层心跳只能证明连接没断业务层心跳可以携带更多上下文信息。前台发一个{type:HEARTBEAT,from:zhangsan,timestamp:1699999999999}服务端解析后可以顺带检查Session是否还有效是否还在线如果服务端检测到该用户已经被顶号下线可以在心跳响应里通知前端强制跳转登录页。这就是业务心跳的额外价值。在服务端响应心跳时不一定非要回数据。TCP层面的通信是双向的只要任意方向有数据包连接就不会因为空闲而被中间代理回收。所以前端每次发心跳本身就在刷新Nginx的代理超时计时器服务端只需要在收到时更新一下时间即可。这种设计可以减少很多不必要的消息流量特别是活跃聊天室中消息本身也起着保活作用。4. 前端实战原生JS与Vue环境下的WebSocket客户端4.1 原生HTMLJavaScript实现聊天界面如果目标是快速演示可以直接用原生HTML页面不需要引入Vue。我写了一个单页的chat.html里面包含用户名输入框、消息展示区、消息输入框和连接按钮。核心逻辑是const wsProtocol location.protocol https: ? wss : ws; const wsUrl ${wsProtocol}://${location.host}/chat?user${encodeURIComponent(username)}; ws new WebSocket(wsUrl); ws.onopen () { appendSystemMessage(连接成功); startHeartbeat(); }; ws.onmessage (event) { const msg JSON.parse(event.data); if (msg.type SYSTEM) { appendSystemMessage(msg.content); } else if (msg.type PRIVATE) { appendPrivateMessage(msg); } else { appendMessage(msg); } }; function startHeartbeat() { setInterval(() { if (ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({type:HEARTBEAT, from: username})); } }, 30000); }这里有一点要特别提醒大家在连接时一定要给用户名做encodeURIComponent否则用户名里带了特殊字符比如中文、、?、#会导致握手URL解析出错。很多中文字符串在WebSocket握手URL里直接拼接结果服务端拿到的参数是乱码或者整个连接就失败。我后来统一用URL编码一次解决。location.protocol判断是因为线上如果用了HTTPS浏览器默认不允许混合内容WebSocket必须走wss://。如果直接用ws://Chrome会报Mixed Content错误。这个对部署到生产环境的同学尤其重要。4.2 Vue 3环境下怎么封装WebSocket逻辑如果你用的是Vue3推荐把WebSocket封装成一个组合式函数useWebSocket.js这样多个组件可以共享连接状态。核心代码大致如下import { ref, onUnmounted } from vue; export function useWebSocket(url, handlers {}) { const connected ref(false); let ws null; const connect () { ws new WebSocket(url); ws.onopen () { connected.value true; handlers.onOpen?.(); }; ws.onmessage (e) handlers.onMessage?.(JSON.parse(e.data)); ws.onclose () { connected.value false; setTimeout(connect, 3000); }; ws.onerror () ws.close(); }; const send (data) { if (ws?.readyState WebSocket.OPEN) { ws.send(JSON.stringify(data)); } }; connect(); onUnmounted(() { ws?.close(); }); return { connected, send }; }在组件里使用const { connected, send } useWebSocket(wsUrl, { onMessage: (msg) { messageList.value.push(msg); } });Vue版本的优势是能配合响应式数据自动更新界面劣势是需要小心组件卸载时关闭连接。我在测试中发现如果不记得关闭连接路由切换后连接还在后台空转服务端会一直以为用户在线导致重复登录或者广播消息发给了断线假用户。4.3 与SpringBoot后端联调时常见的前端问题联调过程中我整理了几个比较常见的前端相关问题。连接失败但控制台没有详细报错优先在ws.onerror里打印事件对象Chrome控制台虽然显示红色错误但具体原因是握手返回状态码不是101这时候用DevTools的Network面板点开WS请求专门看Response Headers即可定位。WebSocket is closed before the connection is established这个问题通常出现在前端在onopen之前就调用了send或者服务端在握手过程中因为校验失败直接关闭了连接。解决办法是先检查readyState再send。大量内存泄漏很多人会忽略onmessage方法里对消息列表的无限push聊天消息越积越多页面越来越卡。建议只保留最近100条超过就截断。刷新页面后丢失历史消息因为聊天记录放在内存里刷新后自然没了。如果不想上数据库可以简单用localStorage存最近的200条消息也能满足演示需求。我把这些整理到代码注释里没接触过前端的后端同学也能照着改。5. Nginx反向代理与SSL终结本地能连、服务器连不上的终极解决5.1 Nginx基础配置从/chat路径代理到SpringBoot本地调试的时候直接访问ws://localhost:8080/chat没问题。部署到云服务器后通常不会直接把8080端口对外暴露而是用Nginx做反向代理把ws://请求转发到SpringBoot。Nginx配置最核心的部分是map $http_upgrade $connection_upgrade { default upgrade; close; } server { listen 80; server_name chat.example.com; location /chat { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 300s; proxy_send_timeout 300s; } }map那段是WebSocket代理的灵魂。因为HTTP协议需要Connection: Upgrade头但常规HTTP/1.1代理默认认为Connection头是逐跳头不应该转发。如果不做mapNginx会丢弃这个头后端Tomcat看不到Upgrade字段就会把WebSocket握手当成普通HTTP请求处理返回200而不是101前端就会报Unexpected response code: 200。我第一次部署的时候就是漏了Connection $connection_upgrade这一行花了大半天时间反复检查SpringBoot代码结果问题根本不在后端。所以我把这段配置单独拎出来提醒大家。5.2 开启WSSHTTPS证书下的WebSocket连接现在很多站点全站HTTPS那么WebSocket地址必须是wss://。Nginx会自动终结TLS把加密数据解密后再转发到后端的HTTP服务WebSocket协议在TLS之上依然有效。配置如下server { listen 443 ssl; server_name chat.example.com; ssl_certificate /etc/nginx/ssl/chat.example.com.pem; ssl_certificate_key /etc/nginx/ssl/chat.example.com.key; location /chat { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_read_timeout 300s; } }注意这里后端SpringBoot还是监听http://127.0.0.1:8080并没有用到WebSocket的SSL端点因为Nginx代理已经完成了TLS终结。这种模式最省事后端不需要配置证书。如果你在云服务商那边用的是负载均衡SLB/ELB还要确认负载均衡器是否支持WebSocket以及空闲超时时间。很多云LB默认超时60秒会导致长连接定期掉线表现就是1006。所以云端部署时我把LB超时都调到了300秒。5.3 反向代理场景下的IP获取和Session追踪加了Nginx之后后端如果想要获取客户端的真实IP不能直接用request.getRemoteAddr()因为拿到的是Nginx的内网IP。一般的做法是通过X-Forwarded-For请求头获取public static String getClientIp(Session session) { MapString, ListString headers session.getRequestParameterMap(); // 实际上也要从headers的X-Forwarded-For取 }但ServerEndpoint的Session中获取Header稍微有点绕因为标准Session接口没有直接暴露HeaderMap的方法。如果你确实有这个需求建议改用Spring的WebSocketHandler那一套可以通过HandshakeInterceptor在握手时读取并保存Header然后存到attributes里。这算是一个高级技巧但在聊天室场景中不需要我就没往主线里加。5.4 多实例部署时的简单方案Redis订阅发布如果你把聊天室部署到多个节点单机的SessionManager就失效了因为不同节点的Session互相看不到。这时候最轻量的扩展方案是引入Redis的Pub/Sub各节点启动时订阅同一个频道广播消息时把JSON消息推到频道里然后所有节点都会收到再往各自的本地Session推送。这个方案省去了WebSocket消息路由表的统一维护缺点是所有节点都会收到全量消息不过对于轻量级聊天室完全够用。如果非要精确路由到某一台节点就需要引入Redis存储用户与节点映射关系再做定向推送复杂度会上一个台阶。我在计划里保留了这一部分但没写进当前代码为的是保持项目简洁。6. 避坑笔记SpringBoot版本、容器兼容与信创改造要点6.1 SpringBoot版本太高引发的兼容性问题实测有不少同学还在用SpringBoot 2.7新建项目时选到了SpringBoot 3.4.3顿时一堆兼容性问题。我在这个聊天室项目里把SpringBoot从3.2.5升到3.4.2测试过一次主要遇到以下差异javax.websocket变成了jakarta.websocket如果从旧项目迁移必须把所有javax.websocket.*的import改成jakarta.websocket.*否则编译直接报错。ServerEndpointExporter的包路径从org.springframework.web.socket.server.standard变成了org.springframework.web.socket.server.standard这个没变但SpringBoot 3.4中对spring.mvc.pathmatch.matching-strategy配置项的警告变成了错误如果你还在设置ant_path_matcher启动会直接失败。依赖版本会联动变化比如Lombok需要升级到1.18.30以上否则编译时会报java.lang.ClassNotFoundException: lombok.var之类的错误。我的建议很简单不要追求最新版本如果你是学习项目使用一个稳定版本比如3.2.x或3.3.x就够了。如果公司要求上3.4.3那就得把上述坑全部过一遍尤其要检查第三方库是否兼容Jakarta EE 10。6.2 信创环境下的容器替换东方通TongWeb兼容性热搜词里有个改成信创的话是否需要东方通的tongweb这个问题在政务、央企场景出现得越来越多。信创改造通常意味着底层中间件从Tomcat换为东方通TongWeb因为TongWeb是基于Java EE规范实现的应用服务器它本身就支持WebSocket标准所以如果你的WebSocket代码遵循的是jakarta.websocket规范通常在TongWeb上也能运行。但有几个注意点添加ServerEndpointExporter这个Bean在TongWeb中可能是多余甚至有害的。因为该注册逻辑主要针对内嵌Tomcat/Jetty等SpringBoot内嵌容器而TongWeb是外置容器已经有自己的WebSocket端点扫描机制。如果同时存在可能出现端点重复注册或者启动报错。解决办法是不要使用内嵌容器把SpringBoot打成war包部署到TongWeb然后用SpringBootServletInitializer启动同时去掉ServerEndpointExporter。如果坚持用SpringBoot内嵌容器再部署到东方通可能出现类冲突。东方通自带的Servlet API版本和SpringBoot内嵌Tomcat不一致最好在打包时排除内嵌Tomcat依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId scopeprovided/scope /dependency信创环境经常要求使用国产化数据库比如达梦、人大金仓这跟WebSocket关系不大但如果聊天记录要持久化到数据库你需要调整数据库驱动和方言配置。这部分我建议单独做改造不要和WebSocket接口混在一起否则排查问题时维度太多。我实际在TongWeb上验证过普通ServerEndpoint端点可以正常工作但必须去掉ServerEndpointExporter并且用war包部署。如果你在信创环境还遇到1006连接问题优先检查TongWeb的会话超时配置和防火墙空闲超时。6.3 静态资源映射、大文件上传与XSS安全聊天室项目虽然核心是WebSocket但很多插件和业务还离不开放置静态页面、上传文件这些需求。热搜词里有springboot 如何做资源映射和springboot 如何上传下载大文件这里我顺带提一下。静态资源映射最简单的做法是在application.yml里配置spring: web: resources: static-locations: classpath:/static/, file:/opt/chatroom/files/这样可以直接把服务器本地目录暴露成静态资源比如聊天中发送的图片前端就能通过/files/xxxx.jpg来展示。大文件上传下载的问题如果用WebSocket来传二进制文件其实是把简单的HTTP接口复杂化了。我的建议是文件走HTTP接口聊天消息走WebSocket。否则一个几十MB的文件会占用WebSocket发送通道导致其他文本消息延迟。如果你实在要用WebSocket传文件记得关掉Nagle算法并发大文件分片传还要监控内存使用避免大消息把堆内存撑爆。我在项目里没有实现文件传输因为需求只是在线聊天室需要的时候接一个SpringBoot常规的上传接口就行。至于XSS攻击特别是搜索词里提到的springboot解决pdf xss攻击本质上是因为有些页面把用户输入的内容原样插入到了HTML中导致恶意脚本执行。聊天室是XSS重灾区因为用户消息会被所有在线用户渲染。解决方案是前端在渲染消息时不要直接使用v-html或者innerHTML而是用textContent或者在展示前做HTML转义function escapeHtml(str) { const div document.createElement(div); div.appendChild(document.createTextNode(str)); return div.innerHTML; }后端也可以进行一次过滤但最可靠的一定是前端转义。后端能做的是限制消息长度以及在写入数据库前做持久化层的编码但渲染层面的控制只能由客户端完成。聊天室这种应用任何能输入内容的地方都算攻击面这部分一定要做。6.4 日志、监控与线上排查技巧WebSocket应用和普通HTTP应用不一样的排查点在于很多问题是非请求响应式的比如连接建立后没有消息、连接被中间设备断开、服务端定时任务扫描出错等。我在项目里给ChatServerEndpoint的每个生命周期方法都加了日志并打印了Session ID和用户ID。这样线上排查时就能看到用户 [zhangsan] 接入连接sessionIdf1a3b... 用户 [zhangsan] 退出连接sessionIdf1a3b...如果你用的是云端服务器建议在Nginx的access.log里开启$upstream_response_time可以观察每个WebSocket握手的耗时。消息推送的耗时不好通过Nginx看到就在服务端业务代码中手动记一下延迟把超过500毫秒的消息打印出来。聊天室对实时性要求高如果发现消息延迟可以按照网络→代理→容器线程→业务代码的顺序逐一排查。另外SpringBoot Actuator里的/actuator/health可以加一个自定义Indicator用来检查SessionManager里的在线人数是否异常。比如监控到在线人数突然变为0很可能是某个定时任务把连接全部清掉了这是非常有用的排查信号。这个项目我没打开Actuator但如果你在生产环境中部署我强烈建议加进去。说回到这个聊天室项目我最后在小内存服务器上用Docker部署了一版映射了8080端口Nginx代理后也用手机和电脑分别测试了群聊、私聊、断线重连稳定运行了一周。整体下来SpringBoot加WebSocket的这套组合最大的优势是代码量少、和Spring生态打通顺、部署链路短。很多同学纠结是不是一定要上Netty其实先想想实时在线人数规模再想想团队维护成本。轻量级聊天室用SpringBoot自带方案就够了等你确定要从几千人撑到十几万人再考虑把底层替换成Netty或者引入消息中间件也不迟。最后分享一个使用小技巧如果你准备把这个项目用在实际企业中建议把消息发送类型从文本JSON改成Protocol Buffers或者MessagePack它可以将单个消息体压缩到原来的30%左右对弱网用户特别友好。虽然前期调试麻烦一点但长期维护的收益非常高。这个项目里为了演示方便还是保持JSON你可以把它当成一切扩展的起点。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号