01 · 项目总览与架构

本文是 xingshuangs/iot-communication 源码分析的第一篇:从 pom.xml 依赖、包结构、顶层接口三个切面建立对整个库的整体认知,并给出架构评价与横向对比。后续三篇分别深入网络层、三大 PLC 协议实现和基础设施。

本文目录


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

一致性最高的三处:

  1. 长度回填都走 selfCheck()——MbTcpRequest.selfCheck()(model/MbTcpRequest.java:79-87)、McMessageReq.selfCheck()(model/McMessageReq.java:79-85)、S7Data.selfCheck()(model/S7Data.java:109-123)都是"先算子项长度,再回填到头部长度字段"。
  2. 都使用 ReentrantLock locker 保护一次请求-响应往返。
  3. 都有 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 核心差异:错误是可抛的还是可查的

这是两者最本质的分歧:

我的判断:工业采集场景下 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. 设计优点 / 缺点 / 可借鉴点

✅ 设计优点

  1. 零运行时依赖:只依赖 slf4j-api,接入任意 Java 8 工程零冲突。对工业现场那种"jar 包要拷进去、不能联网下依赖"的环境极其友好。
  2. IObjectByteArray 的"自描述长度"契约:byteArrayLength() + toByteArray() 让报文组合变成纯粹的拼接,避免了流式扩容,也让 selfCheck() 回填长度成为可能。这是全库最漂亮的一处抽象。
  3. 逐字节注释的报文模型:每一层协议头都有明确的"字节大小 / 字节序数"标注,可读性远超魔数数组,非常适合作为协议学习材料。
  4. 注解驱动的字节映射(@ByteArrayVariable / @S7Variable):把 PLC 数据区直接映射成 Java Bean,是 C# struct + Marshal 在 Java 里很自然的一个落地,工业场景实用价值很高。
  5. 协议无关的算法层:LoopGroupAlg / S7SequentialGroupAlg 被 Modbus / S7 / MC 复用,分包逻辑没有散落在协议代码里。
  6. 回调留痕:BiConsumer<String, byte[]> comCallback 统一提供 REQ/ACK 报文观测能力,抓包调试不用开 Wireshark。

❌ 设计缺点

  1. 异常体系平铺且 unchecked:11 个异常无共同父类型,无法统一 catch;SocketTimeoutException 定义了却未被使用(源码中未见抛出点)。
  2. ICommunicable 形同虚设:只有 5 个方法,缺少 connect(),真正被用的是 TcpClientBasic 上 4~6 参数的 read/write 重载。TCP/UDP 的 read 语义也不一致。
  3. IAckListener / AckCallback 是死代码:定义了完整的异步回调 + 超时等待机制,全库零引用。
  4. 门面类方法爆炸:ModbusSkeletonAbstract 1522 行、S7PLC 1440 行,大量 readInt16/readUInt16/readInt32/... × 单地址/多地址/字节序/字序的重载,维护成本高。
  5. 能力不对称:注解序列化只有 S7 有;服务端只有 Modbus/S7 有;MC 缺失。
  6. BIO + 一连接一线程:服务端用 Executors.newFixedThreadPool(availableProcessors()),连接数被 CPU 核数硬限制(ModbusTcpServer 里 maxAvailableNumber = availableProcessors())。
  7. 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 | 栏目总览 | 下一篇:网络层与粘包处理