01 · HslCommunication 与 iot-communication 横向对比与选型

本篇是「工业设备数据采集」知识体系第 06 篇的第 1 篇。定位:给你一个可执行的选型结论,而不是一份中立的功能清单。

前面 04、05 两篇已经分别把两个库拆开讲过一遍(内核、粘包、协议实现、工程特性)。本篇不再重复那些细节,只做横向对照 + 决策。凡是出现"详见 04 篇第 X 章 / 05 篇第 X 章"的地方,说明该结论的详细论证在那一篇。

本文所有数字与结论均来自两份实际源码:

_source/HslJavaLatest/              HslCommunication Java 版(294 个 .java 文件)
_source/iot-communication/          iot-communication v1.5.6(332 主 + 131 测试 .java 文件)

凡是我无法确定、或两家说法冲突的地方,一律标注 (需实测确认)。


本篇目录

章节 内容 一句话结论
1 两库档案表 体量与授权的第一眼差距
2 授权与商用风险(重点) 本篇最该先读的一章
3 架构对比 通信内核 / 线程 / 粘包 / 返回值 / 地址 / 序列化
4 API 易用性并排对比 同一任务的两种写法
5 性能考量 两者都是 BIO,别指望差一个量级
6 工程化对比 测试、异常、日志、可扩展性
7 选型决策树 一张图定场景
8 给 FastBee 的推荐结论 分层推荐
9 迁移路径与风险清单 换库的真实成本
10 小结 三句话

第 1 章 两库档案表

1.1 基础档案

维度 HslCommunication(Java 版) iot-communication
仓库 github.com/dathlin/HslCommunication github.com/xingshuangs/iot-communication
作者 Richard.Hu(dathlin),2017 至今 Oscura(xingshuang),2019 至今
Star 数量级 ≈ 2.1k(Fork ≈ 733) ≈ 280~300(Fork ≈ 91)
Maven 坐标 com.github.dathlin:HslCommunication com.github.xingshuangs:iot-communication:1.5.6
主语言 C#(主库,99%+),Java/Python 为附属客户端 Java 100%
JDK 要求 1.8(README 徽标 JDK-1.8.0) 1.8(pom maven.compiler.source=1.8)
维护状态 活跃(v12.x/v13.x 持续发布) 活跃(v1.5.6,2026-03 发布)
License 见第 2 章,情况复杂 MIT
构建文件 解出的源码树里没有 pom.xml(04 篇 §1.1 有说明) 标准 Maven 工程,pom.xml 齐备

Star 数量级说明:上面数字为检索时快照,会随时间变化,请以仓库实时数据为准。此处只表达"HSL 高一个数量级"这一相对关系。

1.2 协议覆盖对比

类别 HslCommunication iot-communication
Modbus TCP / RTU / ASCII / UDP / RTU-over-TCP / ASCII-over-TCP,含 Server TCP / RTU-over-TCP / ASCII-over-TCP,含 Server
西门子 S7Net(S7comm)、FetchWriteNet、PPI、S7Server、S7-200/300/400/1200/1500/200Smart S7(S7comm)、SINUMERIK 828D(NCK)、程序上传/下载、PLC 启停控制、S7Server
三菱 A1E、A1EAscii、McAscii、Mc、McR(iQ-R)、McUdp、McServer、FxLinks、FxSerial、A3C MC(A / QnA / Q_L / iQ-R),1E / 3E / 4E 帧,ASCII 与二进制
其它 PLC 欧姆龙、AB、倍福、松下、基恩士、台达、汇川、富士、GE、麦格米特、丰田、信捷、安川、横河、OpenProtocol —
机器人 FANUC、KUKA、EFORT、安川、ABB —
仪表 DLT645、DLT698、IEC104、CJT188、RKC 温控、梅特勒托利多 —
IT 协议 MQTT、WebSocket、Redis、FTP、HTTP、文件传输、DTU 透传 RTSP / RTCP / RTP / H264 / MP4(FMP4)
串口 有(Serial/) 无(仅 RTU-over-TCP)

结论:

1.3 依赖与体积

维度 HslCommunication iot-communication
编译期依赖 零(纯 JDK 手写,Core/Types/ 里连 BitConverter/MemoryStream/List 都自己实现了一份) slf4j-api 2.0.6(唯一的 compile 依赖)
provided — lombok 1.18.26
test — junit 4.13.2、slf4j-simple 2.0.6
传递依赖污染 无 极低(slf4j-api 是日志门面,几乎所有 Java 项目都有)

关于"零依赖"的准确表述(这点常被写错):

iot-communication 不是严格零依赖 —— 它有一个 compile 依赖:slf4j-api。
但 slf4j-api 只是日志门面(接口),不绑定任何实现,
运行时不引入任何额外 jar 也能工作(会用 NOP logger 静默)。
所以它"实质上接近零依赖",但不能说"零依赖"。

HSL Java 版才是真正的零依赖 —— 没有任何外部 jar。

源码规模(实测):

指标 HslJavaLatest iot-communication
main .java 文件数 294 332
test .java 文件数 0(test 目录为空) 131
帧解析器(INetMessage 实现) 33 个 无此抽象层

1.4 文档形态

维度 HslCommunication iot-communication
官网 hslcommunication.cn / hsltechnology.cn 无
API 文档 api.hslcommunication.cn 无
中文文档 官网文档站 + 演示程序(HslCommunicationDemo 含 1300+ 文件、24 大功能分类的 WinForm 界面) README + tutorial/ 下 6 份中文教程
"文档"的真实载体 官网 + Demo 界面 src/test 就是文档
论坛 bbs.hslcommunication.cn、Gitter QQ 群

