01 · HslCommunication 3.9.3 项目总览与架构

本篇定位:给整套源码画一张地图。读完你应该能回答三个问题——这个库由哪些包组成、一次 Read("D100", (short)10) 调用经过了哪几层、以及它和老版本相比到底重构了什么。所有行号均对应 io.github.dathlin:HslCommunication:3.9.3 的官方源码包。

目录


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 帧对象 否

值得注意的两个"比例失衡":

  1. Core 文件数与 Profinet 相当,但行数只有一半。因为 Core 里 34 个 IMessage 实现类大多是 20~50 行的短类,而 Profinet 里 AllenBradleyHelper 1433 行、SiemensS7Net 1397 行,都是"大泥球"。
  2. Authorization.java 一个文件 1285 行,是全库第 4 大文件,超过任何一个通信基类(除 NetworkBase 1115 行)。换句话说,这是一个把"商业授权"写进核心路径的库。详见第 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
    }
}

三个观察:

这个设计的工程价值是真实的:工业现场最忌讳一帧数据错位导致整个采集线程崩掉。返回码强迫调用者处理失败。但它的代价是 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();
}

这段代码有几个具体问题,写在这里作为"版本演进的副作用":

  1. bufferQA 是普通成员变量(NetworkDoubleBase.java:44),后台线程写、业务线程读,没有任何 volatile 或同步屏障,可见性完全依赖巧合。
  2. 线程泄漏:while(true) 的线程没有 daemon 标记,也没有在 ConnectClose() 里被中断(ConnectClose 见 NetworkDoubleBase.java:352-372,只做了 ExtraOnDisconnect + CloseSocket)。
  3. 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( );
}

事实陈述,不带感情色彩:

// 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 版的许可证文件未包含在本源码包中,这是本版本源码的事实缺失)。


设计优点

设计缺点

可借鉴点


下一篇:通信内核 NetworkBase:Socket 层、两阶段读与四级继承 — 逐行拆解四个通信基类、两阶段读、锁的语义与错误码体系。


本文是《工业通信与数据采集》栏目第 10/22 篇 · 上一篇:三菱 MC 协议(MELSEC Communication Protocol / SLMP)详解 | 栏目总览 | 下一篇:通信内核 NetworkBase:Socket 层、两阶段读与四级继承