一、前言
早期公司充电桩后台Socket服务,基于原生Java NIO 一主多从Reactor模式自研实现,完整支撑充电桩设备登录、心跳上报、远程启停充电、参数下发、设备应答回调等核心TCP通信能力。
项目上线初期设备接入量较少,自研NIO架构可稳定运行。但随着线下充电桩设备规模化扩容,连接并发量、报文上报频次大幅提升,原生自研架构的架构缺陷、代码耦合、底层能力缺失全面暴露,频繁出现报文解析异常、服务卡顿、新设备连接失败、接口阻塞等线上问题,已无法满足高并发、高可用的生产需求。
为解决线上痛点、统一通信架构、降低维护成本,本文将详述基于Netty对原生NIO充电桩服务的全量重构方案,深度对比原生Reactor与Netty架构差异,系统性解决粘包半包、代码耦合、性能瓶颈三大核心问题。
二、原生NIO Reactor架构痛点分析
2.1 原生架构设计
之前充电桩Socket服务端严格遵循Reactor事件驱动模型,采用经典的单Boss多Worker线程架构:
-
Boss主线程:独立轮询Selector,监听服务端口,处理客户端TCP连接接入,完成三次握手后将新连接分发至Worker线程池。
-
Worker从线程:多线程并行轮询,负责已建立连接的读写事件监听、ByteBuffer数据读取、报文解析与业务分发。