iot 的"test 即文档"具体指什么:src/test/java/.../protocol/ 下有大量 Demo* 类(DemoS7Read、DemoS7Write、DemoS7Custom、DemoS7Control、DemoReadTest、McDemoReadTest)。它们不是断言型单测,而是带 main() 的可运行示例,逐个 API 演示一遍:

// _source/iot-communication/src/test/java/.../protocol/s7/service/DemoS7Read.java
S7PLC s7PLC = new S7PLC(EPlcType.S1200, "127.0.0.1");
boolean boolData = s7PLC.readBoolean("DB1.2.0");
List<Boolean> boolDatas = s7PLC.readBoolean("DB1.2.0", "DB1.2.1", "DB1.2.7");
int    intData   = s7PLC.readUInt16("DB14.0");
float  floatData = s7PLC.readFloat32("DB1.0");

我的评价:对"我要接一台 PLC"的工程师,test 即文档比官网文档更好用——能直接跑、能看到真实返回值。缺点是缺少系统性概念讲解(比如"为什么 S7 地址要 ×8"),这正是 03 协议篇补的。


第 2 章 授权与商用风险(重点,先读这一章)

这一章我花了最多时间去核实,因为网上关于 HSL 授权的说法大量互相矛盾。

2.1 事实清单(逐条来自源码/官方)

事实 A:随源码分发的 LICENSE 文件是 LGPL-3.0

_source/HslJava/LICENSE(165 行)

    GNU LESSER GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2017 - 2019 Richard.Hu <http://fsf.org/>

注意版权年份是 2017 - 2019。

事实 B:作者在 2019 年 8 月停止开源,转向订阅制

README 原话(英文):"...So from August 2019, no longer open source, turn to the subscription model..."

事实 C:代码里内置授权校验模块,未激活限时 8 小时

_source/HslJavaLatest/HslCommunication/Authorization.java 中,类名与方法名经混淆(如 nzugaydgwadawdibbas()、asdniasnfaksndiqwhawfskhfaiw()),但中文注释是原文:

// 如果需要移除验证,这里返回true即可。
// return true;

/**
 * 商业授权则返回true,否则返回false
 * @return 是否成功进行商业授权
 */
public static boolean asdniasnfaksndiqwhawfskhfaiw( ) { ... }

官方 README 的 C# 调用示例更直白:

// 授权示例 调用一次即可
if(!HslCommunication.Authorization.SetAuthorizationCode( "你的激活码" )) {
    MessageBox.Show( "授权失败!当前程序只能使用8小时!" );
    return;
}

事实 D:官方授权页面区分"试用"与"商用"

试用授权: 加入技术支持VIP群 即可以获得激活码,支持长时间测试使用。
商用授权(赠送源代码): 公对公签订合同。公对公打款,开具增值税发票。
授权页面: hsltechnology.cn/Home/Licence?area=HslCommunication

2.2 我给出的判断(含不确定性)

问题 我的判断 置信度
仓库里的 LICENSE 写着 LGPL-3.0,是否意味着可以自由商用? 不能这么推断。 该文件版权年份止于 2019,与作者"2019 年 8 月停止开源"的声明在时间上吻合;且现行代码内置授权校验、官方页面另行售卖商用授权。二者冲突时,以作者本人的现行声明为准。 高(逻辑一致)
HSL 的 Java 版是否有独立于 C# 主库的授权? 未知。官方授权页面按产品(HslCommunication)售卖,未看到 Java 版单独条款。 需向作者确认
用 LGPL-3.0 的旧版本(2019 年前发布的 jar)是否安全? 法理上更安全,但你拿到的是 7 年前的协议实现,且失去了后续修复。不建议作为规避手段。 中
iot-communication(MIT)商用是否有风险? 基本无风险,只需保留版权声明。README 里作者也特意写了"请严格遵守 MIT 协议,使用时加上作者的版权许可声明"。 高

⚠️ 给工程师的实操建议:这不是法律意见。如果 FastBee 这类项目要闭源商业化交付,在引入 HSL 之前,必须做一件事:发邮件给作者(hsl200909@163.com,官网公示)确认"Java 版用于商业闭源产品的授权口径与费用",把回复留档。这一步的成本远低于事后被追责。

2.3 授权对比总表

维度 HslCommunication iot-communication
文件声明 LGPL-3.0(2017-2019) MIT
现行实际口径 闭源商用授权,未激活限 8 小时 MIT,完全自由
商用是否需要付费 大概率需要(官方售卖商用授权) 不需要
是否需要开放自身源码 LGPL 下:修改库本身才需要;现行口径下按合同 不需要,仅需保留版权声明
是否内置运行时校验 是(Authorization.java) 否
风险等级(闭源商业产品) 🔴 高 🟢 低

第 3 章 架构对比

3.1 通信内核

维度 HslCommunication iot-communication
抽象方式 NetworkBase 抽象基类 + INetMessage 帧描述接口 TcpClientBasic 具体类 + SocketUtils 静态工具类
定帧职责 交给 INetMessage:ProtocolHeadBytesLength() + GetContentLengthByHeadBytes() 调用方自己算:先读固定头,再读 length,再调 read() 读剩余
帧解析器数量 33 个(Core/IMessage/ 下) 0(每个协议自己写两阶段读)
发送入口 ReadFromCoreServer(byte[] send) write(byte[] data) + read(byte[] data, ...)
I/O 模型 BIO(同步阻塞) BIO(同步阻塞)
Socket 选项 基本没设(04 篇 §6.1 穷尽式 grep 的结论) 有 SoTimeout、KeepAlive(05 篇 §3.2)

