01 · 项目总览与架构
本文是
xingshuangs/iot-communication源码分析的第一篇:从pom.xml依赖、包结构、顶层接口三个切面建立对整个库的整体认知,并给出架构评价与横向对比。后续三篇分别深入网络层、三大 PLC 协议实现和基础设施。
本文目录
- 1. 从 pom.xml 看工程定位
- 2. 源码规模统计
- 3. 包结构全景
- 4. 顶层接口设计
- 5. 分层架构与调用链
- 6. 三大协议族的统一抽象对比
- 7. 与 HslCommunication 的设计取向差异
- 8. 设计优点 / 缺点 / 可借鉴点
1. 从 pom.xml 看工程定位
pom.xml 位于 iot-communication/pom.xml,坐标与基础信息如下(pom.xml:7-28):
7→ <groupId>com.github.xingshuangs</groupId>
8→ <artifactId>iot-communication</artifactId>
9→ <version>1.5.6</version>
10→ <packaging>jar</packaging>
...
23→ <properties>
24→ <maven.compiler.source>1.8</maven.compiler.source>
25→ <maven.compiler.target>1.8</maven.compiler.target>
1.1 依赖清单
pom.xml:30-59 的 <dependencies> 一共只有 4 个依赖,全部是"工具/测试"性质:
| 依赖 | 版本 | Scope | 作用 | 备注 |
|---|---|---|---|---|
junit:junit |
4.13.2 | test |
单元测试 | 唯一测试框架 |
org.projectlombok:lombok |
1.18.26 | provided |
@Data / @Getter / @Slf4j |
编译期侵入,全库大量使用 |
org.slf4j:slf4j-api |
2.0.6 | compile |
日志门面 | 唯一的运行时依赖 |
org.slf4j:slf4j-simple |
2.0.6 | test |
日志实现 | 不进生产包 |
1.2 关键结论:三个"没有"
这是本项目最值得先说清楚的事实:
| 问题 | 结论 | 证据 |
|---|---|---|
| 有 Netty 吗? | 没有 | pom.xml:30-59 无任何 netty 坐标;全库 grep -rn "netty" src/ pom.xml 无命中 |
| 有 rxtx / jssc / 串口吗? | 没有 | 全库 grep -rn "rxtx|SerialPort|jssc|CommPortIdentifier" src/ 无命中。串口版 Modbus RTU 不支持(源码中未见),只有 RTU over TCP / ASCII over TCP |
| 有 NIO 吗? | 没有 | 全库 grep -rn "SocketChannel|Selector|java.nio.channels" src/main 无命中,全部是 java.net.Socket / ServerSocket / DatagramSocket 阻塞式 BIO |
Java 版本要求:Java 8(pom.xml:24-25),并大量使用 Java 8 特性(stream、CompletableFuture、LocalDateTime、BiConsumer、IntStream)。
评价:这是一个"零运行时依赖(除 slf4j-api)+ 纯 BIO"的库。好处是引入成本极低、无依赖冲突;代价是性能上限和并发模型被
java.net.Socket锁死,这一点在第二篇会展开。
1.3 许可证与自我定位
pom.xml:13-20 的 <description> 已经把边界写得很清楚:
14→ 目前它只是一个物联网通信的工具,包含
15→ 1、西门子S7通信协议,支持西门子S1500,S1200,S400,S300,S200Smart,西门子机床828D;
16→ 2、Modbus通信协议,支持ModbusTcp, ModbusRtuOverTcp, ModbusAsciiOverTcp, ModbusTcpServer;
17→ 3、三菱Melsec(MC)通信协议,支持PLC iQ-R系列, Q/L系列, QnA系列, A系列, 目前只测试了L系列和FX5U;
18→ 4、RTSP, RTCP, RTP, H264, MP4 (FMP4)协议,RTSP + H264 + FMP4 + WebSocket + MSE + WEB;
19→ 5、基础字节数组解析转换工具;
许可证 MIT(pom.xml:76-82),作者要求"使用时添加作者的版权许可声明"(README-CN.md:61)——这一点在企业项目引入时需要注意合规。
2. 源码规模统计
实测(在 src/main/java/com/github/xingshuangs/iot 下):
| 模块 | 主包 java 文件数 | 说明 |
|---|---|---|
protocol/s7 |
66 | 西门子 S7,最大模块 |
protocol/rtsp |
62 | RTSP + SDP + 摘要认证 |
protocol/melsec |
40 | 三菱 MC |
protocol/mp4 |
40 | FMP4 盒结构(box)模型 |
protocol/modbus |
32 | Modbus |
protocol/rtp |
26 | RTP + H264 payload |
protocol/rtcp |
16 | RTCP |
common |
16 | Buff / 序列化 / 算法 |
exceptions |
11 | 异常体系 |
utils |
13 | 字节/CRC/LRC/MD5/Hex 等工具 |
net |
9 | Socket + 客户端/服务端基类 |
App.java |
1 | 占位入口 |
| 合计(main) | 332 | |
| 合计(test) | 131 | |
| 总计 | 463 | 与题目给出的 463 一致 |
单文件规模 TOP 5(能看出复杂度集中在哪):
1522 protocol/modbus/service/ModbusSkeletonAbstract.java ← Modbus API 门面,类型矩阵爆炸
1440 protocol/s7/service/S7PLC.java ← S7 API 门面
931 protocol/melsec/service/McNetwork.java ← MC 批量读写 + 分组
795 protocol/melsec/service/McPLC.java ← MC API 门面
521 protocol/melsec/enums/EMcDeviceCode.java ← 软元件代码表
观察:三个协议都是「一个瘦网络基类 + 一个胖 API 门面类」的形态,门面类 700~1500 行,方法数量在 60~100 之间(readInt16/readUInt16/readInt32/... × 单地址/多地址重载)。这是典型的"平铺式 API"设计,与之相对的是 HslCommunication 的 ReadInt16(string address) 单一入口 + OperateResult<T> 包装。
3. 包结构全景
3.1 ASCII 目录树
com.github.xingshuangs.iot
├── App.java 占位 main
├── common/ 【基础设施层】协议无关
│ ├── IObjectByteArray.java 对象 → 字节数组(byteArrayLength/toByteArray)
│ ├── IObjectString.java 对象 → 协议字符串(RTSP 报文用)
│ ├── buff/ 字节读写缓冲
│ │ ├── ByteReadBuff.java 读缓冲(游标 + 随机索引双模式)
│ │ ├── ByteWriteBuff.java 写缓冲(链式 putXxx)
│ │ ├── BitWriteBuff.java 位写入缓冲(线圈/位软元件拼包)
│ │ └── EByteBuffFormat.java 4/8 字节字序格式 AB_CD/BA_DC/CD_AB/DC_BA
│ ├── constant/GeneralConst.java 端口、REQ/ACK 标签等常量
│ ├── enums/EDataType.java 数据类型枚举 + 字节长度
│ ├── algorithm/
│ │ ├── LoopGroupAlg.java 循环分组算法(单组 / 双组)
│ │ └── LoopGroupItem.java 分组游标项
│ └── serializer/ 注解驱动的字节映射
│ ├── IByteArraySerializable.java 对象 ↔ byte[]
│ ├── IPLCSerializable.java PLC 对象读写
│ ├── ByteArraySerializer.java 注解解析实现
│ ├── ByteArrayVariable.java @ByteArrayVariable 注解
│ ├── ByteArrayParameter.java 参数化读写载体
│ └── ByteArrayParseData.java 解析中间态
├── exceptions/ 【异常层】11 个,全部 extends RuntimeException
│ ├── SocketRuntimeException / SocketTimeoutException
│ ├── S7CommException / ModbusCommException / McCommException
│ ├── RtspCommException / RtcpCommException / RtpCommException
│ └── ByteArrayParseException / HexParseException / AuthenticationException
├── net/ 【传输层】协议无关
│ ├── ICommunicable.java close/write/read 四个方法
│ ├── SocketUtils.java read/write/close/isConnected 静态实现
│ ├── IAckListener.java 异步响应监听(ok/fail)
│ ├── AckCallback.java 响应回调 + wait/notify
│ ├── client/TcpClientBasic.java TCP 客户端基类(BIO)
│ ├── client/UdpClientBasic.java UDP 客户端基类
│ ├── client/UdpClientSample.java UDP 示例
│ ├── server/TcpServerBasic.java TCP 服务端基类(一连接一线程)
│ └── server/UdpServerBasic.java UDP 服务端基类
├── protocol/
│ ├── modbus/ enums / model(25) / service(5)
│ ├── s7/ algorithm / constant / enums(15) / model(37) / serializer(4) / service(5) / utils
│ ├── melsec/ enums(4) / model(32) / service(4)
│ ├── rtsp/ authentication / constant / enums / model / service
│ ├── rtp/ enums / model/frame / model/payload(16) / service
│ ├── rtcp/ enums / model / service
│ └── mp4/ enums / model(39) ← FMP4 box 结构
└── utils/ ByteUtil/ShortUtil/IntegerUtil/LongUtil/FloatUtil/
BCDUtil/BooleanUtil/CRCUtil/LRCUtil/MD5Util/
HexUtil/StringSpUtil/TimesUtil
3.2 各包职责表
| 包 | 职责 | 是否协议相关 | 关键抽象 |
|---|---|---|---|
common |
字节缓冲、数据类型、序列化、通用算法 | 否(S7 serializer 例外) | IObjectByteArray / ByteReadBuff |
common.buff |
一切报文序列化的底层载体 | 否 | ByteWriteBuff 链式 API |
common.serializer |
注解驱动的 POJO ↔ byte[] | 半(S7 特化在 s7 包) | @ByteArrayVariable |
common.algorithm |
批量读写的分包/合包算法 | 否 | LoopGroupAlg |
exceptions |
统一 unchecked 异常体系 | 半(按协议分) | 见 §4.5 |
net |
Socket 生命期 + 读写原语 | 否 | ICommunicable |
protocol.modbus |
Modbus TCP/RTU/ASCII/服务端 | 是 | ModbusSkeletonAbstract |
protocol.s7 |
S7 客户端/服务端/NCK/上传下载 | 是 | PLCNetwork → S7PLC |
protocol.melsec |
三菱 MC 1E/3E/4E 帧 | 是 | McNetwork → McPLC |
protocol.rtsp/rtp/rtcp/mp4 |
流媒体(RTSP 拉流 → RTP → H264 → FMP4 → WebSocket) | 是 | RtspNetwork |
utils |
纯函数工具(无状态) | 否 | CRCUtil / LRCUtil |
4. 顶层接口设计
整个库只有 6 个顶层接口,全部是极小的"标记 + 契约"接口。这种"接口极少、实现很厚"的风格是它的核心特征。
┌──────────────────────┐ ┌───────────────────────────┐
│ ICommunicable │ │ IObjectByteArray │
│ (net) │ │ (common) │
├──────────────────────┤ ├───────────────────────────┤
│ + close() │ │ + byteArrayLength(): int │
│ + write(byte[]) │ │ + toByteArray(): byte[] │
│ + write(b[],o,l) │ └───────────────────────────┘
│ + read(byte[]): int │ ▲
│ + read(b[],o,l): int │ │ 几乎所有 model 类实现
└──────────────────────┘ │ (MbapHeader/TPKT/COTP/
▲ │ McHeaderReq/S7Data...)
│ 实现 │
┌────────────┴────────────┐ │
│ │ │
TcpClientBasic UdpClientBasic │
▲ ▲ │
│ extends │ extends │
┌──┴──────────────┐ ┌──────┴─────────┐ │
│ PLCNetwork (S7) │ │ RtpUdpClient │ │
│ McNetwork (MC) │ │ RtcpUdpClient │ │
│ ModbusSkeleton │ └────────────────┘ │
│ RtspNetwork │ │
└─────────────────┘ │
│
┌────────────────────────────┐ │
│ IByteArraySerializable │ │
├────────────────────────────┤ │
│ + <T> T toObject(Class,byte[])◄──────────────┘
│ + <T> byte[] toByteArray(T) │
└────────────────────────────┘
▲
│ implements: ByteArraySerializer
┌───────────┴──────────────────────────┐
│ IPLCSerializable │
├──────────────────────────────────────┤
│ + <T> T read(Class<T>) │ ← S7Serializer 实现
│ + <T> void write(T) │
└──────────────────────────────────────┘
┌────────────────────────────┐ ┌────────────────────────┐
│ IObjectString │ │ IAckListener<T> │
├────────────────────────────┤ ├────────────────────────┤
│ + toObjectString(): String │ │ + ok(T ack) │
└────────────────────────────┘ │ + fail(String message) │
↑ RTSP 文本报文用 └────────────────────────┘
↑ AckCallback<T> 持有
4.1 ICommunicable —— 传输层抽象
net/ICommunicable.java:34-79:
34→public interface ICommunicable {
35→
40→ void close();
48→ void write(final byte[] data);
58→ void write(final byte[] data, final int offset, final int length);
67→ int read(final byte[] data);
78→ int read(final byte[] data, final int offset, final int length);
79→}
设计意图:把"通信端点"抽象成 close / write / read 五个动作,让上层协议类只依赖这个接口而与 TCP/UDP 解耦。
实际效果:只有 TcpClientBasic 和 UdpClientBasic 两个实现,且 UDP 的 read(byte[]) 语义(阻塞收一个报文)与 TCP 的(读满 len 字节)并不完全一致——接口在语义上是"弱契约",靠使用者自己知道具体类型。这是该接口最主要的设计缺陷:没有 connect(),也没有超时参数的统一表达,导致 TcpClientBasic 上真正被协议层调用的是带 4~6 个参数的重载(见第二篇),接口本身几乎只是个装饰。
4.2 IObjectByteArray —— 报文自描述契约
common/IObjectByteArray.java:34-51:
34→public interface IObjectByteArray {
42→ int byteArrayLength();
50→ byte[] toByteArray();
51→}
设计意图:这是本项目最有价值的一个接口。任何一个协议报文对象都必须能回答两个问题:我有多长、我的字节是什么。有了 byteArrayLength(),外层就可以先算总长、再一次性分配 buffer,避免了流式的 ByteArrayOutputStream 反复扩容。
典型用法(protocol/s7/model/S7Data.java:84-103):
85→ public byte[] toByteArray() {
86→ ByteWriteBuff buff = ByteWriteBuff.newInstance(this.byteArrayLength());
87→ if (this.tpkt != null) { buff.putBytes(this.tpkt.toByteArray()); }
90→ if (this.cotp != null) { buff.putBytes(this.cotp.toByteArray()); }
93→ if (this.header != null) { buff.putBytes(this.header.toByteArray()); }
96→ if (this.parameter != null) { buff.putBytes(this.parameter.toByteArray()); }
99→ if (this.datum != null) { buff.putBytes(this.datum.toByteArray()); }
102→ return buff.getData();
103→ }
评价:这是一个非常干净的"组合式报文"模型——TPKT + COTP + Header + Parameter + Datum 层层嵌套,每层各自 byteArrayLength() 与 toByteArray(),父层只是拼接。这个模式在本库里被 Modbus / S7 / MC 三个协议一致采用,是全库最一致、最值得学习的设计。
与之配合的是每个 model 类的静态 fromBytes(byte[] data) / fromBytes(byte[] data, int offset) 工厂方法——序列化走接口,反序列化走静态工厂(因为 Java 接口无法约束静态方法)。这是一个无奈但普遍接受的做法。
4.3 IByteArraySerializable —— 对象 ↔ 字节数组
common/serializer/IByteArraySerializable.java:34-56:
34→public interface IByteArraySerializable {
45→ <T> T toObject(Class<T> targetClass, byte[] src);
55→ <T> byte[] toByteArray(T targetBean);
56→}
设计意图:把"一个 POJO 按字节偏移量映射到一个 byte[]"这件事做成通用能力。配合 @ByteArrayVariable 注解使用:
// common/serializer/ByteArrayVariable.java:37-88
40→public @interface ByteArrayVariable {
48→ int byteOffset() default 0; // 字节偏移
56→ int bitOffset() default 0; // 位偏移(仅 BOOL)
64→ int count() default 1; // 数量,>1 时字段必须是 List
71→ EDataType type() default EDataType.BYTE;
79→ boolean littleEndian() default false;
87→ EByteBuffFormat format() default EByteBuffFormat.DC_BA;
88→}
这是本库最具特色的功能(第四篇详细展开):定义一个 Java Bean 就能把一段 PLC 数据区(或任意字节流)直接映射成对象,等价于 C# 里的 struct layout + Marshal。
4.4 IPLCSerializable —— 直接读写 PLC 对象
common/serializer/IPLCSerializable.java:34-54:
34→public interface IPLCSerializable {
44→ <T> T read(Class<T> targetClass);
53→ <T> void write(T targetBean);
54→}
比 IByteArraySerializable 更进一步:连中间 byte[] 都不需要,直接从 PLC 读出一个对象。由 protocol/s7/serializer/S7Serializer.java 实现(配合 @S7Variable(address=..., type=..., count=...))。
⚠️ 注意不对称:只有 S7 有
S7Serializer(protocol/s7/serializer/4 个文件),Modbus 和 MC 没有对应的序列化器(源码中未见)。也就是说"注解驱动的 PLC 对象读写"目前是 S7 独占能力。
4.5 异常体系
exceptions/ 下 11 个类全部 extends RuntimeException(unchecked),且彼此之间没有继承关系,是一张平铺的列表:
| 异常 | 触发场景 |
|---|---|
SocketRuntimeException |
Socket IO 失败、连接失败(TcpClientBasic 包装 IOException) |
SocketTimeoutException |
Socket 超时(源码中未见实际抛出点,仅定义) |
S7CommException |
S7 协议层错误:握手失败、PDU 编号不一致、returnCode 非 SUCCESS |
ModbusCommException |
Modbus 层错误:功能码不匹配、CRC/LRC 校验失败、长度不一致 |
McCommException |
MC 层错误:副帧头不一致、endCode != 0、软元件限制 |
RtspCommException / RtcpCommException / RtpCommException |
流媒体模块 |
ByteArrayParseException |
注解序列化参数非法、类型不可识别 |
HexParseException |
Hex 字符串解析、EByteBuffFormat 未实现 |
AuthenticationException |
RTSP 摘要认证失败 |
评价:平铺 + unchecked 的好处是调用方代码不用写 throws(对工业采集脚本很友好);坏处是调用方无法用一个 catch 捕获"所有通信异常",必须逐个枚举,也拿不到统一的错误码。相比之下 Hsl 的 OperateResult<T>(含 IsSuccess + ErrorCode + Message)在错误可编程处理上更强。
4.6 IAckListener / AckCallback —— 未被使用的异步机制
net/IAckListener.java:34-51 与 net/AckCallback.java:39-160 定义了一套"异步响应 + 超时等待"机制:
// net/AckCallback.java:101-111
101→ public void responseOk(T ackPackage) {
102→ this.ackData = ackPackage;
103→ this.completed = true;
104→ this.errorMessage = "";
105→ synchronized (this.lock) {
106→ this.lock.notifyAll();
107→ }
108→ if (this.listener != null) {
109→ CompletableFuture.runAsync(() -> this.listener.ok(ackPackage));
110→ }
111→ }
重要发现:全库 grep -rn "AckCallback\|IAckListener" src/ 除了这两个文件本身,没有任何引用点。也就是说这套异步回调机制是已定义但未接线的(dead code)。当前所有协议交互都是同步阻塞式的。这在第四篇会再次提到。
5. 分层架构与调用链
5.1 四层结构
┌──────────────────────────────────────────────────────────────┐
│ L4 门面 API 层 │
│ S7PLC / McPLC / ModbusTcp / ModbusRtuOverTcp / │
│ ModbusAsciiOverTcp / RtspFMp4Proxy │
│ · 地址字符串 → 请求项 │
│ · 数据类型读写(readInt16/readFloat32/writeString...) │
│ · 长短连接策略(persistence) │
├──────────────────────────────────────────────────────────────┤
│ L3 协议报文层(model 包,全部 implements IObjectByteArray) │
│ S7Data / TPKT / COTP / Header / RequestItem / DataItem │
│ McMessageReq / McHeader4EReq / McDeviceContent │
│ MbTcpRequest / MbapHeader / MbPdu / MbRtuRequest │
│ · toByteArray() 组装 · fromBytes() 拆解 │
│ · selfCheck() 回填长度/CRC/LRC │
├──────────────────────────────────────────────────────────────┤
│ L2 网络基类层(net 包) │
│ TcpClientBasic · UdpClientBasic │
│ TcpServerBasic · UdpServerBasic │
│ SocketUtils(read/write/close 静态原语) │
├──────────────────────────────────────────────────────────────┤
│ L1 基础设施层(common / utils / exceptions) │
│ ByteReadBuff / ByteWriteBuff / BitWriteBuff │
│ EDataType / EByteBuffFormat / LoopGroupAlg │
│ ByteArraySerializer / S7Serializer │
└──────────────────────────────────────────────────────────────┘
▲
│ JDK: java.net.Socket / ServerSocket / DatagramSocket
5.2 一次 S7 读操作的完整调用链
s7PLC.readInt16("DB1.0")
│
├─► AddressUtil.parseByte("DB1.0", 2) → RequestItem{area=DATA_BLOCKS, dbNumber=1, ...}
│
├─► S7PLC.readByte(address, count) S7PLC.java:135
│ └─► PLCNetwork.readS7Data(RequestItem) PLCNetwork.java:435
│ │
│ ├─► S7SequentialGroupAlg.readRecombination(...) ←── PDU 分包算法
│ │
│ └─► for each S7ComGroup:
│ ├─► S7Data.createReadRequest(items)
│ ├─► readFromServer(S7Data) PLCNetwork.java:228
│ │ └─► readFromServer(byte[]) PLCNetwork.java:243
│ │ ├─► locker.lock()
│ │ ├─► TcpClientBasic.write(sendData)
│ │ ├─► TcpClientBasic.read(data[4]) ← 先读 TPKT
│ │ ├─► TPKT.fromBytes(data) → length
│ │ ├─► TcpClientBasic.read(total, 4, length-4) ← 再读剩余
│ │ └─► locker.unlock()
│ ├─► S7Data.fromBytes(total) ← 反序列化
│ └─► checkPostedCom(req, ack) ← 校验 PDU 引用 + returnCode
│
└─► ByteReadBuff.getInt16() → short
注意链路上 两处"两阶段读"(先读定长头,解析长度,再读剩余体)——这正是本库处理 TCP 粘包的核心手法,第二篇会详细分析。
6. 三大协议族的统一抽象对比
虽然三个协议各自实现,但在"骨架"上高度一致:
| 维度 | Modbus | S7 | MC (Melsec) |
|---|---|---|---|
| 网络基类 | ModbusSkeletonAbstract<T,R> extends TcpClientBasic |
PLCNetwork extends TcpClientBasic |
McNetwork extends TcpClientBasic |
| API 门面 | ModbusTcp / ModbusRtuOverTcp / ModbusAsciiOverTcp |
S7PLC extends PLCNetwork |
McPLC extends McNetwork |
| 报文顶层类 | MbTcpRequest / MbRtuRequest / MbAsciiRequest |
S7Data |
McMessageReq / McMessageAck |
| 长度字段位置 | MBAP 第 4-5 字节,= PDU长度 + 1(unitId) |
TPKT 第 2-3 字节,= 整帧长度 |
3E/4E 帧 dataLength,= 2 + data长度 |
| 校验 | TCP 无 / RTU CRC16 / ASCII LRC | 无(靠 TPKT 长度) | 无(靠 endCode) |
| 事务标识 | transactionId,AtomicInteger 自增且 ≥65536 归零 |
pduReference,同机制 |
4E 帧 serialNumber;1E/3E 无 |
| 地址解析 | 直接 int(0~65535) | AddressUtil.parse() 正则 + 分段 |
McDeviceAddress.createBy() 前缀匹配软元件码 |
| 批量合并 | 无(相邻地址不合包) | 有:MultiAddressRead + 顺序分组算法 |
有:McMultiAddressRead(随机读 word/dword) |
| 自动分包 | 有:LoopGroupAlg.loopExecute |
有:S7SequentialGroupAlg |
有:LoopGroupAlg.biLoopExecute |
| 服务端 | 有:ModbusTcpServer |
有:S7PLCServer |
无(源码中未见) |
| 注解序列化 | 无 | 有:S7Serializer |
无 |
| 枚举数量 | 2 | 15 | 4 |
一致性最高的三处:
- 长度回填都走
selfCheck()——MbTcpRequest.selfCheck()(model/MbTcpRequest.java:79-87)、McMessageReq.selfCheck()(model/McMessageReq.java:79-85)、S7Data.selfCheck()(model/S7Data.java:109-123)都是"先算子项长度,再回填到头部长度字段"。 - 都使用
ReentrantLock locker保护一次请求-响应往返。 - 都有
persistence字段控制长/短连接(唯一例外是ModbusTcpServer/S7PLCServer这类服务端)。
一致性最低的一处:服务端支持——Modbus 和 S7 有,MC 没有。
7. 与 HslCommunication 的设计取向差异
以下为基于本库源码观察 + 对 Hsl 公开 API 形态的对比;Hsl 侧为业界共识性描述,细节以实测为准(需实测确认)。
| 维度 | iot-communication | HslCommunication |
|---|---|---|
| 返回值风格 | 直接返回值,失败抛 unchecked 异常 | OperateResult<T>:;IsSuccess / Content / ErrorCode / Message |
| 错误处理 | 11 个平铺 unchecked 异常,无法统一 catch | 单一返回类型 + 错误码,可用 if (!result.IsSuccess) 判断 |
| 协议支持广度 | 3 个 PLC 协议 + RTSP 流媒体 | 20+ 协议(西门子/三菱/欧姆龙/AB/Modbus/...) |
| 依赖 | 仅 slf4j-api(运行时) | 亦为低依赖,但体积与类数量大得多 |
| 传输实现 | 纯 BIO(java.net.Socket),服务端一连接一线程 + 固定线程池 |
亦为同步 Socket 为主,但提供了更完整的长短连接与重连策略 |
| 报文模型 | IObjectByteArray 接口 + fromBytes() 静态工厂,组合式嵌套 |
多为 byte[] 手工拼装 + SoftBasic 静态工具方法 |
| 粘包处理 | 两阶段读(先定长头 → 解析长度 → 读剩余),;对无长度字段的协议退化成"读到不满即止" | INetMessage 接口 + 两阶段:;GetContentLength() 后按长度读(理念相近) |
| 地址表达式 | S7:DB1.0.1 / M1.1 / V100;;MC:D100 / XFF(支持十六进制) |
类似,另有 OperateResult 包装的解析校验 |
| 对象映射 | @ByteArrayVariable / @S7Variable 注解驱动,;可一次 read 出整个 Bean |
需手工读 byte[] 再用 ByteTransform 逐字段转换 |
| 批量优化 | S7 有顺序分组算法;MC 有双组循环;;Modbus 无相邻地址合并 | 提供了按地址分组的批量读能力(实现不同) |
| 文档/社区 | 中文 README + 5 篇 tutorial + QQ 群 | 中文文档完善,社区更大 |
| 代码风格 | 大量 Lombok @Data;注释中英双语;;门面类方法重载爆炸(60~100 个) |
方法数量更收敛,泛型 + 结果包装复用度高 |
7.1 核心差异:错误是可抛的还是可查的
这是两者最本质的分歧:
- iot-communication 选择"抛异常":
readInt16("DB1.0")直接返回short,读失败抛S7CommException。代码最短,但在采集循环里必须靠 try-catch 兜住,否则一次掉线就把线程打挂;也无法表达"部分成功"。 - Hsl 选择"返回值":
OperateResult<short>需要显式判成功。代码啰嗦,但天然适合采集场景(一次失败不影响下一轮,还能记录错误码)。
我的判断:工业采集场景下 Hsl 的取向更稳妥。iot-communication 的取向更适合"写 Demo / 做协议学习"——这也和它 README 里"主要用于相关协议的学习和开发"(README-CN.md:15)的自我定位完全吻合。
7.2 核心差异:协议建模的严谨度
iot-communication 把每个协议的每一层都做了独立的 model 类(S7 就有 TPKT / COTP / COTPConnection / COTPData / Header / AckHeader / Parameter / ReadWriteParameter / Datum / DataItem / RequestItem / ReturnItem... 共 37 个),每个字段都带"字节大小 / 字节序数"注释。例如 protocol/s7/model/COTPConnection.java:73-78:
73→ /**
74→ * Parameter code tpdu size.
75→ * 参数代码TPDU-Size <br>
76→ * 字节大小:1 <br>
77→ * 字节序数:7
78→ */
这种"逐字节注释"的写法在 Hsl 里比较少见(Hsl 更多是 byte[] buffer = new byte[]{0x03, 0x00, ...} 直接写常量数组)。
结论:iot-communication 更像教学型/可读型实现(报文结构显式建模,适合学习与改造);Hsl 更像工程型实现(API 统一、错误可控、协议覆盖广)。
8. 设计优点 / 缺点 / 可借鉴点
✅ 设计优点
- 零运行时依赖:只依赖
slf4j-api,接入任意 Java 8 工程零冲突。对工业现场那种"jar 包要拷进去、不能联网下依赖"的环境极其友好。 IObjectByteArray的"自描述长度"契约:byteArrayLength()+toByteArray()让报文组合变成纯粹的拼接,避免了流式扩容,也让selfCheck()回填长度成为可能。这是全库最漂亮的一处抽象。- 逐字节注释的报文模型:每一层协议头都有明确的"字节大小 / 字节序数"标注,可读性远超魔数数组,非常适合作为协议学习材料。
- 注解驱动的字节映射(
@ByteArrayVariable/@S7Variable):把 PLC 数据区直接映射成 Java Bean,是 C#struct + Marshal在 Java 里很自然的一个落地,工业场景实用价值很高。 - 协议无关的算法层:
LoopGroupAlg/S7SequentialGroupAlg被 Modbus / S7 / MC 复用,分包逻辑没有散落在协议代码里。 - 回调留痕:
BiConsumer<String, byte[]> comCallback统一提供 REQ/ACK 报文观测能力,抓包调试不用开 Wireshark。
❌ 设计缺点
- 异常体系平铺且 unchecked:11 个异常无共同父类型,无法统一 catch;
SocketTimeoutException定义了却未被使用(源码中未见抛出点)。 ICommunicable形同虚设:只有 5 个方法,缺少connect(),真正被用的是TcpClientBasic上 4~6 参数的read/write重载。TCP/UDP 的read语义也不一致。IAckListener/AckCallback是死代码:定义了完整的异步回调 + 超时等待机制,全库零引用。- 门面类方法爆炸:
ModbusSkeletonAbstract1522 行、S7PLC1440 行,大量readInt16/readUInt16/readInt32/...× 单地址/多地址/字节序/字序的重载,维护成本高。 - 能力不对称:注解序列化只有 S7 有;服务端只有 Modbus/S7 有;MC 缺失。
- BIO + 一连接一线程:服务端用
Executors.newFixedThreadPool(availableProcessors()),连接数被 CPU 核数硬限制(ModbusTcpServer里maxAvailableNumber = availableProcessors())。 - Lombok
provided作用域:编译期注解,使用者无感知,但阅读源码时 getter/setter 是隐式的,对新人有一定门槛。
💡 可借鉴点
| 可借鉴的做法 | 适用场景 |
|---|---|
byteArrayLength() + toByteArray() 的自描述报文契约 |
任何需要手写二进制协议的场景 |
selfCheck() 统一回填长度/校验码 |
避免"改了 body 忘了改 header length"的经典 bug |
| 逐字节注释(字节大小 + 字节序数) | 协议建模文档化,新人 10 分钟看懂报文结构 |
EByteBuffFormat(AB_CD/BA_DC/CD_AB/DC_BA)显式建模字序 |
西门子/三菱混用场景下的 32 位浮点/整数乱码问题 |
分组算法(单组 loopExecute / 双组 biLoopExecute)独立成 common.algorithm |
批量读写自动分包,与协议解耦 |
comCallback 报文观测钩子 |
现场排障,无需抓包工具 |
枚举内部 Holder 静态内部类做懒加载 Map(EDataType.java:88-98、EMcFrameType.java:59-69) |
高频 from(...) 查表且避免静态初始化顺序问题 |
下一篇:网络层与粘包处理 —— 深入 TcpClientBasic / SocketUtils / TcpServerBasic / UdpClientBasic,并对本库的粘包处理做专项评价。
本文是《工业通信与数据采集》栏目第 14/22 篇 · 上一篇:协议实现与字节转换:ModBus / Siemens / Melsec 与 Core.Transfer | 栏目总览 | 下一篇:网络层与粘包处理