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

相关推荐
Ai-_Man21 小时前
千问能否电脑批量导出?深度拆解「AI导出鸭」如何让这件事从“反人性”变成“优雅”
人工智能·ai·小程序
hnsoyon21 小时前
依托物联网工程守护城市地下管网运行安全
物联网·物联网工程·地下管网监测
qq_316411031 天前
物联网大健康管理平台的功能模块与技术架构详解
物联网
太阳之神aboluo1 天前
微信小程序 (1) 基础用法
微信小程序·小程序
SL-staff1 天前
技术实践:解决IoT网关单点故障导致子设备批量‘失联’问题
物联网·zigbee·边缘缓存·jvs·iot架构·物模型设计·设备生命周期
lhldsg1 天前
AI智能商城实战指南:从架构设计到落地开发经验分享
java·人工智能·经验分享·小程序
lhldsg1 天前
树洞交友系统开发实战:从匿名社交需求到技术落地
java·小程序·uni-app·交友
TDengine (老段)1 天前
TDengine 应用案例 — 能源与电力监控
大数据·数据库·物联网·能源·时序数据库·tdengine·涛思数据
虎王物联1 天前
Docker WASM 边缘计算:12ms 冷启动如何重塑 IoT 网关部署
物联网·docker·容器·边缘计算·wasm
2601_956743681 天前
上海物联网应用开发技术选型指南:从HTTP/MQTT/Modbus协议适配、平台化架构设计到源代码交付与私有化部署,解析工程落地的关键考量点
物联网·网络协议·http·开发经验·上海