HSL 的内核模型(详见 04 篇 §3):

NetworkBase.ReceiveByMessage(socket, timeOut, netMessage)
        │
        ├─ 阶段 1:读 netMessage.ProtocolHeadBytesLength() 个字节
        │           ↓
        │     netMessage.setHeadBytes(headBytes)
        │           ↓
        ├─ 校验:netMessage.CheckHeadBytesLegal(token)
        │           ↓
        ├─ 阶段 2:contentLength = netMessage.GetContentLengthByHeadBytes()
        │          读 contentLength 个字节
        │           ↓
        └─ 返回 完整一帧 = headBytes + contentBytes

iot 的内核模型(详见 05 篇 §5):

SocketUtils.read(socket, data, offset, length, maxLength, timeout, waitForMore)
        │
        └─ while (count < length) {
               int len  = (maxLength<=0) ? length-count : min(maxLength, length-count);
               int num  = in.read(data, off, len);
               if (num < 0) throw new SocketException("...disconnected");
               count += num;  off += num;
               if (!waitForMore && num < len) break;   // ← 读短即止
           }

关键差异在 waitForMore:

waitForMore 语义 用途
false(默认) 只要某次 read 返回的字节数 < 请求长度,就认为"这波数据读完了" 普通两阶段读的第二阶段:我只想要这一帧的剩余部分
true 一定要读满 length 才返回,否则继续阻塞 分包/粘包场景,或者头部被拆成多次到达

3.2 线程模型

维度 HslCommunication iot-communication
客户端模型 一设备一对象,调用线程即 I/O 线程 一设备一对象,调用线程即 I/O 线程
内部是否起线程 NetworkDoubleBase 有后台重连/心跳线程(取决于配置) 客户端侧不起线程;TcpServerBasic 每连接一线程
并发安全 SimpleHybirdLock(04 篇 §10.1)锁住单次往返 synchronized / 对象锁,锁住整个往返(05 篇 §8.2)
连接池 无 无(05 篇 §8.3)
异步支持 有 async 系列方法 AckCallback/IAckListener 定义了但未使用(05 篇 §7.1)

两者共有的关键约束:因为都是同步阻塞 + 锁住整个往返,一个设备对象同一时刻只能有一个请求在飞。这对自研采集器的设计影响极大——见第 07 篇第 2 章的"设备串行"原则。

3.3 粘包处理方式

协议 HslCommunication iot-communication
Modbus TCP ModbusTcpMessage:头 8 字节,从 [4][5] 取 length;并带请求推断兜底(读寄存器时按 readLength*2+1 反推) MbTcpResponse:读 7 字节 MBAP,取 length,再读 length-1
Modbus RTU over TCP ModbusRtuMessage:无长度字段,靠"从站号+功能码推断长度" 读到满 1024 或读短即止(waitForMore=false)
S7 S7Message:头 4 字节 TPKT,取 length 读 TPKT(4) → length → 读剩余;响应头 12 字节再解析
MC 3E/4E MelsecQnA3EBinaryMessage:头 11 字节,取后续长度 McHeaderAck 分两步:先读固定头,再按 dataLength 读
MC 1E 靠 "副标题 + 软元件" 推断 无长度字段(03 篇 MC §9.7)

最大的一处工程差异:HSL 的 ModbusTcpMessage 里有一段手写的兜底逻辑(2025 年 7 月还在改):

// _source/HslJavaLatest/HslCommunication/Core/IMessage/ModbusTcpMessage.java
// 从请求帧反推"期望的响应长度",与 length 字段不一致时以请求为准
int readLength   = (SendBytes[10] & 0xff) * 256 + (SendBytes[11] & 0xff);
int expectLength = isBit ? (readLength - 1) / 8 + 2 : readLength * 2 + 1;
if (expectLength != remainLength && length == 6) {
    // 2025年7月23日 00:15:45  此处以读取的长度为准,剩余长度需要重新计算
    remainLength = expectLength;
}

这段代码的注释很直白——它是在给不规范的从站擦屁股。iot 没有这段,遇到写错 length 的从站会直接读错或超时。

3.4 返回值范式(最核心的设计分歧)

维度 HslCommunication iot-communication
范式 OperateResult 家族:返回对象,内含 IsSuccess / Message / ErrorCode / Content 抛异常 + 直接返回值
读一个 Int16 OperateResultExOne<Short> r = dev.ReadInt16("D100"); if (r.IsSuccess) ... else log(r.Message); short v = plc.readInt16("D100"); 失败直接抛 McCommException
家族成员 OperateResult、OperateResultExOne<T>、ExTwo<T1,T2>、ExThree、ExFour 无
错误码 三层:库级 / 协议级 / 设备级(04 篇 §10.2) 异常类型 + 异常消息
异常体系 也有 RemoteCloseException 等,但主路径不用 S7CommException / ModbusCommException / McCommException / SocketRuntimeException / ByteArrayParseException / HexParseException / AuthenticationException

代码对照:

