【从零搭建物联网智能充电桩系统】4、Netty WebSocket 服务:小程序实时推送背后的长连接管理

小程序与云端保持长连接实时接收充电状态,底层是 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 空闲断连

八、常见问题与解答

  1. 为什么 WebSocket 的粘包/拆包问题比 TCP 少?(WS 帧自带长度,协议层已分包;TCP 是字节流才需要)
  2. @Async + CommandLineRunner 启动 Netty,如果不用 @Async 会怎样?(Netty 的 NioEventLoopGroup 非 daemon 线程会阻塞 Spring 容器启动完成)
  3. HttpObjectAggregator 的 64KB 上限意味着什么?(超过 64KB 的 HTTP 请求会被拒绝------WS 握手消息很小,够用)
  4. 一个小程序连接崩溃但 TCP 没断开,靠什么发现?(IdleStateHandler 读空闲 60s → HeartBeatHandler 主动 close)
  5. WebSocket 协议升级的过程?(HTTP 握手 Upgrade: websocket → 101 响应 → 切换帧协议)

小结

Netty 服务端承担了小程序的长连接管理:Pipeline 装配决定协议如何处理,注册表决定消息推给谁,心跳与优雅关闭保证连接的健壮性。它也暴露出单机内存注册表的多实例扩展问题------这在 【从零搭建物联网智能充电桩系统】5、RabbitMQ 消息分发的消息架构里会进一步看到解耦的思路。

相关推荐
sunywz1 小时前
【从零搭建物联网智能充电桩系统】选型篇:Netty、MQTT、RabbitMQ、IoTDB 为什么这么选?
物联网·rabbitmq·iotdb
桐盛科技1 小时前
智慧公厕厕位监测技术选型实战:激光测距、毫米波雷达、红外测距的优缺点对比与场景适配
物联网·智慧城市
sunywz3 小时前
【从零搭建物联网智能充电桩系统】系列导读:从硬件到小程序的完整链路
物联网·小程序
microrain10 小时前
从数据烟囱到融合中心:SagooIoT统一数据处理平台的设计实践
物联网·golang·开源·sagooiot
W.W.H.12 小时前
将驱动编译成模块并安装到rootfs
linux·物联网·wifi·rootfs·驱动
资质小秘书13 小时前
2026 年 8 月行业科普:小程序商家需要北京 ICP 许可证吗?
其他·小程序
单片机杂货铺16 小时前
基于单片机的智能粮仓控制系统设计与实现
stm32·单片机·嵌入式硬件·物联网·计算机外设·51单片机·课程设计
指尖的爷16 小时前
RKNN转化环境搭建(rknn_toolkit2新版本)
嵌入式硬件·深度学习·物联网·目标检测
老孙讲技术16 小时前
周界枪响了、电梯脸却进了物业群:一个 callback,怎么把 alarm 和 faceAnalysis 拆成两条值班线?
后端·物联网