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) |
结论:
- 纯 Modbus / S7 / MC 三协议场景:两者覆盖几乎等价,iot 甚至在西门子数控机床 828D、程序上下传、PLC 启停上更强。
- 多品牌大杂烩现场:HSL 是唯一选择,它的
Profinet/下有 19 个品牌目录。 - 视频流:iot 独有 RTSP/H264/MP4。
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 的从站会直接读错或超时。
- 遇到野鸡设备(length 字段写错、多回一个字节),HSL 更容易"莫名其妙地就能连上"。
- 但代价是:这段代码会把真实错误悄悄吞掉,让你以为设备是好的,直到某个边界条件才爆。
- iot 的做法更干净:报文不对就报错,逼你去修配置或换设备。
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); // 一行搞定
我的判断:
- iot 的注解方案生产力更高,改点表不用改解析代码。
- 但反射有代价:每次
read()都走一遍Class.getDeclaredFields()+ 注解解析。高频采集(100ms 周期)下必须缓存解析结果,否则 GC 压力明显。05 篇 §4.6 对此有评价。 - HSL 的手写方案在热路径上更快、更可控,但点表一改就要动代码。
- 推荐做法:用 iot 的注解做点表定义,但自己写一层解析结果缓存(元数据只解析一次,之后按偏移直接读)。第 07 篇第 2 章的自研采集器就是这么设计的。
第 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 三句话结论
-
授权是第一决策因素:iot-communication 是 MIT(注意:不是常见的 Apache-2.0 说法,实际是 MIT,已在
_source/iot-communication/LICENSE与pom.xml双重核实);HslCommunication 源码树里躺着 LGPL-3.0(2017-2019),但作者 2019 年 8 月起停止开源、现行口径为付费商用授权。闭源商业产品选 iot。 -
架构上 HSL 更精巧,工程上 iot 更好用:HSL 的
INetMessage(33 个帧解析器)是可复用的定帧抽象,这是它唯一明显领先的地方;但 iot 的内置报文回调、注解序列化、131 个测试、标准 Maven 工程,在日常开发里收益更直接。 -
对 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 篇 · 上一篇:基础设施与工程特性 | 栏目总览 | 下一篇:自研采集器设计实战