【从零搭建物联网智能充电桩系统】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 消息分发的消息架构里会进一步看到解耦的思路。

相关推荐
深圳市恒星物联科技有限公司12 小时前
轻量MCU设备通过OpenHarmony兼容性测评的全流程关键要点与实战踩坑经验
大数据·人工智能·物联网·鸿蒙
TDengine (老段)12 小时前
TDengine 常见问题 TOP3
大数据·数据库·物联网·时序数据库·tdengine·涛思数据
河北清兮网络科技14 小时前
开发软件怎么找靠谱的公司?普通人最全筛选避坑指南
小程序·app·短剧·短剧app·广告联盟
D_codingXuChu14 小时前
2026年物联网应用开发服务商怎么选:从设备接入到业务闭环的采购评估指南
物联网·技术分享·开发经验
YCOSA202517 小时前
BUG修复---雨晨 Windows 11 IoT 企业版 LTSC 23H2 终结版 22631.7588
windows·物联网
奶油喜多多18 小时前
深度测评2026培训机构消课系统,课时核算落地经验
小程序·需求分析
BreezeJiang19 小时前
别把 WebSocket 当成一门新协议学:搞懂"借 HTTP 握手",双端 Demo 和跨域就都通了
websocket·node.js
星恒讯工业路由器21 小时前
“铸链计划”第三批启动,工业5G产品从“有”到“优”的淘汰赛开始了
网络·物联网·智能路由器·工业物联网·铸链计划·工业5g·工业路由器选型
码流子1 天前
几万路监控怎么接:高速公路视频汇聚平台的架构设计与落地坑点
大数据·人工智能·物联网·算法·架构
新思维软件1 天前
基于 STM32 的快递柜自动存取系统设计与实现(项目编号 X604041)
stm32·嵌入式硬件·物联网·物联网开发