// ============ HSL:必须显式判成功 ============
OperateResultExOne<Short> read = siemens.ReadInt16("M100");
if (read.IsSuccess) {
    short v = read.Content;
} else {
    log.warn("read failed: code={}, msg={}", read.ErrorCode, read.Message);
}
// ============ iot:正常路径就是一行 ============
try {
    short v = s7PLC.readInt16("M100");
} catch (S7CommException e) {
    log.warn("read failed: {}", e.getMessage());
}

我的判断(明确观点):

场景 更合适
采集器主循环(一次要读几百个点,部分失败不影响整体) HSL 的 OperateResult —— 不用 try/catch 包每层循环,可以直接收集"失败清单"
业务代码 / 一次性操作 iot 的异常 —— 代码短,不会被"忘记判 IsSuccess"坑
平台级 SDK 两者都行,但必须统一。混用是灾难

HSL 的范式有个真实的坑:Java 没有 C# 的 out 参数,OperateResultExOne<T> 是唯一出路,于是调用方很容易写成 short v = dev.ReadInt16("M100").Content; —— 一行拿到值,但完全忽略了 IsSuccess,网络断了会得到上一次的残留值或默认值。这是现场最难查的 bug 之一。

// ⚠️ 危险写法:编译通过、看起来很优雅,但断网时静默返回垃圾值
short v = melsec.ReadInt16("D100").Content;

工程建议:如果用 HSL,在代码规范里明令禁止上面的写法;或封装一层强制检查的 helper。如果用 iot,注意别用 catch (Exception e) {} 把 McCommException 吞掉——两者是同一类错误的两种表现形式。

3.5 地址解析

协议 HslCommunication iot-communication
Modbus "富地址"语法:s=2;x=3;100 支持站号、功能码前缀;ModbusInfo.TranslateToModbusAddress() 直接传数字地址 readHoldRegister(0, 4);不支持地址字符串
S7 瀑布 if-else(S7AddressData.java):DB1.100、M100、I0、Q0、V100(→ DB1)、AI0、AQ0、SM、T、C AddressUtil 正则解析,EArea 枚举映射;支持 DB1.2.0(字节.位)三段式
MC switch + 进制表(McAddressData.java),已知 bug:case 'D' 分支把大写 S 误判为 DY(源码 :95) EMcDeviceCode 枚举,McDeviceAddress 解析

一个关键差异:iot 的 S7 地址支持 DB1.2.0 这种"DB号.字节地址.位地址"三段式,而 HSL 的 DB1.2 第二段是字节地址还是位需要看上下文。这块最易踩坑,详见 03 篇 S7 §12。

3.6 序列化设计

维度 HslCommunication iot-communication
机制 手写:IDataTransfer 接口 + ParseSource/ToSource 由实现者自己写字节 反射 + 注解:@S7Variable / @ByteArrayVariable 标注字段,序列化器自动映射
字节序 IByteTransform 三实现:RegularByteTransform、ReverseBytesTransform、ReverseWordTransform EByteBuffFormat 枚举(ABCD/BADC/CDAB/DCBA),ByteReadBuff/ByteWriteBuff
自定义结构体 自己实现 IDataTransfer(04 篇 §6.2) 定义 POJO + 打注解(05 篇 §4)

代码对照(读取一个结构体):

// ===== HSL:手写 parse(节选自 04 篇 §6.2)=====
public class TankData implements IDataTransfer {
    public float temperature;
    public short  pressure;

    @Override
    public void ParseSource(byte[] source) {
        temperature = byteTransform.TransFloat(source, 0);
        pressure    = byteTransform.TransInt16(source, 4);
    }
    @Override public byte[] ToSource() { /* 反向写回 */ }
}
// ===== iot:注解(节选自 05 篇 §4.2)=====
public class DemoBean {
    @S7Variable(address = "DB1.0", type = EDataType.INT16)  private short value;
    @S7Variable(address = "DB1.2", type = EDataType.FLOAT32) private float temperature;
}
DemoBean bean = s7Serializer.read(DemoBean.class);      // 一行搞定

我的判断:


第 4 章 API 易用性并排对比

下面三个任务,两库各自给完整可编译代码。

4.1 任务一:读 Modbus 保持寄存器 40001 开始的 4 个字

// ==================== HslCommunication ====================
import HslCommunication.ModBus.ModbusTcpNet;
import HslCommunication.Core.Types.OperateResultExOne;

public class HslModbusDemo {
    public static void main(String[] args) {
        ModbusTcpNet modbus = new ModbusTcpNet("192.168.1.100", 502, (byte) 1);
        // 站号=1,地址从 0 开始(isAddressStartWithZero 默认 true)
        modbus.setAddressStartWithZero(true);

        OperateResultExOne<byte[]> read = modbus.Read("40001", (short) 4);
        if (read.IsSuccess) {
            byte[] data = read.Content;                    // 8 字节
            for (byte b : data) System.out.printf("%02X ", b);
        } else {
            System.out.println("failed: " + read.Message);
        }
        modbus.ConnectClose();
    }
}
// ==================== iot-communication ====================
import com.github.xingshuangs.iot.protocol.modbus.service.ModbusTcp;
import com.github.xingshuangs.iot.utils.HexUtil;

public class IotModbusDemo {
    public static void main(String[] args) {
        ModbusTcp plc = new ModbusTcp(1, "192.168.1.100", 502);
        // 可选:原始报文回调,调试神器
        plc.setComCallback((tag, bytes) ->
            System.out.printf("%s[%d] %s%n", tag, bytes.length, HexUtil.toHexString(bytes)));

        byte[] data = plc.readHoldRegister(0, 4);           // 注意:地址从 0 开始
        for (byte b : data) System.out.printf("%02X ", b);

        plc.close();
    }
}

