Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
- 增加全部 Item 格式、长度边界、嵌套 List、分段输入、非法输入和往返协议向量测试。
- 增加动态 SecsMessage、HSMS Data Message 信封与可选单根 Item 的 Payload 编解码。
- 增加完整线上帧、无 Body、空 List、分段输入、非法 SType/PType 和消息往返测试。
- 增加 StreamFrame 内部会话适配器,提供单调 Session ID、会话失效通知和旧会话发送拒绝。
- 增加基于实际 Socket 写出分片的整帧发送确认,以及可替换计时器驱动的未完成帧超时。
- 增加发送竞态、迟到接收、超时重置和 StreamFrame 真实 TCP 回环测试。
- 升级官方 StreamFrame 2.3.0,采用原生单调 Session ID、会话失效通知、
会话绑定发送和未完成帧超时,删除 #38/#39 对应的临时回调适配。
- 发送任务由 StreamFrame 在整帧写入本机 Socket 后完成,旧会话消息不会
跨重连重放;T8 FrameError 映射为保留 transport Session ID 的诊断。
- T8 配置改为要求可由正整毫秒精确表示且不超过 <code>int.MaxValue</code>
毫秒;亚毫秒、截断和溢出输入在公共选项边界立即拒绝。
- 增加发送竞态、迟到接收、原生 T8 映射和 StreamFrame 真实 TCP 回环测试。
- 增加 Active/Passive 共用的内部 HSMS-SS Select/Separate 状态机,
分离 TCP Connected、Selecting 与 Selected 状态。
- 增加从整帧实际写出后启动的 T6、连接建立后启动的 T7、System Bytes
Expand All @@ -40,8 +44,8 @@
Active/Passive 双端真实 TCP 数据事务测试。
- 增加显式 HSMS T5/T8 运输配置;Active 连接把 T5 无损映射为固定
StreamFrame 重试间隔并关闭指数退避,Passive 监听重试保持独立。
- 增加按接收进展与会话代次隔离的 T8 监视、专用超时异常,以及部分长度
头、部分 Payload、陈旧回调、替换会话和真实 TCP 超时测试。
- 增加 StreamFrame 原生接收进展 T8、专用超时异常及关闭原因关联,覆盖
部分长度头、替换会话和真实 TCP 超时测试。
- 增加公共 <code>HsmsConnection</code> 与不可变
<code>HsmsConnectionOptions</code>,组合动态消息收发、Selected 等待、
单消费者事件流、入站一次性回复和控制命令。
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
<PackageVersion Include="PolySharp" Version="1.16.0" />
<PackageVersion Include="Secs4Net" Version="3.1.0" />
<PackageVersion Include="StreamFrame" Version="2.2.0" />
<PackageVersion Include="StreamFrame" Version="2.3.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="4.0.0" />
</ItemGroup>
Expand Down
21 changes: 10 additions & 11 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,17 @@ HSMS 会话切换后,旧会话的控制请求、数据事务和等待中的应
<code>IHsmsTransport</code> 使用单一事件流按 Session ID 报告会话打开、
帧到达和会话关闭。<code>StreamFrameHsmsTransport</code> 已在内部实现:

- 每次 TCP Connected 生成单调递增、不可复用的 Session ID;
- 收到的帧在 codec 解码时绑定 Session ID,迟到消费不会被标成新会话;
- 发送信封在编码前再次校验 Session ID,旧会话消息不会上线;
- 同一时间只向 StreamFrame 提交一帧,并根据 RawBytesSent 的实际分片
累计确认,整帧写完后才完成发送;
- 直接采用 StreamFrame 2.3.0 的单调 TCP Session ID;
- 通过会话感知接收流保留帧的原始 Session ID,迟到消费不会被标成新会话;
- 会话绑定发送在 StreamFrame 原生 FIFO 中校验 Session ID,旧会话消息
不会重放,整帧写入本机 Socket 后才完成;
- Active 连接把显式 T5 映射为固定 StreamFrame 重试间隔,关闭指数退避;
- 原始接收字节独立跟踪长度前缀和剩余 Payload,仅有未完成帧时运行 T8,
并用代次标识隔离接收进展与替换会话的陈旧回调。
- 显式 T8 映射为 StreamFrame 原生未完成帧超时,并把对应 FrameError
转换为带 transport Session ID 的 HSMS 关闭原因。