主要架构如上图所示,该架构实现了IO事件解耦,相较于BIO模型大幅提升了并发能力,但属于原生NIO Reactor多线程架构,无任何封装优化,生产落地存在致命缺陷。具体参考先前的文章,任意门:Java NIO服务端多线程化问题
2.2 三大核心线上问题
1. TCP粘包/半包问题无法根治
TCP属于流式传输协议,无天然报文边界。原生NIO架构需手动维护ByteBuffer缓冲区、自定义报文截断逻辑、循环匹配帧头帧尾切割完整报文。
之前未做粘包/半包报文处理,虽然单充电桩只有在设备主动发送心跳或者设备状态报文同时有用户发送开始充电报文的时候才会出现该情况,但是随着设备数量增多,极易出现报文粘包、半包堆积、帧错乱、解析失败,直接导致设备离线、指令下发失效等异常。
2. 架构分层混乱,业务与IO高度耦合
原生NIO代码无分层设计,IO事件轮询、缓冲区读写、报文解析、业务逻辑处理、结果回写全部耦合在同一套事件逻辑中。
底层IO逻辑与上层业务代码混杂,迭代新功能、修改协议字段、修复BUG时极易牵连底层IO逻辑,引发隐性线上故障,代码可维护性、可扩展性极差。
3. 自研架构性能短板突出,无法支撑高并发
原生NIO无池化缓冲区、无线程优化、无资源自动回收机制,存在诸多性能隐患:
-
频繁创建销毁ByteBuffer,造成大量GC,服务吞吐量下降;
-
手动管理线程生命周期、Selector事件,易出现线程阻塞、资源泄漏;
-
无空闲连接检测、异常熔断机制,无效连接持续占用线程资源;
-
单Worker线程阻塞会拖累整组IO事件轮询,高并发下新设备无法建立连接。
三、Netty架构核心原理
Netty是一款基于NIO封装的高性能、事件驱动、异步非阻塞网络通信框架,底层完全沿用Reactor主从架构,但对原生NIO的所有底层缺陷做了工程级封装与优化,是工业级TCP通信标准方案。本次重构核心基于Netty四大核心能力。
3.1 优化版Reactor主从线程模型
Netty标准化实现了BossGroup + WorkerGroup主从Reactor架构,完美适配充电桩长连接场景:
-
BossGroup:少量线程专职处理端口监听、TCP连接建立,仅负责接收新连接,不处理任何读写业务,保证连接接入无阻塞;
-
WorkerGroup:多线程池化管理,每个线程绑定独立Selector,轮询处理已连接Channel的读写、事件回调、异常处理,线程复用、负载均衡。
相较于使用原生NIO,Netty线程模型经过极致优化,规避了手动线程调度的所有BUG,充分利用多核CPU,支撑万级设备长连接并发。
3.2 内置帧解码器,彻底解决粘包半包
针对充电桩自定义二进制定长协议 (帧头+长度域+报文体+校验位+帧尾),采用Netty官方推荐的LengthFieldBasedFrameDecoder 长度域解码器。
该解码器可根据报文内置的长度字段,自动切割TCP流式数据,向上层处理器只推送完整合法报文,从框架底层彻底杜绝粘包、半包问题,无需业务层手动处理缓冲区与报文边界。
3.3 Pipeline责任链:实现架构彻底解耦
Netty核心设计的ChannelPipeline责任链机制,是解决代码耦合的核心方案。每个Channel绑定一条独立处理器链表,IO事件按层级流转,职责完全拆分。
本次重构严格遵循分层思想,定义标准化Pipeline执行链路:
-
底层帧处理层:长度域解码器,处理TCP流、切割完整报文;
-
协议编解码层:实现二进制字节流与业务实体的序列化/反序列化;
-
连接监控层:空闲检测、异常断开、连接资源回收;
-
业务处理层:仅处理设备登录、心跳、指令应答等纯业务逻辑。
分层架构实现了IO底层、协议解析、业务逻辑完全解耦,各模块独立迭代、互不干扰。
3.4 一次编解码与二次编解码分层设计
本次重构创新性拆分两级编解码逻辑,适配充电桩私有协议规范:
-
一次编解码(帧层处理) :由Netty内置解码器完成,核心职责是字节流→完整协议帧,仅处理报文边界,不解析任何业务字段,专注解决TCP传输层问题;
-
二次编解码(业务层序列化) :自定义编解码处理器,完成协议帧字节数组 ↔ Java业务实体的转换,解析设备地址、指令码、充电参数等业务字段,封装成可直接使用的业务对象。
两级编解码分离,让传输层问题、协议解析问题、业务问题完全隔离,极大降低协议迭代与BUG排查成本。
四、项目技术环境与核心依赖
4.1 技术栈版本环境
本项目采用新版稳定技术栈,基于高版本 JDK 与 SpringBoot 搭建,搭配经典稳定的 Netty 版本,兼顾性能、稳定性与兼容性,具体核心环境如下:
-
JDK 版本:JDK 21(长期支持版,性能优化、内存模型升级)
-
SpringBoot 版本:3.2.5(适配 JDK21,提供完整容器管理、依赖注入、事件监听能力)
-
Netty 版本:4.1.42.Final(工业级稳定版本,适配长连接TCP通信,无已知重大BUG)
-
项目源码 :源码下载
4.2 完整Maven父工程POM依赖
项目采用多模块聚合架构,统一版本管控,通过 dependencyManagement 锁定核心依赖版本,避免版本冲突,适配整体项目架构。完整 pom.xml 配置如下:
xml
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 版本统一管控 -->
<spring.boot.version>3.2.5</spring.boot.version>
<netty.version>4.1.42.Final</netty.version>
<lombok.version>1.18.30</lombok.version>
</properties>
<!-- 统一版本锁定 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>${netty.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- SpringBoot核心基础包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Netty 网络通信核心依赖 -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<!-- Lombok 简化实体、日志代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- 通用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
4.3 项目核心重构方案
本次重构完全保留原有充电桩业务逻辑,仅替换底层通信架构,同时解决HTTP同步接口与TCP异步通信适配、设备并发互斥、断线资源清理、请求超时熔断四大业务痛点。
4.3.1 核心架构改造点
-
基于Netty Boss/Worker线程模型重构TCP服务启动类,标准化配置线程池、连接参数、Pipeline链路;
-
接入
LengthFieldBasedFrameDecoder彻底解决粘包半包问题; -
自定义两级编解码处理器,适配充电桩私有二进制协议;
-
新增连接管理器,统一维护在线设备、Channel通道、设备信息;
-
实现
TcpRequestManager请求管理器,基于CompletableFuture实现HTTP同步接口 ↔ Netty异步TCP的通信桥接。
4.4 项目整体业务场景与对接说明
本项目为充电桩Socket服务端,用于对接第三方硬件充电桩设备。硬件厂商仅提供标准化Socket通信协议与二进制报文规范,无配套服务端程序,所有TCP服务、协议解析、指令交互、设备状态管理均由我方自主开发实现。
整体业务采用长连接TCP双向通信模式:充电桩设备作为客户端主动连接服务端,保持长连接心跳保活,服务端被动接收设备上报报文、主动下发控制指令,完成设备全生命周期管理。
4.4.1 报文结构
报文结构如下图所示:

该通信报文整体由 4 字节帧头、可变长度数据体、1 字节帧校验、1 字节帧尾组成;帧头格式固定为 0x68、0xAA、0xBB、0x68,其中 0xAA 为数据体长度高字节、0xBB 为低字节,通过0xAA*256+0xBB计算得到数据体总字节数,数据体由控制层、地址层、数据层三部分构成,最大长度不超过 65535 字节;帧校验字节为数据体全部字节的八位算术和,计算时忽略溢出进位,帧头和帧尾不参与校验运算;帧尾为固定单字节 0x16,用来标记一帧报文结束。
4.4.2 登录认证报文

本部分为充电桩登录认证相关报文规范,包含充电桩上报登录认证与平台登录认证应答,整体沿用统一帧协议格式,充电桩登录认证功能码为 0x02,设备将运营编码、基站定位、软硬件版本、ICCID、信号值等设备信息以 BKV 键值形式封装在数据层上报平台完成注册建连,平台回复同功能码的应答报文,通过错误代码字段反馈登录结果,0x00 表示登录成功,非 0 值代表登录异常。
4.4.3 充电桩心跳报文


该部分为充电桩心跳包及心跳应答报文规范,帧类型码 0x07,充电桩以 60 秒周期主动上送心跳报文,通过 BKV 结构携带通道开关状态、通道功率、信号值等设备状态信息,用于上报设备通道状态以及维持平台与设备之间通信链路;平台收到心跳后回复对应应答报文,依靠错误代码字段反馈处理结果,0x00 代表正常,非 0 表示异常,整套报文遵循项目统一帧协议格式。
4.4.4 充电桩开始充电报文

本部分为运营平台远程控制启机相关报文规范,帧类型码 0x05,由平台主动下发远程启机请求,数据层携带充电通道、充电模式、充电时长参数,下发远程启动充电指令;充电桩收到指令后返回对应应答报文,通过错误代码反馈命令执行结果,同时回传通道状态,0 代表执行成功,非 0 代表执行出错,报文遵循项目统一帧协议及 BKV 键值数据结构。
4.4.5 核心业务交互流程
设备与服务端交互遵循标准化协议流程,本次项目核心实现设备登录、心跳保活、远程启动充电三大核心双向交互能力,其余停止充电、状态查询等报文交互逻辑与现有流程完全一致,可无缝拓展:
-
设备上线登录流程:充电桩TCP连接建立后,主动向服务端上报登录报文 → 服务端解析设备信息、注册设备连接、返回登录应答 → 设备上线成功,纳入在线设备管理池。
-
心跳保活流程:设备每3分钟主动上报心跳报文,携带通道状态、功率、信号强度等信息 → 服务端更新设备最新心跳时间与设备状态,返回心跳应答 → 维持长连接有效性,超时无心跳自动判定设备离线。
-
远程充电控制流程:前端HTTP发起充电指令 → 服务端校验设备在线、通道空闲 → Netty下发充电指令报文 → 阻塞等待设备应答 → 设备执行充电操作并返回应答报文 → 服务端唤醒阻塞接口,返回操作结果。
-
设备状态管理:设备离线、网络异常、状态变更时主动上报报文,服务端实时更新在线状态、清理无效连接、释放阻塞请求资源。### 4.7 项目整体目录结构说明
4.5 项目源码分析
4.5.1包结构
项目采用分层架构、按职责分包,完全规避原生NIO代码混乱耦合问题,目录清晰、职责单一、便于后续协议拓展与功能迭代:
-
config:Spring容器配置、Netty启动配置、定时任务线程池配置
-
server:Netty服务启动核心类,负责线程组初始化、Pipeline链路装配、服务监听
-
codec:两级编解码处理器(帧层一次编解码、业务层二次BKV序列化)
-
handler:Netty业务处理器、帧校验处理器、空闲断开处理器,处理所有IO事件与业务报文
-
entity:设备连接管理、TCP请求超时管理、所有业务报文实体类
-
service:核心业务层,TCP指令下发、设备状态管理、异步同步转换
-
controller:HTTP对外接口层,承接前端操作请求
-
util:十六进制转换、BKV编解码、校验和计算等工具类
4.5.2Netty 服务核心启动源码与链路解析
基于上述架构设计,完成充电桩 Netty TCP 服务核心启动类开发,标准化实现 Boss/Worker 主从线程模型、Pipeline 责任链装配、空闲连接检测、自定义编解码与业务处理器绑定。整体代码遵循 Netty 最佳实践,严格分层、职责单一,完全规避原生 NIO 的架构缺陷。
为彻底实现传输层与业务层解耦 ,项目严格落地「一次帧编解码 + 二次业务序列化」双层架构。上文的长度域解码器、协议帧编码器属于一次编解码 ,负责TCP流帧切割与标准协议帧组装;本节的BKV对象编解码器属于二次编解码 ,负责二进制BKV结构体与Java业务实体的双向转换,是整个协议解析的核心业务层能力。

该 ChannelPipeline 为充电桩 Netty 服务完整处理器责任链,入站数据从 head 向 tail 方向流转,出站响应从 tail 反向向 head 流转,各 Handler 职责分层隔离:最前端IdDisconnectHandler做连接空闲检测,设备长时间不上报心跳则主动关闭通道,回收无效长连接资源;出站下发报文时,先由ChargerObjectToBkvEncoder将 Java 业务对象序列化为 BKV 二进制键值数据,再经ChargerProtocolEncoder完成协议帧组装,填充帧头、长度字段、校验和、帧尾,输出硬件可识别的完整二进制报文;设备上报的入站字节流首先进入ChargerProtocolDecoder,基于 LengthFieldBasedFrameDecoder 切割 TCP 流,解决粘包、半包问题,输出完整协议帧,随后ChargerFrameVerifyHandler完成帧格式、校验和合法性校验,直接丢弃损坏非法报文,再由ChargerBKVToObjectDecoder把 BKV 二进制数据反序列化为 Java 业务实体;解析完成的业务对象流转至ChargerDataHandler执行业务逻辑,处理登录、心跳、充电交互等业务,业务产生的应答报文会沿着 Pipeline 反向出站,逐级经过编码器封包后发送至充电桩设备,底层 IO、协议解析校验全部前置拦截,业务处理器无需接触原始 ByteBuf 字节流,做到传输层、协议层、业务层充分解耦。
4.5.3主要源码
下面贴出项目的核心代码:
4.5.3.1 启动NettyTcpServer
java
public class NettyTcpServer {
public void start() throws InterruptedException {
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline()
// 空闲检测
.addLast(new IdleStateHandler(idleTimeout, 0, 0))
.addLast(new IdleDisconnectHandler(connManager))
// 出站编码
.addLast(new ChargerProtocolEncoder())
.addLast(new ChargerObjectToBkvEncoder())
// 入站解码
.addLast(new ChargerProtocolDecoder())
.addLast(new ChargerFrameVerifyHandler())
.addLast(new ChargerBKVToObjectDecoder())
//回复消息
.addLast(new ChargerDataHandler(connManager, tcpRequestManager));
}
});
ChannelFuture future = bootstrap.bind(port).sync();
log.info("充电桩TCP服务启动完成,监听端口:{}", port);
future.channel().closeFuture().sync();
}
}
这是 Netty NIO TCP 服务启动入口,bossGroup为单线程事件循环组,专门负责监听端口、接收充电桩设备 TCP 连接;workerGroup负责所有已建立连接的网络 IO 读写、事件处理。ServerBootstrap作为服务启动引导类,NioServerSocketChannel.class指定采用 Java NIO 实现服务端 Socket;SO_BACKLOG=128设置服务端 accept 等待队列长度,控制等待接入的连接排队上限;SO_KEEPALIVE开启操作系统 TCP 保活机制辅助识别僵死连接;TCP_NODELAY关闭 Nagle 算法,让短小充电桩指令报文可以即时发送,减少交互延迟;childHandler是每个充电桩设备新建连接后的回调,在initChannel中向ChannelPipeline装配整套处理器链,完成空闲检测、协议编解码、报文校验、业务处理 Handler 的注册,每个 SocketChannel 拥有独立 Pipeline,设备报文按照 Pipeline 链路完成入站出站流转。
4.5.3.2 ChargerProtocolDecoder类
java
public class ChargerProtocolDecoder extends LengthFieldBasedFrameDecoder {
private static final Logger log = LoggerFactory.getLogger(ChargerProtocolDecoder.class);
// 最大单帧长度 = 4(帧头)+65535(数据体)+1(校验)+1(帧尾) = 65541
private static final int MAX_FRAME_LEN = 65541;
public ChargerProtocolDecoder() {
super(
MAX_FRAME_LEN,
1, // lengthFieldOffset:长度字节起始偏移 跳过第1个0x68
2, // lengthFieldLength:长度占用2字节(AA BB)
3, // lengthAdjustment:长度后还包含0x68一个字节+校验1字节+帧尾1字节,总共+3
4, // initialBytesToStrip:解码完成后剥离最前面4字节帧头
true // failFast
);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
// 交给父类完成拆包,得到完整帧,不做任何业务校验
ByteBuf frame = (ByteBuf) super.decode(ctx, in);
if (frame == null) {
return null;
}
// 直接透传给下游handler,这里千万不要做校验和、帧尾判断
log.info("[ChargerFrameDecoder]拆出完整帧,frame可读字节={}", frame.readableBytes());
return frame;
}
}
ChargerProtocolDecoder继承 Netty 内置LengthFieldBasedFrameDecoder,作为充电桩协议帧拆包处理器,专门解决 TCP 粘包半包问题;LengthFieldBasedFrameDecoder是基于报文长度域的通用解码器,依靠报文中的长度字段识别完整报文边界,避免手动处理字节流。构造参数中MAX_FRAME_LEN限制单帧最大报文长度防止内存溢出;lengthFieldOffset=1跳过 1 字节帧头定位到 2 字节长度域;lengthAdjustment=3补偿长度头未包含的后续帧头、校验、帧尾共 3 个字节;initialBytesToStrip=4拆包完成后剔除开头 4 字节帧头与长度域;failFast=true超长报文直接快速报错。重写decode方法调用父类完成拆包,只输出完整 ByteBuf,不做校验和、帧尾校验,将合法性校验交由下游处理器,保证拆包职责单一。
4.5.3.3 ChargerBKVToObjectDecoder类
java
public class ChargerBKVToObjectDecoder extends MessageToMessageDecoder<byte[]> {
@Override
protected void decode(ChannelHandlerContext ctx, byte[] rawFrame, List<Object> out) throws Exception {
log.info("[debug-rawFrame] length={}, hex={}", rawFrame.length, HexByteUtil.bytesToHex(rawFrame));
try {
// 1. byte[]裸BKV → BaseChargerMessage
BaseChargerMessage baseMsg = decode(rawFrame);
Object outMsg = baseMsg;
BKV businessBkv = new BKV();
for (KV kv : baseMsg.getBusinessKvs()) {
businessBkv.add(kv);
}
KV kvAfn = businessBkv.get(0x4);
if (kvAfn != null) {
String function = HexByteUtil.bytesToHex(kvAfn.getValue());
if (StringUtils.isNotBlank(function)) {
// AFN=02 登录帧,转为子实体ChargerLoginMessage
if ("02".equals(function)) {
ChargerLoginMessage loginMsg = convertToLoginMessage(baseMsg);
log.info("[BKV解码]解析登录消息:{}", loginMsg);
outMsg = loginMsg;
}
// AFN=07 心跳帧,转为子实体ChargerHeartBeatMessage
if("07".equals(function)){
ChargerHeartBeatMessage heartBeatMessage = covertToHeartBeatMessage(baseMsg);
log.info("[BKV解码]解析心跳消息:{}", heartBeatMessage);
outMsg = heartBeatMessage;
}
// AFN=05 启动从充电回复帧,转为子实体ChargeStartAckMessage
if("05".equals(function)){
ChargerStartAckMessage startMessage = covertToChargeStartAckMessage(baseMsg);
log.info("[BKV解码]解析充电开始回复消息:{}", startMessage);
outMsg = startMessage;
}
}
}
out.add(outMsg);
} catch (Exception e) {
log.error("[消息编解码]反序列化异常", e);
ctx.close();
}
}
}
ChargerBKVToObjectDecoder 继承 Netty 泛型解码器MessageToMessageDecoder<byte\[\]>,属于项目二次反序列化处理器,负责完成纯字节数组到业务 Java 实体对象的转换,接收上游校验完成后的原始报文字节数组,实现 BKV 二进制键值协议反序列化并按功能码分发对应业务实体。
整体流程:接收上游传递的byte\[\]原始报文 → 调用decode()方法解析外层 BKV,提取控制层、设备地址层、业务 BKV 数据封装通用基类BaseChargerMessage;读取业务区 0x4 功能码 AFN,根据不同指令类型(02 登录、07 心跳、05 充电应答)分别调用转换方法,把通用基类映射为ChargerLoginMessage、ChargerHeartBeatMessage、ChargerStartAckMessage细分业务实体;解析完成后将业务对象放入输出列表传递给下游业务 Handler。捕获解析异常时打印错误日志并关闭异常连接,避免脏连接占用资源,实现协议二进制数据与业务实体解耦,让后续业务层无需处理底层 BKV 字节解析逻辑。
4.5.3.4 ChargerDataHandler类
java
@RequiredArgsConstructor
public class ChargerDataHandler extends ChannelInboundHandlerAdapter {
private static final Logger log = LoggerFactory.getLogger(ChargerDataHandler.class);
private final ChargerConnManager connManager;
private final TcpRequestManager tcpRequestManager;
/** 收到解析完成的数据体字节数组 */
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
Channel channel = ctx.channel();
ChargerConnInfo connInfo = channel.attr(ChargerConnManager.DEVICE_ATTR_KEY).get();
if (connInfo == null) {
return;
}
// 更新最后心跳时间
connInfo.setLastHeartbeatTime(new Date());
if(msg instanceof ChargerLoginMessage){
ChargerLoginMessage login = (ChargerLoginMessage) msg;
log.info("收到登录帧:{}",login);
// ============构造登录应答报文============
ChargerLoginAckMessage ack = new ChargerLoginAckMessage();
//复制控制层、地址层
ack.setControlLayer(login.getControlLayer());
ack.setAddressLayer(login.getAddressLayer());
//业务KV:AFN=02,应答码00成功
ack.setBusinessKvs(new LinkedList<>());
ack.getBusinessKvs().add(new KV(0x4, CodecUtil.hexToBytes("02")));
ack.getBusinessKvs().add(new KV(0x5, CodecUtil.hexToBytes("00")));
//发送应答,增加监听器看发送结果
ChannelFuture future = ctx.writeAndFlush(ack);
future.addListener((ChannelFutureListener) f -> {
if(f.isSuccess()){
//将设备序列号回填到connInfo,断线时可以清除上下文
connInfo.setDeviceId(login.getAddressLayer());
connManager.bindDeviceId(ack.getAddressLayer() ,connInfo);
channel.attr(ChargerConnManager.DEVICE_ATTR_KEY).set(connInfo);
log.info("登录应答提交netty队列成功");
}else{
log.error("登录应答发送失败", f.cause());
}
});
}else if(msg instanceof ChargerHeartBeatMessage){
ChargerHeartBeatMessage heartBeat = (ChargerHeartBeatMessage) msg;
log.info("收到心跳帧:{}",heartBeat);
// ============构造登录应答报文============
ChargerHeartBeatAckMessage ack = new ChargerHeartBeatAckMessage();
//复制控制层、地址层
ack.setControlLayer(heartBeat.getControlLayer());
ack.setAddressLayer(heartBeat.getAddressLayer());
//业务KV:AFN=02,应答码00成功
ack.setBusinessKvs(new LinkedList<>());
ack.getBusinessKvs().add(new KV(0x4, CodecUtil.hexToBytes("07")));
ack.getBusinessKvs().add(new KV(0x5, CodecUtil.hexToBytes("00")));
//发送应答,增加监听器看发送结果
ChannelFuture future = ctx.writeAndFlush(ack);
future.addListener((ChannelFutureListener) f -> {
if(f.isSuccess()){
log.info("心跳应答提交netty队列成功");
}else{
log.error("心跳应答发送失败", f.cause());
}
});
}else if(msg instanceof ChargerStartAckMessage ackMsg){
String devAddr = ackMsg.getAddressLayer();
String chNo = ackMsg.getChannelNo();
// 唤醒Controller等待的future
tcpRequestManager.complete(devAddr, chNo, ackMsg);
} else if(msg instanceof BaseChargerMessage){
// 其他通用报文
}
ctx.fireChannelRead(msg);
}
}
ChargerDataHandler是 Netty Pipeline 末端纯业务处理器,负责处理通道生命周期事件与解析完成后的充电桩业务实体;设备新建连接时初始化并缓存通道上下文,收到登录、心跳报文会组装对应应答下发,收到充电回执则调用请求管理器唤醒阻塞的 HTTP 同步接口;通道断开或通信异常时联动连接管理器清理通道、销毁未完成异步任务防止内存泄漏。若需要执行设备上下线记录、心跳数据、充电回执等数据库持久化或数据集交互操作,不能在 Netty Worker IO 线程同步执行,需通过自定义业务线程池提交异步任务,将耗时存储逻辑剥离,避免阻塞网络读写、影响全量设备通信,全程不操作原始二进制字节,实现网络层、业务交互、数据持久化分层解耦。
4.5.3.5 ChargerObjectToBkvEncoder类
java
public class ChargerObjectToBkvEncoder extends MessageToMessageEncoder<BaseChargerMessage> {
@Override
protected void encode(ChannelHandlerContext ctx, BaseChargerMessage msg, List<Object> out) throws Exception {
BKV outerBkv = new BKV();
if(msg.getControlLayer() != null){
outerBkv.add(new KV(0x1, CodecUtil.hexToBytes(msg.getControlLayer())));
}
if(msg.getAddressLayer() != null){
outerBkv.add(new KV(0x2, CodecUtil.hexToBytes(msg.getAddressLayer())));
}
BKV innerBkv = new BKV();
for (KV kv : msg.getBusinessKvs()) {
innerBkv.add(kv);
}
byte[] innerBytes = innerBkv.pack();
outerBkv.add(new KV(0x3, innerBytes));
byte[] outerBkvBytes = outerBkv.pack();
out.add(outerBkvBytes);
}
}
ChargerObjectToBkvEncoder 是 Netty 出站编码器,继承MessageToMessageEncoder,负责业务实体对象序列化为 BKV 二进制字节数组,属于出站流程的二次编码环节。下发登录、心跳、充电应答等报文时,接收封装好的业务实体,依次组装外层 BKV 结构:0x1 控制层、0x2 设备地址层、0x3 业务内层 BKV 数据,将业务 KV 集合打包为字节后塞入外层 KV,最后整体打包成完整二进制数组交给上游帧编码器,完成业务对象到原始报文数据的转换,实现业务层与底层二进制协议解耦,仅专注对象序列化,不参与协议帧头、校验、帧尾封装,上层统一由ChargerProtocolEncoder完成整帧封包。
4.5.3.6 开启充电桩充电接口
java
@PostMapping("/start")
public ResponseEntity<?> startCharger(@RequestBody ChargerStartMessage dto) {
try {
ChargerStartAckMessage resp = (ChargerStartAckMessage)operateService.startCharge(dto);
if("0000".equals(resp.getErrorCode())){
return ResponseEntity.ok("开始充电成功");
}else{
return ResponseEntity.ok("开始充电失败");
}
}catch (RuntimeException e){
return ResponseEntity.badRequest().body(e.getMessage());
}
}
java
public Object startCharge(ChargerStartMessage dto) {
String deviceAddr = dto.getAddressLayer();
String channelNo = dto.getChannelNo();
//1.校验设备在线
Channel channel = connManager.getDevice(deviceAddr).getChannel();
if(channel == null || !channel.isActive()){
throw new RuntimeException("设备不在线");
}
//2.注册等待回执future
CompletableFuture<Object> future = tcpRequestManager.register(deviceAddr, channelNo, 5000);
if(future == null){
throw new RuntimeException("该通道正在执行指令,请稍后重试");
}
//3.组装下发报文
ChargerStartMessage startMsg = new ChargerStartMessage();
startMsg.setAfn("05");
startMsg.setControlLayer(dto.getControlLayer());
startMsg.setAddressLayer(deviceAddr);
startMsg.setChannelNo(channelNo);
startMsg.setChargeMode(dto.getChargeMode());
startMsg.setChargeTime(dto.getChargeTime());
startMsg.setBusinessKvs(new LinkedList<>());
startMsg.getBusinessKvs().add(new KV(0x4, CodecUtil.hexToBytes("05")));
startMsg.getBusinessKvs().add(new KV(0x30, CodecUtil.hexToBytes(startMsg.getChannelNo())));
startMsg.getBusinessKvs().add(new KV(0x59, CodecUtil.hexToBytes(startMsg.getChargeMode())));
startMsg.getBusinessKvs().add(new KV(0x5A, CodecUtil.hexToBytes(startMsg.getChargeTime())));
//4.Netty下发
ChannelFuture futureMsg = channel.writeAndFlush(startMsg);
// 增加监听,打印发送是否成功
futureMsg.addListener(f -> {
if(f.isSuccess()){
System.out.println("报文发送成功");
}else{
System.err.println("报文发送失败:" + f.cause().getMessage());
f.cause().printStackTrace();
}
});
try {
//5.等待5秒回执
Object resp = future.get(5, TimeUnit.SECONDS);
return resp;
} catch (TimeoutException e) {
throw new RuntimeException("指令等待设备回执超时");
} catch (Exception e) {
throw new RuntimeException("下发启动充电指令异常:"+e.getMessage());
}
}
远程启动充电功能分为 HTTP 控制层接口与业务处理方法两部分,用来打通前端同步请求和 Netty 异步 TCP 通信。前端 POST 请求传入充电参数后,控制层调用业务方法,根据设备应答错误码返回操作结果并统一捕获异常;业务层先校验充电桩长连接是否在线,再基于设备地址 + 通道注册限时 CompletableFuture 实现指令互斥,避免同一通道并发下发造成报文混乱,接着按 BKV 协议组装充电指令,通过 Netty 通道下发并监听发送状态,最后阻塞等待 5 秒设备回执,超时或通信异常都会抛出业务提示,依靠 Future 机制实现异步 TCP 回执同步回传给前端页面。
五、测试
为完成 TCP 双向报文本地联调,项目配套轻量化 Netty 测试客户端,用于模拟真实充电桩硬件设备,无需复杂自动报文封装,支持手动输入十六进制报文交互,可完整覆盖设备登录、心跳上报、远程充电指令全业务流程调试。
本项目所有报文均为手动预制组装,客户端无自动业务报文生成逻辑,测试流程简单清晰,可稳定复现双向交互:
- 启动服务端:运行 SpringBoot + Netty 服务端,监听 8098 端口;
- 启动测试客户端 :运行
ChargerNettyClient,自动连接本地服务端,建立长连接; - 设备上线 :客户端控制台粘贴预制登录报文发送,服务端接收登录、注册设备连接、返回登录应答,设备进入在线状态;
- 保活校验:可手动发送心跳报文,维持设备长连接;
- 接口测试 :打开 ApiPost,调用服务端
POST /charger/start开始充电接口; - 双向交互闭环:服务端下发充电指令 → 客户端自动识别并回复应答报文 → 服务端接收回执、返回前端成功/失败结果。
5.1 ApiPost测试参数