对比点评:

点 HSL iot
地址写法 字符串 "40001",符合工控习惯 数字 0,需要自己换算
站号 构造函数第 3 参数 构造函数第 1 参数
报文调试 需要自己加 LogNet + 抓包 内置 setComCallback,一行打全报文 ⭐
错误处理 IsSuccess try/catch

setComCallback 是 iot 最实用的一项设计,现场排查"为什么读不到"时能省掉 80% 的 Wireshark 时间。这是我给 iot 加分的第一个理由。

4.2 任务二:读西门子 DB1 中偏移 0 开始的 4 个字(8 字节)

// ==================== HslCommunication ====================
import HslCommunication.Profinet.Siemens.SiemensS7Net;
import HslCommunication.Profinet.Siemens.SiemensPLCS;
import HslCommunication.Core.Types.OperateResultExOne;

public class HslS7Demo {
    public static void main(String[] args) {
        SiemensS7Net siemens = new SiemensS7Net(SiemensPLCS.S1200, "192.168.1.110");

        OperateResult connect = siemens.ConnectServer();
        if (!connect.IsSuccess) {
            System.out.println("connect failed: " + connect.Message);
            return;
        }

        OperateResultExOne<byte[]> read = siemens.Read("DB1.0", (short) 8);
        if (read.IsSuccess) {
            byte[] data = read.Content;                    // 8 字节
            for (byte b : data) System.out.printf("%02X ", b);
        } else {
            System.out.println("read failed: " + read.Message);
        }
        siemens.ConnectClose();
    }
}
// ==================== iot-communication ====================
import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType;
import com.github.xingshuangs.iot.utils.HexUtil;

public class IotS7Demo {
    public static void main(String[] args) {
        S7PLC s7PLC = new S7PLC(EPlcType.S1200, "192.168.1.110");
        s7PLC.setComCallback((tag, bytes) ->
            System.out.printf("%s[%d] %s%n", tag, bytes.length, HexUtil.toHexString(bytes)));

        byte[] data = s7PLC.readByte("DB1.0", 8);             // DB1,偏移 0,8 字节
        for (byte b : data) System.out.printf("%02X ", b);

        s7PLC.close();
    }
}

DB 区与位地址写法两库一致:

siemens.Read("DB1.0", (short) 8);   // HSL:DB1,偏移 0,8 字节
s7PLC.readByte("DB1.0", 8);          // iot:同上

siemens.ReadBool("DB1.0.3");         // HSL:DB1,字节 0,位 3
s7PLC.readBoolean("DB1.0.3");        // iot:同上

siemens.ReadBool("M0.3");            // HSL:M 区字节 0 的位 3
s7PLC.readBoolean("M0.3");           // iot:同上

⚠️ 但要警惕:两库的 TSAP 默认值不同(HSL 用 0x0100 PG 连接,iot 用 0x0300 S7 单边),详见 03 篇 S7 §5.2。这是"同一台 PLC,换库后一边能连一边不能连"的头号原因。

4.3 任务三:读三菱 D100 开始的 10 个字

// ==================== HslCommunication ====================
import HslCommunication.Profinet.Melsec.MelsecMcNet;
import HslCommunication.Core.Types.OperateResultExOne;

public class HslMcDemo {
    public static void main(String[] args) {
        MelsecMcNet melsec = new MelsecMcNet("192.168.1.120", 6000);

        OperateResult connect = melsec.ConnectServer();
        if (!connect.IsSuccess) {
            System.out.println("connect failed: " + connect.Message);
            return;
        }

        OperateResultExOne<byte[]> read = melsec.Read("D100", (short) 10);
        if (read.IsSuccess) {
            short[] values = melsec.getByteTransform().TransInt16(read.Content, 0, 10);
            for (short v : values) System.out.println(v);
        } else {
            System.out.println("read failed: " + read.Message);
        }
        melsec.ConnectClose();
    }
}
// ==================== iot-communication ====================
import com.github.xingshuangs.iot.protocol.melsec.service.McPLC;
import com.github.xingshuangs.iot.protocol.melsec.enums.EMcSeries;
import com.github.xingshuangs.iot.utils.HexUtil;

public class IotMcDemo {
    public static void main(String[] args) {
        McPLC mcPLC = new McPLC(EMcSeries.Q_L, "192.168.1.120", 6000);
        mcPLC.setComCallback((tag, bytes) ->
            System.out.printf("%s[%d] %s%n", tag, bytes.length, HexUtil.toHexString(bytes)));

        byte[] bytes = mcPLC.readBytes("D100", 10);           // 返回 20 字节
        System.out.println(HexUtil.toHexString(bytes));

        mcPLC.close();
    }
}

注意:EMcSeries 决定了帧类型和软元件编码方式(03 篇 MC §3.3)。选错系列的典型症状是 PLC 回 0xC059(命令/子命令不正确)。HSL 则是用不同的类来区分(MelsecMcNet / MelsecMcRNet / MelsecA1ENet),这是两种建模思路的差别:

HSL  :用「类」区分系列   → MelsecMcNet(QnA/3E) / MelsecMcRNet(iQ-R) / MelsecA1ENet(A/1E)
iot  :用「枚举」区分系列 → new McPLC(EMcSeries.Q_L | QnA | IQ_R | A, ...)