这些能力隔离当前 StreamFrame 2.2.0 尚未提供的高级语义,不进入公共
API。#38 已合入上游但尚未发布,#39 正在实现;正式包可用后只替换
<code>IHsmsTransport</code> 实现。详细失效模式与替换条件见
这些能力仍隔离在 <code>IHsmsTransport</code> 后,不进入公共 API。此前
跟踪的 StreamFrame #38/#39 已在 2.3.0 完成迁移;SecsFrame 只保留
协议关闭原因和异常边界转换。详细失效模式与验证证据见
[STREAMFRAME-ADAPTER.md](STREAMFRAME-ADAPTER.md)。

### 单线程会话状态机
Expand Down Expand Up @@ -136,7 +135,7 @@ HSMS 头 Session ID 与 System Bytes。没有 W-Bit 的出站消息在写出后
组合层。它要求显式网络、协议 Session ID 和 T3/T5/T6/T7/T8 配置,公开
动态消息发送、一次性入站回复、控制命令、Selected 等待和单消费者事件
流。公共层不暴露 transport Session generation、StreamFrame
<code>StreamConnectionOptions</code> 或 #38/#39 的回调适配。
<code>StreamConnectionOptions</code> 或 StreamFrame 的会话感知接口。

连接内部独占消费事务事件,并分别更新状态等待信号与公共事件 channel,
因此 readiness 等待不会与业务消息消费者竞争。详细生命周期和取消语义
Expand Down
14 changes: 7 additions & 7 deletions docs/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@

这些条目只是调查清单,不代表当前实现已经支持。

## 上游能力适配
## 上游能力集成

StreamFrame #38(未完成帧超时)已经合入上游 <code>main</code>,但尚未
进入正式 NuGet 版本;#39(会话感知发送确认与消息上下文)正在实现。
SecsFrame 当前固定使用 StreamFrame 2.2.0,并通过内部
<code>StreamFrameHsmsTransport</code> 补足对应语义。
StreamFrame #38(未完成帧超时)与 #39(会话感知发送确认及消息上下文)
已经随官方 StreamFrame 2.3.0 发布。SecsFrame 固定该 NuGet 版本,并由
内部 <code>StreamFrameHsmsTransport</code> 使用原生会话 API;此前基于
原始字节回调、发送信封和自建 Session ID 的临时实现已经删除。

这不是设备兼容模式:

- 不放宽协议输入,也不接受标准严格模式本应拒绝的数据;
- 不提供默认关闭的厂商开关;
- 不暴露为公共 API,不形成长期兼容承诺;
- 上游能力可用并完成等价测试后,直接替换内部实现。
- 不暴露 StreamFrame 类型为公共 API,不形成跨层兼容承诺;
- SecsFrame 仅保留 HSMS 关闭原因、异常和事件边界转换。

具体机制、测试证据和已知代价见
[STREAMFRAME-ADAPTER.md](STREAMFRAME-ADAPTER.md)。
Expand Down
8 changes: 4 additions & 4 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@

- [x] 完整 SECS-II Item 类型、嵌套 List 和严格二进制编解码。
- [x] 动态 SecsMessage、可选单根 Item 与 HSMS Data Message Payload 编解码。
- [x] StreamFrame #38/#39 的内部会话绑定、写出确认和未完成帧超时适配。
- [x] 迁移到 StreamFrame 2.3.0 原生会话绑定、写出确认和未完成帧超时。
- [x] Active/Passive 共用状态机的 Select/Select Response、Separate、
T6/T7、System Bytes 关联和会话失效处理。
- [x] Linktest、Reject、Deselect 控制消息与对应 T6 分支。
- [x] Selected 数据发送、Primary/Secondary 关联、T3 与入站同事务回复。
- [x] 显式 T5 固定重连节流、T8 接收进展监视和跨会话旧回调隔离。
- [x] 显式 T5 固定重连节流、原生 T8 映射和跨会话故障隔离。
- [x] 最小公共连接外观、显式 T3/T5/T6/T7/T8 配置、动态消息事件与
确定的取消/释放语义。
- [x] 使用 nuget.org 官方 Secs4Net 3.1.0 的 Active/Passive、Select、
Expand All @@ -25,8 +25,8 @@
- [ ] 使用授权 E37/E37.1 核对 T5/T8 默认值与精确启停边界。
- [x] Host、Equipment 双角色端点及两种 Active/Passive 拓扑的双向回环。

