结论是:当连接数超3000、qps破千时,spring boot原生@serverendpoint易因线程阻塞、内存抖动和gc频繁而瓶颈,netty的reactor模型与零拷贝机制才是高并发实时通信的底层支撑。

直接说结论:用 Netty 实现 WebSocket 服务端,不是为了“炫技”,而是当连接数超过 3000、消息吞吐量每秒破千时,Spring Boot 原生 @ServerEndpoint 很容易成为瓶颈——线程阻塞、内存抖动、GC 频繁,甚至偶发连接假死。Netty 的 Reactor 模型和零拷贝机制才是高并发实时通信的底层支撑。
为什么不能直接用 Spring Boot 的 WebSocket 支持
Spring Boot 内置的 WebSocket 默认走 Tomcat/Undertow 容器,本质仍是基于 Servlet 容器的阻塞式 I/O 封装:
-
HttpServerCodec和WebSocketServerProtocolHandler这类 Netty 原生组件,在 Spring 的@ServerEndpoint里是不可见、不可替换的 - 每个连接默认绑定一个线程(尤其在 Tomcat 中),5000 连接 ≈ 5000 线程,光上下文切换就吃掉大量 CPU
- 无法精细控制心跳超时、帧压缩、SSL 握手重试等关键参数,出问题只能看日志猜
- 集群场景下,
ConcurrentHashMap<string session></string>这种本地会话缓存根本没法跨实例同步
Netty WebSocket 服务端的核心 Pipeline 配置
真正起作用的是 ChannelPipeline 中那几个不可跳过的 Handler,顺序错了会直接握手失败或收不到帧:
- 必须先加
HttpServerCodec:处理 HTTP 升级请求(GET /ws HTTP/1.1 + Upgrade: websocket) - 紧跟着
HttpObjectAggregator(65536):把分片的 HTTP 请求头+body 聚合成完整FullHttpRequest,否则WebSocketServerProtocolHandler拿不到完整握手信息 -
WebSocketServerProtocolHandler("/ws")是核心:它完成协议升级、自动处理 Ping/Pong 帧、拦截非法路径,并把后续流量转为TextWebSocketFrame或BinaryWebSocketFrame - 自定义 Handler(如
WebSocketFrameHandler)必须放在它之后,否则收到的永远是原始HttpRequest,不是 WebSocket 帧
常见错误:漏掉 HttpObjectAggregator,结果客户端连得上但服务端收不到任何 TextWebSocketFrame,日志里只看到一堆 HttpRequest —— 这不是代码写错了,是 Pipeline 链断了。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
如何安全地广播消息并避免 ChannelInactiveException
Netty 的 Channel 生命周期极短,channel.writeAndFlush() 之前必须确认它还活着,否则抛 ChannelInactiveException,且不会自动重试:
- 永远不要在 Map 里长期缓存
Channel对象;改用ConcurrentMap<string weakreference>></string>或监听handlerRemoved事件及时清理 - 广播前逐个检查:
if (channel.isActive() && channel.isWritable()),缺一不可(isActive判连接状态,isWritable判写缓冲区是否满) - 对单个 Channel 发送失败时,不要
ctx.close(),而应捕获ExceptionEvent并记录 ID,让心跳检测自然淘汰异常连接 - 别用
channel.writeAndFlush(msg).sync():阻塞当前 EventLoop 线程,整个 Worker Group 都会卡住
生产环境必须调优的三个 Netty 参数
这些参数不写进代码,光靠默认值扛不住真实流量:
-
NioEventLoopGroup(4):Boss Group 通常设为 1,Worker Group 推荐设为 CPU 核数 × 2(4 核机器设 8),太少压不住连接,太多反而争抢锁 -
childOption(ChannelOption.SO_KEEPALIVE, true):开启 TCP Keepalive,防止 NAT 网关静默断连(尤其微信小程序长连场景) -
childOption(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 64 * 1024):写缓冲区水位从默认 64KB 提到 256KB,避免高频小消息触发channelWritabilityChanged频繁回调
最容易被忽略的是:WebSocket 心跳不是靠前端 setInterval 发 Ping,而是由 WebSocketServerProtocolHandler 自动响应 Pong,但前提是你的 Netty 版本 ≥ 4.1.90.Final(旧版有心跳超时后不关闭连接的 bug)。










