01 · HslCommunication 3.9.3 项目总览与架构
本篇定位:给整套源码画一张地图。读完你应该能回答三个问题——这个库由哪些包组成、一次
Read("D100", (short)10)调用经过了哪几层、以及它和老版本相比到底重构了什么。所有行号均对应io.github.dathlin:HslCommunication:3.9.3的官方源码包。
目录
- 1. 版本与构建事实
- 2. 包结构全景
- 3. 模块划分:五层职责
- 4. 设计哲学:三条贯穿全局的主线
- 5. 核心继承链(ASCII 类图)
- 6. 一次读操作的调用时序
- 7. 版本演进:v7.0.1 → v3.9.3
- 8. 授权模块 Authorization
- 设计优点 / 设计缺点 / 可借鉴点
1. 版本与构建事实
先把客观事实钉死,避免后续讨论建立在猜测上。
| 项目 | 实际情况 | 依据 |
|---|---|---|
| Maven 坐标 | io.github.dathlin:HslCommunication:3.9.3 |
用户指定 / 源码来源包 HslJavaLatest |
| 源码文件数 | 294 个 .java |
find . -name "*.java" | wc -l |
| 源码总行数 | 62169 行 | find . -name "*.java" -print0 | xargs -0 cat | wc -l |
| 构建文件 | 没有 pom.xml,没有 build.gradle,没有 build.xml,没有任何 .iml |
在整个 HslJavaLatest/ 树下 find 构建文件返回空 |
| 非 Java 资源 | 0 个(find . -type f ! -name "*.java" 返回空) |
没有 .properties、没有 service loader 注册、没有图片资源 |
| JDK 语法基线 | 大致 Java 6~7 水平 | 大量使用 Started<T> 手写 getter、switch-case 无箭头语法、无 lambda(除个别处匿名内部类) |
| 硬依赖 | 无第三方 jar | 全树只见 java.net / java.io / java.util / java.security / java.nio.charset |
1.1 关于"没有构建文件"的说明
这份源码包是 sources-only 分发:目录下只有 HslCommunication/、META-INF/MANIFEST.MF、空的 main/java、main/resources、test/java。也就是说:
HslJavaLatest/
├── HslCommunication/ ← 全部 294 个 .java,包名根 HslCommunication
├── META-INF/MANIFEST.MF
├── main/java/ ← 空目录
├── main/resources/ ← 空目录
└── test/java/ ← 空目录
这对使用者的直接影响是:想本地编译就得自己写一个 pom。实测可用的最小编译单元就是 JDK 自带的 javac,因为它零第三方依赖——这在 2020 年代做 Maven 发布的库里相当罕见,也直接决定了它能在各种边缘的国产 PLC/工控机上跑起来(无需拖一堆 jar)。这一点后面会反复当作"优点"被引用。
但代价也很明显:没有单元测试(test/java 是空目录)。一个 6 万行、涉及二进制协议解析的库,零自动化测试。
判断是否真的存在测试用例的结论:未在本版本源码中见到任何
test或*Test.java。
2. 包结构全景
2.1 ASCII 目录树
HslCommunication/ (包名根, 3 个根级 .java, 2247 行)
├── Authorization.java 授权/商业许可校验 (1285 行, 全库第 4 大)
├── StringResources.java 多语言资源入口 (349 行)
├── Utilities.java 字节/字符串/时间静态工具 (913 行)
│
├── BasicFramework/ [8 文件 / 2621 行] 与通信无关的基础件
│ ├── SoftBasic.java ★ 字节数组瑞士军刀 (1176 行)
│ ├── SoftBuffer.java 定长环形缓冲
│ ├── SoftIncrementCount.java 循环自增消息号
│ ├── SoftZipped.java SoftSecurity.java SoftAuthorize.java SharpList.java SystemVersion.java
│
├── Core/ [112 文件 / 14566 行] ★ 内核
│ ├── Thread/SimpleHybirdLock.java ReentrantLock 薄封装
│ ├── Types/ [29 文件] OperateResult 系列 + 函数式替身
│ ├── Transfer/ [7 文件] IByteTransform / DataFormat 四象Transform
│ ├── Address/ [17 文件] ModbusAddress / S7AddressData / McAddressData...
│ ├── IMessage/ [34 文件] INetMessage + 33 个协议帧解析器
│ ├── Pipe/ [2 文件] PipeBase / PipeSocket
│ ├── Net/
│ │ ├── NetworkBase/ [9 文件] ★ 四级通信基类
│ │ ├── StateOne/ [2 文件] AppSession / AlienSession
│ │ ├── IReadWriteNet.java IReadWriteDevice.java HslProtocol.java NetSupport.java ...
│ └── Security/ [5 文件] AES / RSA / 证书 / MD5
│
├── LogNet/ [6 文件 / 748 行] 日志抽象
├── Language/ [2 文件 / 1137 行] DefaultLanguage(中文) / English
│
├── ModBus/ [10 文件 / 3356 行] Modbus TCP/RTU/UDP + 服务器
├── Profinet/ [106 文件 / 28938 行] ★ 24 个 PLC 品牌协议
│ ├── Siemens/ Melsec/ Omron/ AllenBradley/ Beckhoff/ Keyence/
│ ├── Panasonic/ Inovance/ XINJE/ Fuji/ Delta/ FATEK/ GE/
│ ├── YASKAWA/ Yokogawa/ Toyota/ MegMeet/ OpenProtocol/
├── CNC/ [12 文件 / 2012 行] FANUC 数控机床
├── Robot/ [6 文件 / 1371 行] FANUC 机器人
├── Instrument/ [8 文件 / 1944 行] DLT645 电表 / RKC 温控
├── Serial/ [3 文件 / 230 行] SoftCRC16 / SoftLRC / CheckType
├── Enthernet/ [6 文件 / 1149 行] HSL 自有应用层协议 + Redis 客户端
├── MQTT/ [11 文件 / 1813 行] MQTT 客户端/同步客户端
└── WebSocket/ [1 文件 / 37 行] WebSocketMessage
2.2 顶层包职责表
| 包 | 文件数 | 行数 | 职责 | 是否核心 |
|---|---|---|---|---|
Profinet |
106 | 28938 | 24 个 PLC/设备品牌的协议实现(含 20 个 Helper 子包) | 量大,但非内核 |
Core |
112 | 14566 | 通信内核、结果类、帧解析、字节转换、地址解析 | 是 |
ModBus |
10 | 3356 | Modbus 协议栈(TCP/RTU/UDP/ASCII + Server) | 是(最大单协议) |
BasicFramework |
8 | 2621 | SoftBasic 等与协议无关的工具 |
是 |
CNC |
12 | 2012 | FANUC 0i 系列机床数据采集 | 否 |
MQTT |
11 | 1813 | MQTT 3.1.1 客户端 + 同步 RPC 客户端 | 否 |
Instrument |
8 | 1944 | 电能表 DLT645、RKC 温控表 | 否 |
Robot |
6 | 1371 | FANUC 机器人关节/报警读取 | 否 |
Enthernet |
6 | 1149 | HSL 私有的 NetSimplify / NetComplex / NetPush / Redis | 否 |
Language |
2 | 1137 | 中英文两套异常文案 | 否 |
LogNet |
6 | 748 | 日志门面 + 文件日志实现 | 否 |
Serial |
3 | 230 | CRC16 / LRC 校验 | 否 |
WebSocket |
1 | 37 | WebSocket 帧对象 | 否 |
值得注意的两个"比例失衡":
Core文件数与Profinet相当,但行数只有一半。因为Core里 34 个IMessage实现类大多是 20~50 行的短类,而Profinet里AllenBradleyHelper1433 行、SiemensS7Net1397 行,都是"大泥球"。Authorization.java一个文件 1285 行,是全库第 4 大文件,超过任何一个通信基类(除NetworkBase1115 行)。换句话说,这是一个把"商业授权"写进核心路径的库。详见第 8 节。
3. 模块划分:五层职责
从依赖方向看(箭头 = 依赖):
┌──────────────────────────────────────────────────────────────┐
│ L5 应用层 / 业务桥接 │
│ ModBus/ Profinet/* CNC/ Robot/ Instrument/ MQTT/ │
│ Enthernet/ WebSocket/ │
│ → 每种设备一个 <Xxx>Net,实现自己的 Read/Write + 报文构造 │
└────────────────────────────┬─────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ L4 地址解析层 Core/Address/ (17 类) │
│ ModbusAddress S7AddressData McAddressData OmronFins… │
│ → 把字符串 "DB20.100" / "D100" 翻译成结构体 │
└────────────────────────────┬─────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ L3 数据变换层 Core/Transfer/ (7 类) │
│ IByteTransform ← ByteTransformBase ← Regular/Reverse* │
│ → DataFormat.ABCD/BADC/CDAB/DCBA 四字节序 │
└────────────────────────────┬─────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ L2 帧解析 + 会话层 Core/IMessage/ + Core/Pipe/ │
│ INetMessage (33 实现) PipeSocket/PipeBase (连接参数+锁) │
└────────────────────────────┬─────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ L1 传输层 Core/Net/NetworkBase/ (9 类) │
│ Socket 收发、超时、长/短连接、两阶段读 │
└────────────────────────────┬─────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ L0 通用工具 BasicFramework/SoftBasic │
│ Utilities.java Core/Types/OperateResult* LogNet/ │
└──────────────────────────────────────────────────────────────┘
这五层是单向依赖,没有反向或循环依赖——这是整个库最干净的结构性优点。Core 不依赖 ModBus/Profinet,中间只有一处例外:Core/IMessage/ModbusRtuMessage.java:27 反向调用了 HslCommunication.ModBus.ModbusInfo.CheckRtuReceiveDataComplete。这是一处明确的分层泄漏,在下一篇会更详细展开。
4. 设计哲学:三条贯穿全局的主线
4.1 主线一:返回 OperateResult 而非抛异常
这是 HslCommunication 最鲜明的 signature。所有可能失败的通信 API 都不抛受检异常,也不抛运行时异常,而是把失败编码进返回值。
Core/Types/OperateResult.java:8-52:
public class OperateResult {
public boolean IsSuccess = false; // 行 40
public String Message = StringResources.Language.UnknownError(); // 行 46
public int ErrorCode = 10000; // 行 52
}
配套的是一整家族 OperateResultExOne<T> / ExTwo<T1,T2> / ExThree / ExFour——因为 Java 没有 C# 的 OperateResult<T> 泛型偏特化,也没有 tuple,作者干脆手写了 5 个类 × 每类约 200 行。
由此产生的代码风格,在 Core/Net/NetworkBase/NetworkBase.java:128-153 的接收方法中非常典型:
protected OperateResultExOne<byte[]> Receive(Socket socket, int length, int timeout, ActionOperateExTwo<Long, Long> reportProgress ) {
if (length == 0) return OperateResultExOne.CreateSuccessResult(new byte[0]); // 行 129
if (!Authorization.nzugaydgwadawdibbas( )) return new OperateResultExOne<>( StringResources.Language.AuthorizationFailed() ); // 行 131
try {
// ...
} catch (RemoteCloseException ex) {
CloseSocket(socket);
return new OperateResultExOne<byte[]>(StringResources.Language.RemoteClosedConnection()); // 行 149
} catch (IOException ex) {
CloseSocket(socket);
return new OperateResultExOne<byte[]>(ex.getMessage()); // 行 152
}
}
三个观察:
- 异常在边界处被吃掉,转成 error code + message。
- 每进入一个方法体就先查一次
Authorization(见 131 行),这是它独特的商业模式,第 8 节详述。 - 调用方必须自己
if (!result.IsSuccess) return ...,因此源码里这种三行式的"错误穿透"写得满键盘都是。
这个设计的工程价值是真实的:工业现场最忌讳一帧数据错位导致整个采集线程崩掉。返回码强迫调用者处理失败。但它的代价是 Java 里产生大量样板——作者为此引入了 FunctionOperate*/ActionOperate* 共 10 个函数式接口来做链式调用(见 OperateResult.java:153-204):
public OperateResult Then( FunctionOperate<OperateResult> func ) { // 行 153
return IsSuccess ? func.Action() : this;
}
public <T> OperateResultExOne<T> ThenExOne( FunctionOperate<OperateResultExOne<T>> func ) { // 行 164
return IsSuccess ? func.Action() : OperateResultExOne.CreateFailedResult(this);
}
这其实是 2016 年前后 Java 版为对齐 C# 版 API 而手搓的 mini-Optional。放到今天应该用 java.util.Optional 或干脆返回 sealed class。
4.2 主线二:INetMessage 抽象解决"怎么算一帧"
TCP 是字节流,没有消息边界。传统解法是 Netty 的 LengthFieldBasedFrameDecoder,而 Hsl 的做法是把"这一帧多长"这个问题抽成接口:
Core/IMessage/INetMessage.java:9-109 定义的三个核心方法:
| 方法 | 行号 | 语义 |
|---|---|---|
int ProtocolHeadBytesLength() |
15 | 第一次读多少字节(返回 -1 表示"我不知道,请在流式柿子里二次判断") |
int GetContentLengthByHeadBytes() |
22 | 拿到头之后,算出还要读多少字节 |
boolean CheckHeadBytesLegal(byte[] token) |
30 | 头合法性校验 |
int CheckMessageMatch(byte[] send, byte[] receive) |
107 | 收发匹配:1=匹配,0=错误,-1=继续收 |
于是 ReceiveByMessage 就能写成协议无关的通用代码(NetworkBase.java:316-330):
else {
OperateResultExOne<byte[]> headResult = Receive(socket, netMessage.ProtocolHeadBytesLength(), timeOut); // 行 318
if (!headResult.IsSuccess) return headResult;
netMessage.setHeadBytes(headResult.Content); // 行 321
int contentLength = netMessage.GetContentLengthByHeadBytes(); // 行 322
if (contentLength <= 0) return headResult;
OperateResultExOne<byte[]> contentResult = Receive(socket, contentLength, timeOut, reportProgress); // 行 325
// ...
}
33 个协议帧解析器(详见第 03 篇)共享同一段读取代码,这是这套库能快速铺到 24 个品牌的关键。
4.3 主线三:IReadWriteNet 统一接口让设备可互换
Core/Net/IReadWriteNet.java:15 定义的是一个 396 行的"大而全"接口,包含 Read/Write/ReadBool/ReadInt16/ReadInt32/ReadFloat/ReadDouble/ReadString/ReadCustomer 以及对应的全部写方法与数组版本。
public interface IReadWriteNet {
OperateResultExOne<byte[]> Read( String address, short length ); // 行 45
OperateResult Write( String address, byte[] value ); // 行 54
OperateResultExOne<boolean[]> ReadBool( String address, short length ); // 行 69
OperateResultExOne<Boolean> ReadBool( String address ); // 行 77
// ...
}
设计价值:上层业务只依赖 IReadWriteNet,换 PLC 只改一行 new ModbusTcpNet(...) → new SiemensS7Net(...)。这在多品牌产线集成里是刚需。
设计代价:接口太大且不可部分实现。Core/Net/NetworkBase/NetworkDeviceBase.java:33-52 的默认实现就直接返回"不支持":
public OperateResultExOne<byte[]> Read(String address, short length) {
return new OperateResultExOne<byte[]>(StringResources.Language.NotSupportedFunction()); // 行 34
}
public OperateResult Write(String address, byte[] value) {
return new OperateResult(StringResources.Language.NotSupportedFunction()); // 行 38
}
也就是说:编译期 device typing 是假的,运行时才报 NotSupported。我在 Profinet/Siemens/SiemensS7Net.java:549-575 就看到 Write(String address, boolean[] values) 的实现是"先读出整字节、改位、再写回"——因为它不支持位写入;而 Modbus 原生支持。这种差异只能靠文档约定而不能靠类型系统。
5. 核心继承链(ASCII 类图)
┌─────────────────────────────────────────────┐
│ NetworkBase (abstract) │ NetworkBase.java:30
│ ── LogNet, Token, CoreSocket, fileCacheSize │
│ ── Receive × 3 重载 / Send × 2 重载 │
│ ── ReceiveByMessage × 3 重载 │
│ ── CreateSocketAndConnect × 4 重载 │
│ ── ReceiveAndCheckBytes / Hsl / Redis / MQTT │
│ / WebSocket 专用接收 │
└────────────────┬────────────────────────────┘
│
┌────────────────────────┼────────────────────────┬─────────────────────┐
│ │ │ │
┌───┴──────────────────┐ ┌───┴────────────────┐ ┌────┴─────────────┐ ┌─────┴──────────┐
│ NetworkDoubleBase │ │ NetworkXBase │ │ NetworkServerBase│ │ NetworkUdpBase │
│ (double = 长短连接) │ │ (X = 异步/文件) │ │ (服务端基类) │ │ (UDP) │
│ ── PipeSocket │ │ ── SendBytesAsync │ │ ── NetworkData- │ │ ── NetworkUdp- │
│ ── GetAvailableSocket│ │ ── BeginReceive- │ │ ServerBase │ │ DeviceBase │
│ ── CreateSocketAnd- │ │ Background │ │ │ │ │
│ Initialication │ │ ── 文件流收发 │ │ │ │ │
│ ── ReadFromCoreServer│ │ ── ~200 行注释代码 │ │ │ │ │
└───┬──────────────────┘ └────────────────────┘ └──────────────────┘ └────────────────┘
│ ★ 关键:长/短连接在这里实现
│
┌───┴────────────────────────────────────────────┐
│ NetworkDeviceBase implements IReadWriteDevice │
│ ── protected short WordLength = 1 │ (西门子设为 2)
│ ── ReadInt16/32/64, UInt16/32, Float, Double, │
│ String 的批量实现(基于 byteTransform) │ NetworkDeviceBase.java:24
│ ── ReadCustomer/WriteCustomer (IDataTransfer) │
└───┬────────────────────────────────────────────┘
│
├──────────────┬──────────────┬──────────────┬────────────────
│ │ │ │
┌───┴──────┐ ┌─────┴─────┐ ┌──────┴──────┐ ┌─────┴──────────┐
│ Modbus- │ │ Siemens- │ │ MelsecMcNet │ │ OmronFinsNet │ ... 共 39 个具体设备类
│ TcpNet │ │ S7Net │ │ │ │ │
│ WordLen=1│ │ WordLen=2 │ │ WordLen=1 │ │ WordLen=1 │
│ Reverse- │ │ Reverse- │ │ Regular- │ │ ReverseWord- │
│ Word │ │ Bytes │ │ ByteTransform│ │ Transform │
└──────────┘ └───────────┘ └─────────────┘ └────────────────┘
5.1 四级各自"加了什么"(一句话版)
| 层级 | 增加的能力 | 关键字段/方法 |
|---|---|---|
NetworkBase |
裸 Socket 收发,三种接收形态 | Receive / Send / ReceiveByMessage / CreateSocketAndConnect |
NetworkDoubleBase |
长连接 vs 短连接、帧收发闭环、重连、帐号证书、设备主动推送 | pipeSocket、isPersistentConn、GetAvailableSocket、ReadFromCoreServer |
NetworkDeviceBase |
把字节变成类型(ReadInt16 等),WordLength 概念 |
WordLength、getByteTransform() |
| 具体设备类 | 协议报文构造、地址解析、GetNewNetMessage()、PackCommandWithHeader() |
各协议 Helper |
注意 MelsecMcNet 用 RegularByteTransform(大端,符合三菱原生字节序),而 ModbusTcpNet 用 ReverseWordTransform(见 ModBus/ModbusTcpNet.java:28)、SiemensS7Net 用 ReverseBytesTransform(见 Profinet/Siemens/SiemensS7Net.java:63)。字节序差异全部收敛在 Core/Transfer 层,这就是为什么 Modbus/S7 明明字节序完全不同,却共享同一份 NetworkDoubleBase 代码。
5.2 还有一个"隐藏第五级"
Core/Net/NetworkBase/NetworkDataServerBase.java(646 行)继承 NetworkServerBase,用于 ModbusTcpServer / SiemensS7Server / MelsecMcServer / OmronFinsServer——即这套库不只写了客户端,还实现了 4 种 PLC 的服务端模拟器。这对没有真机的场景非常有用(可直接拿 ModbusTcpServer 做联调靶机)。
6. 一次读操作的调用时序
以 modbusTcpNet.ReadInt16("D100", (short)10) 为例:
调用方
│
▼
① ModbusTcpNet / NetworkDeviceBase.ReadInt16(address, length)
NetworkDeviceBase.java:92-99
└─ Read(address, length * WordLength) ← Modbus: WordLength=1
│
▼
② ModbusHelper.Read(IModbus modbus, address, length) ModbusHelper.java:90
├─ modbus.TranslateToModbusAddress(address, ReadRegister)
├─ ModbusInfo.BuildReadModbusCommand(...) ← 可能切成多条命令
└─ 循环多条: modbus.ReadFromCoreServer(cmd[i])
│
▼
③ ModbusTcpNet.ReadFromCoreServer(byte[] send) ModbusTcpNet.java:147
└─ super.ReadFromCoreServer(send, true, true) ← hasResponseData=true, usePackAndUnpack=true
│
▼
④ NetworkDoubleBase.ReadFromCoreServer(byte[],bool,bool)
NetworkDoubleBase.java:662-697
├─ 666: GetPipeSocket().PipeLockEnter() ★ 串行化整次请求
├─ 669: GetAvailableSocket() ★ 取长短连接 socket
├─ 678: ReadFromCoreServer(socket, send, ...)
│ ├─ 582: PackCommandWithHeader() ★ 加 MBAP 头 (7 字节)
│ ├─ 590: Send(socket, sendValue)
│ ├─ 621: ReceiveByMessage(socket, timeout, sendValue, netMessage)
│ │ ├─ 读 8 字节头 → ModbusTcpMessage.ProtocolHeadBytesLength()
│ │ ├─ GetContentLengthByHeadBytes() 算出剩余长度
│ │ └─ 再读 body,拼回完整帧
│ ├─ 633: netMessage.CheckMessageMatch() ★ 校验事务ID/站号
│ └─ 650: UnpackResponseContent() ★ 剥 MBAP + 抽真实数据+错误码
├─ 692: ExtraAfterReadFromCoreServer(read)
├─ 694: PipeLockLeave()
└─ 695: if (!isPersistentConn) CloseSocket(...) ★ 短连接则关掉
│
▼
⑤ ByteTransformHelper.GetResultFromBytes(...) → getByteTransform().TransInt16(...)
NetworkDeviceBase.java:93-98
│
▼
返回 OperateResultExOne<short[]>
有 五个可覆写钩子 分布在这条链上,这是这套库扩展性的支点:
| 钩子 | 位置 | 谁在用 |
|---|---|---|
GetNewNetMessage() |
NetworkDoubleBase.java:87 |
所有设备类(SiemensS7Net.java:48 返回 new S7Message()) |
PackCommandWithHeader() |
NetworkDoubleBase.java:503 |
ModbusTcpNet.java:123、MelsecMcNet.java:108 |
UnpackResponseContent() |
NetworkDoubleBase.java:513 |
ModbusTcpNet.java:127、MelsecMcNet.java:112 |
InitializationOnConnect() |
NetworkDoubleBase.java:381 |
SiemensS7Net.java:227 用它做 ISO-on-TCP 两次握手 |
ExtraOnDisconnect() |
NetworkDoubleBase.java:416 |
少数协议 |
7. 版本演进:v7.0.1 → v3.9.3
说明:对比用的旧版源码位于
_source/HslJava/HslCommunication_Java/HslCommunication/src,只有 90 个.java文件(新版 294 个),下文所有"旧版"结论均基于对该目录的实读。版本号本身未在任一源码文件中出现,"v7.0.1"这一说法取自用户提供的目录元信息,标注为"(未在源码中见到)"。
7.1 规模对比
| 维度 | 旧版 | 新版 3.9.3 | 倍数 |
|---|---|---|---|
.java 文件数 |
90 | 294 | 3.3× |
Core/IMessage 实现数 |
7 | 33 | 4.7× |
Core/Address 类数 |
1 (DeviceAddressBase) |
17 | 17× |
Profinet 品牌数 |
5 (Keyence/Melsec/Omron/Siemens + *) | 24 | ~5× |
NetworkDoubleBase 行数 |
621 | 734 | 1.2× |
Core/Types 类数 |
13 | 29 | 2.2× |
7.2 三个结构性重构
重构一:从「反射泛型」到「虚方法覆写」
旧版 NetworkDoubleBase 是一个双参数泛型类,靠反射实例化消息对象和字节变换对象:
// 旧版 Core/Net/NetworkBase/NetworkDoubleBase.java:21-37
public class NetworkDoubleBase<TNetMessage extends INetMessage, TTransform extends IByteTransform> extends NetworkBase
{
public NetworkDoubleBase( Class<TNetMessage> netMessageClass, Class<TTransform> tTransformClass )
{
this.netMessageClass = netMessageClass;
this.tTransformClass = tTransformClass;
simpleHybirdLock = new SimpleHybirdLock();
try {
byteTransform = tTransformClass.newInstance();
} catch (Exception ex){ System.out.println(ex.getMessage()); }
// ...
}
private Class<TNetMessage> netMessageClass; // 行 41
private Class<TTransform> tTransformClass; // 行 42
}
新版完全去掉了泛型参数,改成 GetNewNetMessage() 保护方法 + setByteTransform():
// 新版 Core/Net/NetworkBase/NetworkDoubleBase.java:30, 87-89, 96-107
public class NetworkDoubleBase extends NetworkBase { // 行 30 —— 无泛型
protected INetMessage GetNewNetMessage() { // 行 87
return null;
}
public IByteTransform getByteTransform() { return byteTransform; } // 行 96
public void setByteTransform(IByteTransform transform) { // 运行期可热替换
byteTransform = transform;
}
}
评价:这是一个明确的改进。旧的双泛型 + 构造函数传 Class 对象非常 Java 味且笨重(每个子类构造函数都得 super(ModbusTcpMessage.class, ReverseWordTransform.class, ...)),而且 getGenericSuperclass() 反射在多级继承下会失效(旧版文件里还有 Core/Net/NetworkBase/NetworkDoubleBase.java:56-60 的 getInstanceOfTTransform() 反射兜底代码,说明作者遇到过这个问题)。新版干掉了所有反射,也顺便让 setByteTransform 支持运行期动态切换字节序——这对"现场接了一台字节序妖怪的设备"是救命能力。
重构二:连接参数与锁下沉到 PipeSocket / PipeBase
旧版把这些字段直接平铺在 NetworkDoubleBase 里:
// 旧版 NetworkDoubleBase.java:44-50
private String ipAddress = "127.0.0.1";
private int port = 10000;
private int connectTimeOut = 10000;
private int receiveTimeOut = 10000;
private boolean isPersistentConn = false;
private SimpleHybirdLock simpleHybirdLock = null;
private boolean IsSocketError = false;
新版抽出了 Core/Pipe/PipeSocket.java(248 行)+ Core/Pipe/PipeBase.java(44 行):
// 新版 PipeSocket.java:240-247
private String ipAddress = "127.0.0.1";
private int[] _port = null; // ← 变成了数组!
private int indexPort = -1; // ← 多端口切换游标
private Socket socket;
private int receiveTimeOut = 5000;
private int connectTimeOut = 10_000;
private int sleepTime = 0;
private boolean isSocketError = false;
关键是 _port 从 int 变成了 int[],于是有了 SetMultiPorts()(PipeSocket.java:202)和 GetConnectIPEndPoint() 里的自动轮转(PipeSocket.java:214-234):
public SocketAddress GetConnectIPEndPoint( ) {
if (_port.length == 1) return new InetSocketAddress( getIpAddress(), _port[0] ); // 行 216
ChangePorts( ); // 行 217
int port = _port[this.indexPort];
return new InetSocketAddress( getIpAddress(), port );
}
public void ChangePorts( ) { // 行 226
if (_port.length == 1) return;
if (this.indexPort < this._port.length - 1) this.indexPort++;
else this.indexPort = 0;
}
这就是用户所说的"双端口冗余"机制:三菱等 PLC 支持多端口网口,配 [5000, 5001] 后每次连接失败自动换下一个端口。
同时 SimpleHybirdLock 也从"每个 NetworkDoubleBase 一把私有锁"改成了挂在 PipeBase 上、可被多个连接共享:
// 新版 Core/Pipe/PipeBase.java:21-32
public void PipeLockEnter( ){
lockingTick.getAndIncrement();
this.hybirdLock.Enter( );
}
public void PipeLockLeave( ) {
this.hybirdLock.Leave( );
lockingTick.decrementAndGet();
}
然后是 SetPipeSocket(PipeSocket)(NetworkDoubleBase.java:245-252),允许多个设备对象共享同一条 TCP 连接——用于一个网口后面挂多台串口设备的场景。
评价:这是一次高质量的重构。isSocketError 标志位是单点 bool 而非 volatile(见 PipeSocket.java:247),跨线程可见性依赖于 ReentrantLock 的 happens-before 保护,目前逻辑上成立但极为脆弱——一旦有人在不上锁的路径上去读这个字段就会出问题。这一点在 02 篇会展开。
重构三:ModbusAddress 搬家 + GetAvailableSocket 开放
| 项 | 旧版 | 新版 3.9.3 |
|---|---|---|
ModbusAddress 位置 |
ModBus/ModbusAddress.java |
Core/Address/ModbusAddress.java |
GetAvailableSocket 可见性 |
private(旧 NetworkDoubleBase.java:343) |
protected(新 NetworkDoubleBase.java:523) |
Core/Address 包存在性 |
只有 DeviceAddressBase.java |
17 个地址类 |
GetAvailableSocket 从 private 提到 protected,意味着子类可以接管 socket 获取策略(例如做连接池、做双机热备)。这是个实用的开放。
7.3 一个新引入的争议点:useServerActivePush
新版 NetworkDoubleBase 增加了服务器主动推送支持(NetworkDoubleBase.java:42, 62-68, 381-407),会在 InitializationOnConnect 里起一个后台线程死循环接收,通过 AutoResetEvent 与业务线程同步。
// NetworkDoubleBase.java:383-405(节选)
if (this.useServerActivePush){
threadBackReceive = new Thread(new Runnable() {
public void run() {
while (true){
OperateResultExOne<byte[]> receive = ReceiveByMessage(socket, -1, null, GetNewNetMessage());
if (!receive.IsSuccess) { pipeSocket.setIsSocketError(true); return; } // 行 390
if (DecideWhetherQAMessage( socket, receive )) {
bufferQA = receive.Content;
autoResetEvent.set( ); // 行 399
}
}
}
});
threadBackReceive.start();
}
这段代码有几个具体问题,写在这里作为"版本演进的副作用":
bufferQA是普通成员变量(NetworkDoubleBase.java:44),后台线程写、业务线程读,没有任何 volatile 或同步屏障,可见性完全依赖巧合。- 线程泄漏:
while(true)的线程没有daemon标记,也没有在ConnectClose()里被中断(ConnectClose见NetworkDoubleBase.java:352-372,只做了ExtraOnDisconnect+CloseSocket)。 threadBackReceive字段被无条件覆盖,重复连接会丢失旧线程句柄。
这是典型的"为了支持某个具体客户场景而塞进主干"的代码,属于设计债。
8. 授权模块 Authorization
这个文件有必要单列一节,因为它长 1285 行,且在每一个通信入口被调用。
Authorization.java:22-34:
public static boolean nzugaydgwadawdibbas( )
{
moashdawidaisaosdas++;
// 如果需要移除验证,这里返回true即可。
// return true;
if (naihsdadaasdasdiwasdaid == niasdhasdguawdwdad && nuasgdaaydbishdgas > 0) return nuasduagsdwydbasudasd( );
if (((iashdagsdawbdawda( ).getTime() - niahdhiduasdbubfas.getTime()) / 1000d / 60d / 60) < nuasgdawydaishdgas)
{
return nuasduagsdwydbasudasd( );
}
return asdhuasdgawydaduasdgu( );
}
事实陈述,不带感情色彩:
- 类名、方法名全部是混淆后的无意义字符串(
nzugaydgwadawdibbas、asdniasnfaksndiqwhawfskhfaiw),显然是刻意防窥.My mother 花了 guilty 也难以静态追踪。 - 源码注释里明写了"如果需要移除验证,这里返回 true 即可"(第 25 行)。
- 调用点遍布全库:
NetworkBase.java:131、ModbusInfo.java:159, 622、McBinaryHelper.java:509, 517……
// ModBus/ModbusInfo.java:159 —— 决定读取是否允许自动分片切割(也就是一次能读多少个字)
if (mAddress.getFunction() == ReadCoil ||
mAddress.getFunction() == ReadDiscrete ||
mAddress.getFunction() == ReadRegister ||
mAddress.getFunction() == ReadInputRegister ||
Authorization.asdniasnfaksndiqwhawfskhfaiw()) {
OperateResultExTwo<int[], int[]> bytes = HslHelper.SplitReadLength(mAddress.getAddress(), length,
(mAddress.getFunction() == ReadCoil || mAddress.getFunction() == ReadDiscrete) ? (short) 2000 : (short) 120);
也就是说:未授权时某些"高级功能"(如批量读写、标签读写、Modbus 自动分片、扩展日志)会被静默降级或拒绝,且错误信息是 StringResources.Language.InsufficientPrivileges()。这与很多"开源后可商用"的直觉相冲突,选型时需特别注意。
合规提醒:本节仅描述源码事实。使用该库前请自行确认其许可证条款(该 Java 版的许可证文件未包含在本源码包中,这是本版本源码的事实缺失)。
设计优点
- 单向分层、零第三方依赖:
Core不依赖上层协议包;整个库只吃 JDK,可以在任何 Java 6+ 的工控环境直接javac编译落地,这一点在这个领域几乎无敌。 INetMessage抽象真的赢:33 个协议帧解析共享同一份ReceiveByMessage读取循环,新增一种私有协议的边际成本极低(写一个 30 行的 Message 类即可)。- **
OperateResult消除了"忘记 try-catch 导致采集线程死亡"**这一现场最常见的故障模式。虽然样板多,但对目标人群(电气工程师转 Java)反而是降低门槛。 IReadWriteNet统一了 24 个品牌的读写 API,换设备只需改一行构造。这一点在国产 Hsl 生态里被大量二次集成,是有真实价值的抽象。- 服务端模拟器齐全:
ModbusTcpServer/SiemensS7Server/MelsecMcServer/OmronFinsServer让无真机调试成为可能。 - 3.9.3 的三次重构都是向好:去掉反射泛型、抽出
PipeSocket支持多端口冗余与连接共享、开放GetAvailableSocket。
设计缺点
- 零构建文件、零单元测试:62169 行涉及二进制位操作的库没有任何自动化测试,
test/java是空目录。任何协议改动只能靠现场 QA。 IReadWriteNet接口过大且不可部分实现:默认实现直接返回NotSupportedFunction()(NetworkDeviceBase.java:34, 38),编译期发现不了能力缺失。- 反射 Perthpero 版改成虚方法后,
byteTransform可以为 null:NetworkDoubleBase构造函数不初始化byteTransform(NetworkDoubleBase.java:34-37),忘了设就会 NPE;旧版至少有newInstance()兜底。 Authorization侵入到Receive主路径(NetworkBase.java:131):每次收一帧都要跑一遍授权逻辑,且在商业版限制下会静默降级功能。Core/IMessage/ModbusRtuMessage.java:27反向依赖ModBus包,破坏了 Core 层的封闭性。useServerActivePush后台线程存在可见性与线程泄漏问题(NetworkDoubleBase.java:383-405)。NetworkXBase.java有约 200 行被注释掉的死代码(行 155-357,还残留 C# 的/// < summary >和Newtonsoft.Json.Linq.JObject引用),说明 Java 版是从 C# 版逐行翻译过来的,翻译未完成的部分直接注释留在了主干里。
可借鉴点
- 把"流怎么分帧"抽象成接口而不是配置。Netty 的
LengthFieldBasedFrameDecoder用 4 个 int 参数描述长度域偏移;Hsl 用一段可执行代码。对于国产自定义协议(长度域类型千奇百怪:BCD 码的、ASCII 十六进制的、要靠"已发命令反推"的),代码比参数更灵活——MelsecA1EBinaryMessage那种"根据发送报文推断回复长度"的实现用参数根本表达不出来。 setByteTransform做成可热替换:把字节序这个变量从类型系统里拿出来,放到对象属性里,是应对"同一型号 PLC 不同固件字节序不同"的实用做法。- 多端口数组 + 轮转游标(
PipeSocket.SetMultiPorts)用 20 行代码实现了双网口冗余,性价比极高。 Concurrent之外的选择:SimpleHybirdLock只薄封装了ReentrantLock,还加了一个AtomicInteger lockingTick计数器用于观测当前排队深度(PipeBase.java:37-39)。这种"顺手埋一个可观测钩子"的做法值得学。- 服务端模拟器先行:写协议客户端的同时把服务端写了,既是测试手段也是产品能力,一举两得。
下一篇:通信内核 NetworkBase:Socket 层、两阶段读与四级继承 — 逐行拆解四个通信基类、两阶段读、锁的语义与错误码体系。
本文是《工业通信与数据采集》栏目第 10/22 篇 · 上一篇:三菱 MC 协议(MELSEC Communication Protocol / SLMP)详解 | 栏目总览 | 下一篇:通信内核 NetworkBase:Socket 层、两阶段读与四级继承