简介JAVA 充电桩协议库JCPP是一套面向充电桩平台开发者与物联网后端工程师的协议实现合集聚焦国内主流充电桩通信协议的对接与落地适合需要快速搭建充电运营平台、完成多协议适配的中高级 Java 开发者。资源包共 586 个文件以 468 个 Java 源码为核心辅以 35 个 Markdown 说明、20 个 XML 配置、15 个 TSX 与 10 个 TS 前端文件以及 yml、proto、json、Dockerfile、sql 等配套内容整体约 1.06MB覆盖协议解析、服务配置、容器部署与前端交互等环节。协议层面支持云快充 1.5/1.6、南网 104、京能、绿能、挚达、星星、领充、EN 等并涉及互联互通、多租户与分时计费。结合慧知开源充电平台全套源码读者可参考完整业务流程涵盖小程序、管理后台、多商户与模拟桩等模块用于协议对接、平台搭建与二次开发。目前已有 72 人学习。1. 一个协议库吞下八种充电桩JCPP 到底在解决什么问题做过充电桩后台的同行大概都有过这种体验项目立项时只说要接一个品牌的桩代码写得干干净净三个月后商务谈下来三家新客户每家的协议都不一样于是你的ChargeService里开始出现if (brand.equals(yunkuai))这种分支半年后这个类膨胀到三千行谁都不敢动。JCPPJAVA 充电桩协议库要解决的就是这件事——它把云快充、南网104、京能、绿能、挚达、星星、领充、EN 这些国内主流充电桩协议收敛到一套统一的 Java 接口下让上层业务只面对「充电桩」这个概念而不是面对八套报文格式。这篇文章面向三类人正在做充电运营平台后端、被多品牌协议折磨的 Java 工程师准备自建充电桩管理系统、需要评估协议库选型的架构师以及做充电桩硬件对接、想搞清楚后台到底怎么解析自己上报数据的嵌入式开发者。我会按「协议库怎么分层 → 单协议怎么跑通 → 多协议怎么统一 → 坑在哪 → 怎么验证」的顺序讲代码可以直接抄参数可以直接改。2. 拆开 JCPP 的分层从 TCP 帧到业务对象要过几道手在动手写第一行对接代码之前得先搞清楚一个协议库的分层逻辑。很多团队翻车的根源不是不会写 socket而是把「收字节」「解帧」「校验」「转业务对象」「状态机推进」这五件事揉在一个类里结果换一个协议就要重写一遍。JCPP 这类库的价值恰恰在于它把这五层切开了你只需要在正确的层上做扩展。2.1 传输层、编解码层、会话层、业务层的职责边界传输层只干一件事维持 TCP 长连接处理粘包拆包。充电桩协议绝大多数是 TCP 短帧或长连接帧云快充走的是 TCP 自定义帧头南网104 走的是类似 IEC 104 的 APDU 结构EN 有些型号走的是 TCP 上的 JSON。传输层不应该知道任何业务字段它只负责把字节流按帧边界切出来交给上层。编解码层负责字节与对象的互转。这一层是协议差异最大的地方云快充的帧头是68 长度 序列号 加密标志 命令字南网104 是68 长度 控制域 地址域 类型标识。JCPP 的做法是给每个协议实现一套Codec把byte[]转成统一的Message对象。会话层维护「这把枪现在是什么状态」——空闲、已插枪、充电中、已充满、故障。充电桩协议里大量报文是状态上报和状态查询会话层要保证状态迁移合法比如没插枪就收到「开始充电」指令应该直接拒绝而不是往下传。业务层才是你写订单、计费、下发控制指令的地方。它只调用ChargePointService.startCharging(deviceId, connectorId)这样的方法完全不关心底层是哪个品牌。提示如果你现在的代码里decode()方法超过 200 行基本可以判定分层没做好先别急着接新协议把编解码抽出来。2.2 用 Maven 引入 JCPP 并跑通第一个连接假设你已经拿到 JCPP 的 jar 包或源码第一步是把它作为依赖引入。常见做法是本地 install 到私服或者直接把源码模块加进你的工程。下面是一个典型的pom.xml片段dependency groupIdcom.example.jcpp/groupId artifactIdjcpp-core/artifactId version1.0.0/version /dependency dependency groupIdcom.example.jcpp/groupId artifactIdjcpp-protocol-yunkuai/artifactId version1.0.0/version /dependencyjcpp-core提供传输、会话、统一消息模型jcpp-protocol-xxx是各协议的具体实现按需引入不要一次性全加进来否则类冲突和启动扫描会变慢。接下来写一个最小的服务端启动类监听 9000 端口注册云快充协议public class JcppServerBootstrap { public static void main(String[] args) throws Exception { // 1. 创建协议注册中心注册云快充协议 ProtocolRegistry registry new ProtocolRegistry(); registry.register(new YunKuaiProtocol()); // 2. 创建会话管理器负责维护设备连接与状态 SessionManager sessionManager new DefaultSessionManager(); // 3. 启动 TCP 服务绑定端口 JcppServer server new JcppServer(9000, registry, sessionManager); server.start(); System.out.println(JCPP server started on 9000); } }这段代码的逻辑很直白注册中心决定「收到字节后找谁解」会话管理器决定「解出来的消息归哪个设备」服务端负责网络 IO。参数上端口按你实际部署环境改ProtocolRegistry支持注册多个协议后面讲多协议共存时会用到。2.3 一个最小可用的消息处理器长什么样协议库解出来的消息最终要交到你的业务代码里。JCPP 一般提供MessageHandler接口你实现它即可public class MyChargeHandler implements MessageHandler { Override public void onMessage(Session session, Message message) { // 根据消息类型分发这里只处理登录和心跳 switch (message.getType()) { case LOGIN: // 设备登录记录 deviceId 与 session 的映射 session.setAttribute(deviceId, message.getField(deviceId)); session.send(Ack.ok(message.getSeq())); break; case HEARTBEAT: // 心跳直接回并刷新会话活跃时间 session.refresh(); session.send(Ack.ok(message.getSeq())); break; default: // 其他消息交给业务线程池避免阻塞 IO 线程 BusinessExecutor.submit(() - dispatch(message)); } } }关键点有三个登录消息必须把deviceId和session绑定否则后续主动下发指令找不到连接心跳要刷新会话很多现场掉线就是因为服务端没做超时剔除耗时业务必须扔到业务线程池IO 线程只做收发这是血泪经验曾经有个项目在 IO 线程里查数据库结果 200 个桩同时上报时整个服务卡死。3. 单协议跑通以云快充为例走完登录、心跳、充电全流程选一个协议先跑通是评估任何协议库的正确姿势。云快充在国内中小运营商里覆盖率高报文结构相对规整适合作为第一个打通的协议。这一章按「登录 → 心跳 → 下发充电 → 接收状态 → 停止充电」的顺序走一遍每一步给出代码和参数说明。3.1 云快充的帧结构与登录报文解析云快充的帧头一般是68后面跟长度、序列号、加密标志、命令字再后面是数据域最后是校验和。登录命令字常见为0x01数据域里包含设备编号、桩类型、枪数量等。JCPP 的YunKuaiCodec会把这些字段解成Message你拿到的message.getField(deviceId)就是设备编号。如果你要自己校验解析对不对可以打开 debug 日志把原始十六进制打出来对照// 在 handler 里临时加一行观察原始帧 log.debug(raw frame: {}, HexUtil.encodeHexStr(message.getRawBytes()));常见坑是长度字段算错。云快充的长度一般指「数据域长度」而不是整帧长度差一个字节就会导致粘包解析错位。如果你发现第二帧开始全部乱掉先查长度定义。3.2 心跳超时与会话剔除的参数怎么设心跳间隔由桩端决定常见 30 秒到 60 秒。服务端要做的是「超过 N 个心跳周期没收到就剔除会话」。JCPP 的DefaultSessionManager一般提供setIdleTimeout参数DefaultSessionManager sessionManager new DefaultSessionManager(); // 90 秒无任何报文则判定离线约为 3 个心跳周期 sessionManager.setIdleTimeout(90_000); // 每 10 秒扫描一次超时会话 sessionManager.setScanInterval(10_000);参数怎么定如果桩端心跳是 30 秒超时设 90 秒比较稳能容忍两次丢包设 35 秒会导致网络抖动时频繁误判离线运营那边会收到一堆无意义的告警。扫描间隔不要小于 5 秒否则大连接量下 CPU 会被扫描线程吃掉。3.3 下发开始充电指令与订单号绑定下发充电是后台主动发起的典型场景。你需要构造一个StartChargingCommand填入设备编号、枪号、订单号、限流值等StartChargingCommand cmd new StartChargingCommand(); cmd.setDeviceId(3201000001); cmd.setConnectorId(1); cmd.setOrderNo(ORD20240520001); cmd.setLimitCurrent(32.0); // 单位 A cmd.setLimitDuration(3600); // 单位秒 // 通过会话管理器找到对应连接并下发 Session session sessionManager.getSessionByDeviceId(3201000001); if (session null) { throw new IllegalStateException(device offline); } session.send(cmd);逻辑说明orderNo必须全局唯一后续状态上报里会带回这个订单号用来对账limitCurrent是桩端限流不是电池请求电流设太大桩会拒绝limitDuration到点后桩端会自动停后台也要有定时任务兜底。下发后不要立刻认为充电成功要等桩端回「充电中」状态才算真正启动。3.4 接收充电状态上报并落库桩端会周期性上报充电状态包含电压、电流、电量、SOC、充电时长等。JCPP 解出来的ChargingStatusMessage字段比较全落库时注意单位换算public void onChargingStatus(ChargingStatusMessage msg) { ChargeRecord record new ChargeRecord(); record.setOrderNo(msg.getOrderNo()); record.setVoltage(msg.getVoltage() / 10.0); // 桩端常以 0.1V 上报 record.setCurrent(msg.getCurrent() / 10.0); // 0.1A record.setSoc(msg.getSoc()); record.setPower(msg.getPower() / 1000.0); // 0.001kWh 转 kWh chargeRecordMapper.insert(record); }单位换算是高频翻车点。不同品牌、甚至同品牌不同型号电量单位可能是 0.001kWh 也可能是 0.01kWh接之前一定要拿真实桩抓一次报文确认别信文档。落库频率也要控制状态上报可能 5 秒一次直接 insert 会把数据库写爆常见做法是内存聚合、每 30 秒或状态变化时才落库。4. 多协议共存让八种桩走同一套业务代码单协议跑通只是及格线JCPP 真正的价值在多协议共存。这一章讲怎么在不改业务层的前提下把云快充、南网104、京能、绿能、挚达、星星、领充、EN 接进来以及协议识别、字段映射、指令适配三个关键问题。4.1 按端口、按帧头还是按设备号识别协议多协议共存第一个问题是「一条连接进来我怎么知道它是哪个协议」。常见三种做法识别方式适用场景优点缺点按端口区分每个品牌独立端口实现简单零歧义端口占用多运维配置繁琐按帧头区分帧头特征明显单端口接入帧头相似时误判按设备号前缀设备编号有品牌段灵活依赖编号规范我一般会优先用「按端口 按帧头」组合主流品牌各占一个端口同一端口内再用帧头兜底。JCPP 的ProtocolRegistry支持按端口注册不同协议也支持注册一个ProtocolDetector做帧头嗅探。// 端口 9001 只跑云快充 registry.register(9001, new YunKuaiProtocol()); // 端口 9002 跑南网104 registry.register(9002, new NanWang104Protocol()); // 端口 9003 跑一个探测器自动识别京能/绿能 registry.register(9003, new ProtocolDetector() .addMatcher(new JingNengMatcher()) .addMatcher(new LvNengMatcher()));4.2 统一消息模型与字段映射表多协议最大的工作量在字段映射。每个协议的字段名、单位、枚举值都不一样JCPP 的统一Message模型定义了一套标准字段各协议 Codec 负责把私有字段映射过来。下面是一张典型的映射表统一字段云快充南网104京能说明deviceId设备编号地址域桩编号统一为字符串connectorId枪号信息体地址枪序号从 1 开始socSOC荷电状态剩余电量统一 0-100power功率有功功率实时功率统一 kWstatus状态字状态量工作状态映射到统一枚举映射代码通常写在 Codec 里比如京能的 SOC 字段可能是0xFF表示无效映射时要转成null而不是 255否则业务层会算出离谱的充电曲线。4.3 下发指令的适配层一个 startCharging 覆盖八种桩业务层只调一个方法底层适配八种协议这是 JCPP 最舒服的地方。实现方式是给每个协议实现一个CommandAdapterpublic interface CommandAdapter { Message adapt(StartChargingCommand cmd); } public class YunKuaiCommandAdapter implements CommandAdapter { Override public Message adapt(StartChargingCommand cmd) { YunKuaiStartMsg msg new YunKuaiStartMsg(); msg.setDeviceId(cmd.getDeviceId()); msg.setGun(cmd.getConnectorId()); msg.setOrderNo(cmd.getOrderNo()); msg.setCurrent((int) (cmd.getLimitCurrent() * 10)); return msg; } }业务层代码变成public void startCharging(String deviceId, int connectorId, String orderNo) { StartChargingCommand cmd new StartChargingCommand(deviceId, connectorId, orderNo); // 根据设备所属协议自动选择适配器 commandDispatcher.dispatch(deviceId, cmd); }commandDispatcher内部根据设备注册时记录的协议类型选适配器。这样新增一个品牌只需要加一个 Codec 和一个 Adapter业务层一行不改。这是评估协议库时最该看的扩展点如果它要求你改业务代码才能接新协议那这个库的分层就是假的。5. 避坑与排查多协议对接里最容易翻车的五件事这一章是我和同行踩过的坑合集每条按「现象 → 原因 → 解决」写能帮你省下不少通宵。5.1 现象桩显示已连接后台却收不到登录原因多数是粘包处理有问题。桩端可能把登录帧和心跳帧连在一起发如果你的解码器按「一次 read 一帧」处理第二帧就被丢掉登录帧恰好被拆到两次 read 里也会解析失败。解决确认 JCPP 的传输层用的是累积缓冲区而不是单次 read。自己写的话用ByteBuf或ByteArrayOutputStream累积按帧头 长度循环切帧。排查时打开 hexdump 日志看第一帧长度字段和实际字节数是否一致。5.2 现象南网104 的地址域解析出来是负数原因南网104 的地址域可能是 2 字节或 4 字节且有无符号问题。Java 的byte是有符号的直接(bytes[0] 8) | bytes[1]在最高位为 1 时会得到负数。解决用ByteBuffer或 0xFF掩码处理int addr ((bytes[0] 0xFF) 8) | (bytes[1] 0xFF);如果地址是 4 字节用ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt() 0xFFFFFFFFL。字节序也要确认南网104 常见小端云快充常见大端搞反了设备号会变成天文数字。5.3 现象充电中途订单号对不上对账差钱原因部分协议在充电过程中会重新分配订单号或者桩端重启后订单号重置。后台如果只按订单号关联就会丢单。解决用「设备号 枪号 开始时间」做联合主键兜底订单号只作为辅助。JCPP 的会话层可以挂载业务属性建议在会话里存一个后台生成的sessionOrderNo所有上报都往这个号上归集桩端的订单号只做透传记录。5.4 现象多协议同时上线后CPU 飙到 100%原因常见两个一是每个连接一个线程的阻塞 IO 模型几百个桩就几百个线程二是心跳扫描和状态落库频率太高。解决传输层换成 Netty 或 Java NIOJCPP 默认实现一般基于 Netty。心跳扫描间隔调到 10 秒以上状态落库做批量或变化触发。另外检查日志级别debug 级别在高频报文下会产生大量字符串拼接生产环境务必用 info 或 warn。5.5 现象某品牌桩下发指令成功但桩不动作原因指令字段单位或枚举不对。比如限流值桩端要 0.1A 为单位你传了安培或者启动模式枚举桩端要0x01你传了1但字节序错了。解决拿该品牌桩的抓包对照逐字段核对。JCPP 的 Adapter 里建议把单位换算写成常量并加注释别在业务层随手乘 10。遇到实在对不上的先用厂商提供的调试工具发一帧标准指令抓下来和 JCPP 生成的帧做二进制 diff差异一眼就能看出来。6. 验证与进阶用回放和压测确认你的 JCPP 接入是可靠的代码写完不代表能上线充电桩协议对接最怕的是「实验室好的现场就崩」。这一章讲两个我常用的验证手段以及一个能显著提升稳定性的技巧。6.1 用报文回放做回归测试把现场抓到的真实报文存成文件写一个回放器按时间戳重放到 JCPP 服务端断言业务层产生的订单、状态是否正确。这样每次改 Codec 都能快速回归不用真接桩。public class ReplayTest { Test public void testYunKuaiLoginAndCharge() throws Exception { // 从文件读取十六进制报文按行回放 ListString frames Files.readAllLines(Paths.get(src/test/resources/yunkuai.log)); EmbeddedServer server new EmbeddedServer(9000); server.start(); try (Socket socket new Socket(127.0.0.1, 9000)) { for (String hex : frames) { socket.getOutputStream().write(HexUtil.decodeHex(hex)); socket.getOutputStream().flush(); Thread.sleep(50); // 模拟真实间隔 } } // 断言订单已创建、状态已更新 assertEquals(1, orderRepository.countByDeviceId(3201000001)); } }参数说明Thread.sleep(50)是模拟桩端发送间隔太快会掩盖粘包问题太慢测试跑得久一般 20 到 100 毫秒。断言要覆盖登录、充电中、结束、结算四个关键节点。6.2 用连接压测确认会话管理不崩用脚本模拟 500 个桩同时连接、发心跳、上报状态观察内存、线程数、GC 和消息延迟。重点看两个指标会话超时剔除是否生效断开一半后内存是否回落以及消息处理延迟 P99 是否在可接受范围一般要求 500ms 内。# 用 tcpreplay 或自写压测客户端模拟 500 连接 java -jar jcpp-stress.jar --host 127.0.0.1 --port 9000 --connections 500 --interval 30如果 P99 超过 1 秒先查业务线程池大小和数据库写入别急着怪协议库。我一般会把业务线程池设为 CPU 核数的 2 倍队列有界满了直接拒绝并告警避免雪崩。6.3 一个让排障效率翻倍的习惯给每条消息打上追踪号最后分享一个习惯在会话层给每条进出消息生成一个traceId贯穿日志、订单、告警。现场出问题时运营给你一个订单号你能在几秒内捞出这条订单关联的所有原始报文和状态迁移不用再让现场复现。JCPP 的Session支持挂载属性把traceId塞进去即可。这个习惯看起来不起眼但真到对账差钱、桩主投诉的时候它就是你的后悔药。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?