这是接口ApiPost测试参数图,使用 JSON 格式请求体,用于模拟平台下发0x05 远程启机充电指令:请求传入地址层、控制层报文原始十六进制串,同时指定通道号channelNo、功能码afn、充电模式chargeMode、充电时长chargeTime;后端接收该 JSON 入参后,按照充电桩自定义协议完成 BKV 数据层组装、帧头长度计算、校验和运算、拼接帧头帧尾,生成完整二进制报文,通过 Netty 向下发给对应充电桩设备执行远程开机充电。
六、项目说明与生产优化补充
原生Java NIO自研Reactor架构适合入门学习与低并发场景,但缺乏工程级封装,无法支撑规模化生产场景。Netty基于优化版Reactor主从模型,封装了TCP通信的所有底层细节,通过标准化线程模型、内置帧解码、Pipeline分层架构、两级编解码设计 ,完美解决了充电桩项目的粘包半包、代码耦合、性能瓶颈三大核心问题。
本次重构不仅完成了底层通信架构的升级,更实现了异步TCP通信与同步HTTP业务的完美适配 ,构建了一套高可用、高并发、易扩展的工业级充电桩TCP通信服务,为后续设备规模化接入、协议迭代、功能拓展奠定了坚实的架构基础。
重要说明:本文所有代码为项目核心框架代码,仅适用于学习与基础联调
当前代码仅完成 TCP通信层、协议解析层、基础业务调度、登录/心跳/开始充电双向交互 核心能力,还可以进行以下方面的优化:
- 接入 MySQL/Redis 实现设备信息、在线状态、充电记录、登录日志持久化存储;
- 完善设备白名单、权限校验、指令幂等、重复请求拦截;
- 拓展停止充电、设备参数查询、故障上报、批量状态同步等全量协议交互逻辑;
- 新增业务日志归档、操作记录、异常日志持久化;
七、总结
本文完成了充电桩Socket服务从原生 NIO 到 Netty 架构的整体重构,解决了原生NIO架构存在的粘包半包、代码耦合、并发性能差、资源难以管控等问题。基于 SpringBoot + Netty 构建标准化长连接通信服务,通过主从 Reactor 线程模型、长度域自动拆包、两级编解码与 Pipeline 分层机制,实现网络通信、协议解析、业务逻辑彻底解耦。
项目通过CompletableFuture实现HTTP 同步接口与 TCP 异步通信的适配,完成设备登录、心跳保活、远程充电指令下发等完整双向交互,并加入超时熔断、指令互斥、断线资源回收等健壮性机制。重构保留全部原有业务逻辑,大幅提升系统并发能力与稳定性,适配大批量充电桩设备长连接接入,为物联网硬件通信项目提供了规范、可复用的 Netty 落地方案。