4.4 API 风格总结

维度 HSL iot 我更倾向
地址表达 字符串,贴近工控 数字/字符串混合 HSL(点表是人写的)
调用行数 多(要判 IsSuccess) 少 iot
调试报文 要外接 内置回调 ⭐ iot
系列区分 类 枚举 平手(枚举更省类)
方法命名 PascalCase(C# 风格) camelCase(Java 风格) iot(Java 项目里 PascalCase 很刺眼)
返回值 OperateResult 值 + 异常 看场景,见 §3.4

第 5 章 性能考量

5.1 先破除一个幻想

两个库都是 BIO(同步阻塞),都不使用 Netty。

✗ 不要期待"换一个库性能提升一个量级"——不可能
✗ 不要期待"一个线程管 1000 台设备"——不可能
✓ 真正的性能杠杆在:批量合并、轮询编排、连接复用

5.2 对比表

维度 HslCommunication iot-communication
I/O 模型 BIO BIO
连接复用 SetPersistentConnection() 长连接;默认短连接 长连接 + getAvailableSocket() 自动重连
连接池 无 无(05 篇 §8.3 明确指出)
锁粒度 单次往返 整个往返(含连接检查)
批量读 自动分包(Modbus 120 字 / 2000 位;S7 PDU−28) 自动分包(LoopGroupAlg / S7SequentialGroupAlg)
多地址合并 调用方传 Read(String[] address, short[] length) readInt16(String... addresses) 变参 + MultiAddressRead
S7 分包算法质量 两级切分(先分组再切片) 更精细:extraSize 显式建模(03 篇 S7 §13.3)

5.3 默认连接策略的差异(重要)

库 默认行为 后果
HSL 默认短连接:每次读写新建 TCP + 握手,用完断开 低频采集 OK;高频采集会产生大量 TIME_WAIT,且每次都要 S7 的 COTP + Setup 两次握手
iot 长连接 + 断线自动重连 高频采集友好;但需要自己处理"连接假死"

这是选型时最容易被忽略、但影响最大的一点。 如果你用 HSL 做 1 秒周期的 S7 采集且没开长连接,每秒会产生一次完整的 TCP 建连 + COTP CR/CC + Setup Communication,开销是"读一帧"的 3~4 倍。

// HSL 必须显式开长连接
siemens.SetPersistentConnection();

5.4 批量优化能力对比

优化手段 HSL iot
Modbus 自动分包 ✅ 120 字 / 2000 位(ModbusInfo.java:155-172) ✅
相邻地址合并 ⚠️ 调用方自己算 ⚠️ 部分自动
S7 多 item 一次读 ✅ Read(String[], short[]),硬限 19 item ✅ MultiAddressRead,按 PDU 动态算
MC 随机读(不连续地址) ✅ ReadRandom(String[]) ✅ McMultiAddressRead
跨协议统一批量 ❌ ❌

第 6 章 工程化对比

维度 HslCommunication iot-communication
单元测试 无(源码树里 test 目录为空) 131 个测试类,每个模型类都有 XxxTest
测试形态 — 混合:模型类是断言型单测;Demo* 是可运行示例
异常体系 零散(RemoteCloseException 等) 完整(7 个自定义异常,05 篇 §4.6 / §6)
日志 自带 LogNet 模块(文件/控制台,6 个等级) 只依赖 slf4j-api,交给宿主
报文调试 需通过 LogNet 或抓包 内置 comCallback ⭐
包命名 HslCommunication.*(非 Java 惯例,首字母大写) com.github.xingshuangs.iot.* ✅
构建 无构建文件 标准 Maven(含 source/javadoc/gpg/发布插件)
Lombok 不用(手写 getter/setter) 用(代码量显著减少)
可扩展性(新增协议) 实现 INetMessage + 继承 NetworkDeviceBase 实现 ICommunicable + IObjectByteArray

6.1 新增一个协议的真实成本(估算)

步骤 HSL iot
1. 定义报文模型类 手写 byte[] 拼接(通常无独立模型类) 建 model/XxxReq.java、XxxAck.java,实现 IObjectByteArray
2. 定帧 写一个 INetMessage 实现(约 30~60 行),之后内核自动处理粘包 自己写两阶段读,嵌在 service 类里
3. 读写 API 继承 NetworkDeviceBase,覆盖 Read/Write 继承 TcpClientBasic,写 readXxx/writeXxx
4. 地址解析 自己写 if-else / switch 自己写,或加进枚举
5. 测试 自己搭 照抄现有 XxxTest
总体 中等。粘包框架帮了大忙 中等。靠"照抄现有协议"走通

关键差异:HSL 的 INetMessage 让"怎么处理粘包"这件事可复用——新协议只要回答"头多长""剩下多长"两个问题。iot 每个协议都要把这段循环再写一遍(虽然也就 10 行)。这也是 HSL 架构上唯一明显优于 iot 的地方。

6.2 日志方案评价

HSL 自带 LogNet 模块(文件/控制台,6 个等级)不依赖任何日志框架,但与 Spring Boot 的 logback 是两套体系,要额外配置才能统一;iot 只依赖 slf4j-api,天然融入 Spring Boot 日志体系(Spring Boot 默认就有 logback 实现)。对 FastBee 而言,iot 的选择更合适。


第 7 章 选型决策树

                           ┌──────────────────────────┐
                           │  你要接什么设备/协议?     │
                           └────────────┬─────────────┘
                                        │
            ┌───────────────────────────┼───────────────────────────┐
            │                           │                           │
            ▼                           ▼                           ▼
   ┌─────────────────┐        ┌──────────────────┐        ┌──────────────────┐
   │ Modbus / S7 /MC │        │ 欧姆龙/AB/基恩士 │        │ OPC UA / 非标    │
   │ 三协议之内       │        │ 机器人/电表 ...  │        │ 私有 TCP 协议     │
   └────────┬────────┘        └────────┬─────────┘        └────────┬─────────┘
            │                           │                           │
            ▼                           ▼                           ▼
   ┌─────────────────┐          ┌──────────────┐          ┌──────────────────┐
   │ 闭源商用交付?   │          │ 基本只能选    │          │ OPC UA → 用      │
   └────┬──────┬────┘          │ HslCommunic- │          │ Eclipse Milo     │
        │      │               │ ation        │          │ 私有 → Netty 自研 │
      是│      │否             └──────────────┘          └──────────────────┘
        ▼      ▼
  ┌─────────┐ ┌──────────────┐
  │ 选 iot- │ │ 设备规模?    │
  │ communi-│ └──┬───────┬───┘
  │ cation  │    │       │
  │ (MIT)   │ ≤200台   >200台 或
  └─────────┘    │       周期<200ms
                 ▼        ▼
        ┌────────────┐ ┌──────────────┐
        │两者皆可    │ │ 上 Netty     │
        │推荐 iot    │ │ 自研内核     │
        │(MIT+干净)  │ │ (见 07 篇)   │
        └────────────┘ └──────────────┘

7.1 各分支的展开说明

分支 选什么 理由
只需要 Modbus/S7/MC + 闭源商用 iot-communication MIT 无后顾之忧;API 更干净
只需要三协议 + 内部自用/开源 两者皆可,略推荐 iot iot 的 comCallback 与注解序列化生产力更高
现场设备很野(不规范的从站) HSL ModbusTcpMessage 的兜底逻辑能"莫名其妙连上"
多品牌大杂烩 HSL 19 个品牌目录,iot 只有 3 个协议
需要串口直连(非 TCP) HSL iot 无串口模块
需要 RTSP 视频 iot HSL 无
需要西门子 828D 数控机床 / 程序上下传 / PLC 启停 iot HSL 无
≤ 200 台设备,周期 ≥ 500ms BIO 即可(见 07 篇 §10 性能预估) 一设备一线程完全够用
> 200 台,或周期 < 200ms Netty 自研内核 BIO 线程成本扛不住
要接 OPC UA Eclipse Milo 两库都不支持,别硬造
私有 TCP 协议 Netty + 自定义解码器 或 HSL 的 INetMessage(若坚持 BIO)

7.2 什么时候不该用这两个库

❌ 别用它们做 OPC UA        → Eclipse Milo(Java 事实标准)
❌ 别用它们做 MQTT 上行      → Eclipse Paho / HiveMQ MQTT Client
❌ 别用它们做高并发网关       → Netty
❌ 别用它们做硬实时控制       → 周期 < 10ms 的场合请上现场总线/运动控制器
❌ 别用它们做历史存储        → TDengine / InfluxDB / IoTDB

第 8 章 给 FastBee 的推荐结论

8.1 FastBee 采集器的现状与需求

项目:FastBee 物联网平台
采集器模块:fastbee-collector
协议需求:Modbus / S7 / MC(三种)
技术栈:Java / Spring Boot / Netty 或 BIO
上行:MQTT
存储:TDengine

三条硬约束:

约束 含义
协议边界明确 只要三个协议,不需要 19 个品牌
要长期演进 采集器是平台核心,不能绑死在一个有授权风险的库上
Spring Boot 宿主 库必须干净融入,不要自带一套日志/线程体系

8.2 我的推荐(分层结论)

┌───────────────────────────────────────────────────────────────┐
│  推荐:iot-communication 为主,HSL 为参考实现,SPI 隔离       │
└───────────────────────────────────────────────────────────────┘

具体分三层:

层 推荐 说明
① 驱动实现层 用 iot-communication MIT 授权无风险;三个协议齐全;comCallback 便于现场排查;test 是可运行文档
② 抽象层 自己定义 SPI 接口(见 07 篇第 2 章) ⭐ 这是最关键的一条。不要让业务代码直接依赖任何第三方库的类型
③ 参考/备选 读 HSL 源码 学它的 INetMessage 定帧抽象、分包算法;极端场景下可替换实现

8.3 为什么是"SPI 隔离"而不是"直接用"

因为无论选哪个库,直接依赖都是错的:

// ❌ 反例:业务代码直接依赖第三方类型
//    一旦要换库 / 库有 bug / 授权出问题,全平台跟着改
private S7PLC s7PLC = new S7PLC(EPlcType.S1200, ip);

// ✅ 正解:定义自己的驱动接口
public interface ProtocolDriver {
    void connect(DeviceConfig cfg) throws DriverException;
    byte[] read(TagConfig tag) throws DriverException;
    void write(TagConfig tag, byte[] value) throws DriverException;
    void close();
}
// 实现类 S7Driver / ModbusDriver / McDriver 内部爱用什么用什么

好处:换库成本 = 换一个实现类(授权出问题可一周内换掉);可混用(S7 用 iot、MC 用 HSL);可自研(某个协议自己写不影响别的);可测试(mock driver 就能测调度器)。

07 篇第 2 章会给出这个 SPI 的完整可编译代码。

8.4 什么时候 FastBee 该上 Netty

见 07 篇 §10 性能预估表。一句话结论:

单采集器 ≤ 200 台设备、采集周期 ≥ 500ms  → BIO 一设备一线程,够用,别折腾
超过这个规模                             → 内核换 Netty,驱动接口不动

因为接口层已经隔离了,换内核不影响上层——这正是 SPI 设计的价值兑现点。


第 9 章 迁移路径与风险清单

9.1 迁移真实成本与风险总表

成本分布(从便宜到昂贵):

成本项 大小 说明
调用点改写 小(1~2 天) 都是 readInt16("D100") 这种一行调用
地址字符串差异 中 Modbus 尤其明显:HSL 用 "40001" 字符串,iot 用数字 0
字节序默认值 ⚠️ 中 HSL Modbus 默认 ReverseWordTransform,MC 默认 RegularByteTransform;iot 需显式指定
连接策略差异 中 HSL 默认短连接,iot 长连接;迁过去后行为会变
错误码体系 中 三层错误码 ↔ 异常类型,日志/告警全要改
返回值范式转换 ⚠️ 中~大 OperateResult ↔ 异常是两种心智模型,每个调用点都要重写错误处理
协议细节差异 🔴 大 S7 的 TSAP 默认值不同(03 篇 S7 §5.2);MC 的 ZR 进制不同。这些是"能连上但数据错"的隐蔽 bug 源
现场重测 🔴 最大 每次换库,所有机型都要重新联调一遍

风险总表:

风险 等级 缓解措施
HSL 商用授权风险 🔴 高 先做 SPI 隔离;商用前书面确认授权
iot 的 star/维护资源少于 HSL 🟡 中 作者仍在活跃更新(v1.5.6,2026-03);关键协议自己要有兜底实现能力
iot 无串口支持 🟡 中 串口场景用 HSL,或自己写(RTU 很简单,见 03 篇 Modbus §8)
iot 协议覆盖窄 🟡 中 明确边界:只做三协议;超出边界就说明不支持
两库 TSAP/进制差异导致"数据错但能连" 🔴 高 迁移后必须逐台实测,不能只看"能连通"
HSL 默认短连接导致 TIME_WAIT 堆积 🟡 中 显式 SetPersistentConnection()
iot 无连接池 🟡 中 自研层做连接复用(07 篇 §3)

9.3 授权合规检查清单(给法务/技术负责人)

□ 1. 确认最终产品是否闭源交付?如果是 → HSL 需要书面授权
□ 2. 确认是否修改了库的源码?如果是 → LGPL 要求开放修改部分
□ 3. 确认是否只有 Java 版,还是 C# 主库也要用?→ 分别确认
□ 4. 发邮件给作者确认口径,留存回复
□ 5. 在项目 LICENSE/NOTICE 中声明依赖及其许可
□ 6. iot(MIT):保留版权声明即可,记得加
□ 7. 建立依赖清单(SBOM),定期审计

第 10 章 小结

10.1 三句话结论

  1. 授权是第一决策因素:iot-communication 是 MIT(注意:不是常见的 Apache-2.0 说法,实际是 MIT,已在 _source/iot-communication/LICENSE 与 pom.xml 双重核实);HslCommunication 源码树里躺着 LGPL-3.0(2017-2019),但作者 2019 年 8 月起停止开源、现行口径为付费商用授权。闭源商业产品选 iot。

  2. 架构上 HSL 更精巧,工程上 iot 更好用:HSL 的 INetMessage(33 个帧解析器)是可复用的定帧抽象,这是它唯一明显领先的地方;但 iot 的内置报文回调、注解序列化、131 个测试、标准 Maven 工程,在日常开发里收益更直接。

  3. 对 FastBee:用 iot 做实现,用自研 SPI 做隔离。不要让任何第三方类型泄漏到业务层。这样授权风险、库 bug、性能瓶颈三者都变成了"换一个实现类"的问题。

10.2 一张速查卡

你问 答案
选哪个? 闭源商用 → iot;多品牌/串口 → HSL
iot 零依赖吗? 接近,但有 1 个 compile 依赖 slf4j-api
HSL 能白嫖商用吗? 大概率不能,内置 8 小时限时校验
两者性能差多少? 同一量级,都是 BIO。差别在批量与连接策略
关键坑? HSL 默认短连接;两库 S7 TSAP 默认值不同
最重要的一条建议 先定 SPI 接口,再选库

10.3 接下来读什么

你想做什么 去哪
动手写采集器 → 06 篇 02-自研采集器设计实战
查端口 / 功能码 / 错误码 → 07 篇 01-速查手册
现场连不上 / 数据不对 → 07 篇 02-常见问题排查FAQ
搞懂某条协议的字节 → 03 篇对应协议详解

本篇数据来源:_source/HslJavaLatest/(294 个 .java)、_source/iot-communication/(v1.5.6,332 主 + 131 测试)。Star/Fork 为检索时快照,以仓库实时数据为准。所有协议级数字(125 / 2000 / 123 / PDU-28 / 19 item / 960 字 / 7168 位)已在 03 协议篇三篇文档中逐条核对。


本文是《工业通信与数据采集》栏目第 19/22 篇 · 上一篇:基础设施与工程特性 | 栏目总览 | 下一篇:自研采集器设计实战