阶段 1 的工程能力已经形成完整基础链路。下一切片进入 GEM 通用行为,同时
继续跟踪 StreamFrame #38/#39 的正式包发布并准备等价迁移。任何标准默认值
阶段 1 的工程能力已经形成完整基础链路,StreamFrame #38/#39 的正式包
迁移与等价测试已经完成。后续切片继续扩展 GEM 通用行为。任何标准默认值
和完整合规结论都依赖团队合法获得的 E37/E37.1 及相关 GEM 标准副本与
一致性测试。

Expand Down
137 changes: 58 additions & 79 deletions docs/STREAMFRAME-ADAPTER.md
Original file line number Diff line number Diff line change
@@ -1,91 +1,70 @@
# StreamFrame 内部适配
# StreamFrame 会话适配

## 背景
## 上游基线

SecsFrame 当前依赖 StreamFrame 2.2.0。以下上游能力仍在跟踪:
SecsFrame 固定依赖官方 [StreamFrame 2.3.0](https://www.nuget.org/packages/StreamFrame/2.3.0)。
该版本已经发布并解决此前跟踪的两项能力缺口:

- [StreamFrame #38](https://github.com/CSJ608/StreamFrame/issues/38):
未完成帧超时已经合入上游 <code>main</code>,但尚未进入正式包;
- [StreamFrame #39](https://github.com/CSJ608/StreamFrame/issues/39):
会话感知的发送确认与消息上下文正在实现。
- [#38](https://github.com/CSJ608/StreamFrame/issues/38) / [PR #41](https://github.com/CSJ608/StreamFrame/pull/41):
未完成帧超时与 <code>IncompleteFrameTimeout</code> 诊断;
- [#39](https://github.com/CSJ608/StreamFrame/issues/39) / [PR #42](https://github.com/CSJ608/StreamFrame/pull/42):
会话编号、会话绑定发送、写出确认和带会话归属的接收消息。

StreamFrame 现有 <code>SendAsync</code> 在消息进入连接级队列后完成,队列
会跨 TCP 重连保留;<code>GetMessages</code> 也是跨重连的稳定消息流。
这些语义适合通用业务连接,但不足以支撑 HSMS 的严格会话边界。因此
SecsFrame 在内部 <code>IHsmsTransport</code> 后提供临时适配,公共 API
不依赖 StreamFrame 的队列或原始字节回调。
SecsFrame 不再维护基于原始字节回调的 T8 监视器、发送确认计数器、会话
信封 codec 或自建 TCP Session ID。公共 API 和内部
<code>IHsmsTransport</code> 契约保持不变,上层 HSMS 状态机不直接依赖
StreamFrame 类型。

## 会话事件
## 会话边界

内部传输输出同一条有序事件流:
<code>StreamFrameHsmsTransport</code> 只消费
<code>ISessionAwareStreamConnection&lt;HsmsFrame&gt;</code>:

- <code>SessionOpened</code>:TCP Connected 后分配单调递增 Session ID;
- <code>FrameReceived</code>:携带 codec 解码时绑定的 Session ID;
- <code>SessionClosed</code>:会话失效,超时时附带原因。
- <code>CurrentSessionId</code> 作为 TCP 会话的单调编号;
- <code>GetSessionMessages</code> 保留帧的原始会话归属,迟到消费不会被
标记为新会话;
- <code>SendInSessionAsync</code> 把帧绑定到指定会话,旧会话帧不会在
重连后重放;
- 发送任务仅在整帧写入本机 Socket 后完成,T3/T6 因而仍从明确的写出
完成点启动。

接收帧不是在业务消费时读取“当前会话”,因此旧解码任务的迟到结果仍
保留旧 Session ID。状态机可以按 ID 丢弃已关闭会话的事件。
状态机还可以通过 <code>TryCloseSession</code> 只关闭匹配的当前 Session
ID,并把协议错误或 T6/T7 超时原因带入 <code>SessionClosed</code>。
<code>GetMessages</code> 与 <code>GetSessionMessages</code> 是同一接收通道
的竞争消费视图,适配器只使用后者。发送中途失败时,远端是否收到部分字节
仍是未知状态;上层必须依靠事务关联、超时和业务幂等恢复,不能假设远端
一定没有收到。

## 发送确认与不重放
StreamFrame 的 <code>SessionExpiredException</code> 在内部边界转换为
<code>HsmsTransportSessionExpiredException</code>,并保留原异常。协议
主动关闭或 T8 关闭时,适配器按 TCP Session ID 保留首个关闭原因,使
<code>SessionClosed</code> 与尚未完成的发送观察到同一个协议异常。

适配器的发送路径一次只允许一个待确认帧:

1. 入队前确认调用方 Session ID 仍是当前会话;
2. 信封进入 StreamFrame codec 时再次确认,关闭会话的信封编码失败;
3. 根据 <code>RawBytesSent</code> 每次实际 Socket 写出的字节数累计;
4. 四字节长度前缀、十字节头和 Body 全部写完后完成发送任务;
5. 会话先关闭时,以明确的会话失效异常完成等待任务。

因此会话层 T6 与事务层 T3 都从发送任务成功完成后启动。调用取消若发生
在进入 StreamFrame 队列之前会取消发送;一旦成功入队,适配器等待明确
的写完或会话失效结果,避免向调用方返回含糊状态。

旧信封即使因极窄竞态留在 StreamFrame 的跨重连队列中,也会在新会话
编码前失败,绝不会写入 Socket。当前 fail-closed 代价是该编码失败会让
StreamFrame 再重建一次连接;上游 #39 提供原生会话队列后可消除此代价。

## T5 连接重试
## T5 与 T8

内部 <code>HsmsTransportOptions</code> 要求调用方显式给出 T5 与 T8,
不内置未经授权标准核对的默认值。Active 连接通过独立选项副本把 T5
无损转换为 StreamFrame 的整毫秒连接重试间隔,并把最大退避设置为零,
使连续失败保持固定间隔。原 <code>StreamConnectionOptions</code> 不会
被修改,其余队列、缓冲、KeepAlive 和解码选项逐项保留。

Passive 端的监听失败重试不是当前适配器定义的 T5 行为,因此不会覆盖
<code>AcceptRetryDelayMs</code> 或最大退避设置。T5 必须为正值、可由
整毫秒精确表示,并且不超过 StreamFrame 2.2.0 的 <code>int</code>
毫秒范围;适配器拒绝截断、舍入和溢出。

## T8 接收超时

适配器直接观察 StreamFrame 的原始接收分片,并独立跟踪 HSMS 四字节
大端长度前缀与剩余 Payload:

- 从未收到字节或完整帧后的空闲期不计时;
- 收到部分长度头或部分 Payload 后启动计时;
- 每次收到后续字节重新计时;
- 当前帧完整且没有下一帧残片时停止计时;
- 超时触发当前连接重建,并用携带 transport Session ID 的
<code>HsmsT8TimeoutException</code> 关闭会话。

每次接收进展都创建带代次标识的新计时器。旧计时器即使已经排队、在
Dispose 后仍执行回调,也不能使后续进展或替换 TCP 会话超时。测试使用
手动计时器覆盖部分长度头、部分 Payload、进度重置、完整帧后空闲、连续
完整帧、下一帧残片和跨会话陈旧回调;真实 TCP 回环验证分片收包与实际
T8 到期关闭。

以上 T5/T8 是当前工程基线。具体默认值、连接失败分类、T5 起止点、T8
对每个网络分片还是协议字符的精确解释,以及标准结论,仍须依据团队合法
获得的 SEMI E37/E37.1 版本及一致性测试核对。

## 替换条件

当 #38/#39 都发布稳定 NuGet API 后:

1. 运行现有会话、发送确认、迟到消息和超时测试向量验证语义等价;
2. 用原生 Session ID、发送完成任务和未完成帧超时替换内部回调适配;
3. 删除旧信封 fail-closed 路径;
4. 保持 <code>IHsmsTransport</code> 及其上层状态机契约不变。
不内置未经授权标准核对的默认值。Active 连接把 T5 映射为固定的
<code>ConnectRetryDelayMs</code> 并关闭最大退避;Passive 监听重试配置
保持调用方设置。原 <code>StreamConnectionOptions</code> 始终复制后再
适配,不会被原地修改。

T8 映射为 StreamFrame 的 <code>IncompleteFrameTimeoutMs</code>。因此
T5/T8 都必须为正值、能由整毫秒精确表示且不超过 <code>int.MaxValue</code>
毫秒;不允许截断、舍入或溢出。调用方在源 StreamFrame 选项中设置的
<code>IncompleteFrameTimeoutMs</code> 会被显式 HSMS T8 覆盖。

StreamFrame 只在已有未完成帧时运行该超时:空闲连接和完整帧后的静默不
触发;接收进展重置计时;到期先发布
<code>FrameErrorKind.IncompleteFrameTimeout</code>,再拆除对应会话。
适配器把该诊断转换为携带 transport Session ID 的
<code>HsmsT8TimeoutException</code>,不再次请求重连。

## 验证边界

适配器单元测试覆盖原生 Session ID、迟到消息、发送完成、并发入队、会话
失效、显式关闭原因和 T8 诊断映射。真实 TCP 回环测试覆盖分片帧收发与
实际 T8 到期;完整套件继续覆盖 T3/T6 从发送完成点启动以及旧会话不
重放。

这些测试证明当前工程契约与 StreamFrame 2.3.0 的互操作行为,不构成
SEMI 合规声明。T5/T8 的标准默认值、精确启停边界和异常恢复仍须依据团队
合法获得的 SEMI E37/E37.1 版本及一致性测试核对。仓库不得提交标准正文、
表格或 Schema。
18 changes: 13 additions & 5 deletions src/SecsFrame/Hsms/Connection/HsmsConnectionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public sealed class HsmsConnectionOptions
/// <param name="t5">The Active connection retry interval.</param>
/// <param name="t6">The control transaction reply timeout.</param>
/// <param name="t7">The selection timeout after TCP connection.</param>
/// <param name="t8">The incomplete-message receive timeout.</param>
/// <param name="t8">The positive whole-millisecond incomplete-message receive timeout.</param>
public HsmsConnectionOptions(
IPAddress ipAddress,
int port,
Expand Down Expand Up @@ -59,7 +59,7 @@ public HsmsConnectionOptions(
ValidateT5(t5, nameof(t5));
ValidatePositive(t6, nameof(t6), "T6");
ValidatePositive(t7, nameof(t7), "T7");
ValidatePositive(t8, nameof(t8), "T8");
ValidateStreamFrameMilliseconds(t8, nameof(t8), "T8");

Port = port;
ConnectionMode = connectionMode;
Expand Down Expand Up @@ -116,13 +116,21 @@ private static void ValidateT5(
TimeSpan value,
string parameterName)
{
ValidatePositive(value, parameterName, "T5");
ValidateStreamFrameMilliseconds(value, parameterName, "T5");
}

private static void ValidateStreamFrameMilliseconds(
TimeSpan value,
string parameterName,
string timerName)
{
ValidatePositive(value, parameterName, timerName);
if (value.Ticks % TimeSpan.TicksPerMillisecond != 0)
{
throw new ArgumentOutOfRangeException(
parameterName,
value,
"T5 must be representable as a whole number of milliseconds.");
$"{timerName} must be representable as a whole number of milliseconds.");
}

var milliseconds = value.Ticks / TimeSpan.TicksPerMillisecond;
Expand All @@ -131,7 +139,7 @@ private static void ValidateT5(
throw new ArgumentOutOfRangeException(
parameterName,
value,
"T5 exceeds the supported connection retry-delay range.");
$"{timerName} exceeds the supported StreamFrame millisecond range.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static StreamConnectionOptions Create(
KeepAliveIntervalMs = source.KeepAliveIntervalMs,
ReceiveQueueCapacity = source.ReceiveQueueCapacity,
ReceiveIdleTimeoutMs = source.ReceiveIdleTimeoutMs,
IncompleteFrameTimeoutMs = hsmsOptions.T8Milliseconds,
};

if (isActive)
Expand Down
Loading
Loading