小程序与云端保持长连接实时接收充电状态,底层是 Netty。本文讲解服务端的 ChannelPipeline 如何装配、心跳机制如何保活、连接注册表如何实现按会话精准推送,以及优雅关闭的处理。
一、模块定位
charge-netty-server 是小程序实时通信通道,基于 Netty 4.1 实现 WebSocket 服务端。
- HTTP 端口 8085 | WebSocket 端口 8989(路径 /ws)
- 内部推送接口:POST /netty/push(带 X-Internal-Key 鉴权)
为什么用 Netty 不用 Spring WebSocket?
长连接场景需要精细控制 Pipeline(粘包/拆包、心跳、协议升级顺序),Netty 的 ChannelPipeline 机制比 Spring 封装的 WebSocket 更底层可控,也更利于理解网络编程原理。
二、模块结构(17 个类)
charge-netty-server/.../netty/server/
├── NettyApplication.java 启动类(@EnableAsync)
├── api/
│ ├── ChargeStatCacheApi.java GET /charge/stat/latest/{recordId} 读 Redis 快照
│ ├── NettyPushApi.java POST /netty/push 内部推送入口(X-Internal-Key)
│ └── SimulateApi.java 模拟设备/连接查询(/netty/simulate)
├── handlers/
│ ├── ChargeStatPushRegistry.java ★chargeId→Channel 注册表(139 行)
│ ├── JsonCmdHandler.java ★核心:JSON 指令处理(212 行)
│ ├── YeseesionServerHeartBeatHandler.java 心跳超时处理
│ ├── YeseesionWebSocketInboundHandler.java 入站处理
│ └── YeseesionWebSocketOutboundHandler.java 出站处理(⚠️空实现)
├── mq/
│ ├── ChargeCmdProducer.java RabbitMQ 生产者
│ └── RabbitConfig.java Topic 交换机
├── websocket/
│ ├── WebSocketServ.java ★Netty 服务端引导(161 行)
│ └── YeseesionChannelInit.java ★Pipeline 装配(92 行)
└── utils/HTTPUtils.java 已废弃的 OkHttp 封装(死代码)
三、核心:ChannelPipeline 装配(重点掌握)
YeseesionChannelInit.java 中的 Handler 顺序(必须记住为什么是这个顺序):
IdleStateHandler(readerIdle=60s, writerIdle=0, allIdle=90s)
→ YeseesionServerHeartBeatHandler
→ HttpServerCodec
→ HttpObjectAggregator(64 * 1024) // 64KB 聚合上限
→ WebSocketServerProtocolHandler("/ws")
→ YeseesionWebSocketInboundHandler
→ JsonCmdHandler // ★业务处理
→ YeseesionWebSocketOutboundHandler
每个 Handler 的作用与顺序理由
| Handler | 作用 | 为什么在这 |
|---|---|---|
| IdleStateHandler | 读 60s / 全 90s 空闲检测 | 必须最前:最先捕获空闲事件触发心跳/断连 |
| HeartBeatHandler | 空闲超时关闭连接 | 紧跟 IdleStateHandler 消费其事件 |
| HttpServerCodec | HTTP 编解码 | WebSocket 升级前的 HTTP 握手必须经过它 |
| HttpObjectAggregator | 聚合 HTTP 片段为完整请求 | 必须在 Codec 之后、协议升级之前 |
| WebSocketServerProtocolHandler | WebSocket 协议升级 | 握手完成后才进入 WS 帧模式 |
| Inbound/JsonCmd | 业务处理 START/STOP/RESUME | 协议升级完成后的业务层 |
原理 :Netty 的 Pipeline 是双向链表------入站事件 head→tail 顺序触发,出站事件 tail→head 触发。Handler 顺序错乱会导致类型不匹配(如未聚合收到的是碎片、未升级收到的是 HTTP 帧)。
四、连接注册表:ChargeStatPushRegistry(139 行)
核心设计
java
ConcurrentHashMap<String, ChannelHandlerContext> registry;
// String = chargeRecordId(充电记录 ID,Long 转 String 存入)
// ChannelHandlerContext = 对应小程序的 WebSocket 连接上下文
| 方法 | 作用 |
|---|---|
register(chargeRecordId, ctx) |
指令下发时注册(JsonCmdHandler 调用) |
unregister(chargeRecordId) |
连接关闭/充电结束移除 |
sendTo(chargeRecordId, msg) |
按 ID 精准推送给对应小程序 |
broadcast(msg) |
广播所有连接 |
为什么用 ConcurrentHashMap?
- 多线程场景:Netty 的 worker 线程可能并发操作注册表
- 读多写少:ConcurrentHashMap 的读无锁,写分段加锁
⚠️ 已知缺陷(值得关注)
注册表是单机内存态、无持久化:
- 多实例部署时各实例的 Map 不共享 → 设备数据分发到 Server-2 但小程序连的是 Server-1 → 推送丢失
- 解决方案:① Redis Pub/Sub 广播每条推送,各实例查本地 Map;② 一致性哈希让同一 chargeRecordId 的 WS 连接和推送路由到同一实例
五、心跳机制(重点掌握)
为什么需要心跳?
WebSocket 长连接在 NAT 网关/移动网络下可能"半死"(TCP 已断但连接对象还在),需要周期性探测。
本项目实现
IdleStateHandler(60, 0, 90):- readerIdle=60s:60 秒没收到小程序任何消息 → 触发读空闲
- writerIdle=0:不检测写空闲(服务端是主动推送方,不能因"没写数据"断连)
- allIdle=90s:90 秒完全无读写 → 触发全空闲
- HeartBeatHandler:收到空闲事件后主动关闭连接,释放资源
- 小程序侧:断线后 3 秒自动重连(见 07 篇前端部分)
六、JsonCmdHandler:指令处理(212 行)
支持的指令
| 指令 | 动作 |
|---|---|
| START | 发起充电,投递 RabbitMQ 下行指令,注册推送映射 |
| STOP | 停止充电 |
| RESUME | 断线恢复后续传 |
流程
收到 JSON {action:'START', ...}
→ 构造 ChargeCmdPayload
→ ChargeStatPushRegistry.register(chargeId, ctx)
→ ChargeCmdProducer 投递 Topic (charge.cmd.exchange)
→ mqtt-client 消费 → MQTT publish charge/cmd → 设备
七、工程化亮点
| 措施 | 位置 | 说明 |
|---|---|---|
| 优雅关闭 | WebSocketServ @PreDestroy |
shutdownGracefully() 先停接收再等处理完 |
| 异步启动 | NettyApplication @EnableAsync + WebSocketServ @Async |
避免 Netty 启动阻塞 Spring 主线程 |
| 内部鉴权 | NettyPushApi | X-Internal-Key 头校验(⚠️密钥明文硬编码在 yml) |
| 心跳保活 | IdleStateHandler + HeartBeatHandler | 60s/90s 空闲断连 |
八、常见问题与解答
- 为什么 WebSocket 的粘包/拆包问题比 TCP 少?(WS 帧自带长度,协议层已分包;TCP 是字节流才需要)
@Async+CommandLineRunner启动 Netty,如果不用 @Async 会怎样?(Netty 的 NioEventLoopGroup 非 daemon 线程会阻塞 Spring 容器启动完成)- HttpObjectAggregator 的 64KB 上限意味着什么?(超过 64KB 的 HTTP 请求会被拒绝------WS 握手消息很小,够用)
- 一个小程序连接崩溃但 TCP 没断开,靠什么发现?(IdleStateHandler 读空闲 60s → HeartBeatHandler 主动 close)
- WebSocket 协议升级的过程?(HTTP 握手 Upgrade: websocket → 101 响应 → 切换帧协议)
小结
Netty 服务端承担了小程序的长连接管理:Pipeline 装配决定协议如何处理,注册表决定消息推给谁,心跳与优雅关闭保证连接的健壮性。它也暴露出单机内存注册表的多实例扩展问题------这在 【从零搭建物联网智能充电桩系统】5、RabbitMQ 消息分发的消息架构里会进一步看到解耦的思路。