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
11 changes: 1 addition & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- **Soak 目标矩阵([#66](https://github.com/CSJ608/StreamFrame/issues/66))**:显式选择 Ubuntu net8.0/net10.0、Windows net8.0/net10.0/net48,避免 Ubuntu 启动 .NET Framework 测试;job 名、详细日志与始终尝试上传的 TRX 产物按 OS/TFM 区分,测试断言失败继续使 job 失败。普通 CI 必需检查名不变。

### 变更
- **发版前文档与注释核对清理**:① 修正 `StreamFrame.Protocols.Xml` 包描述与 DESIGN 中的序列化器旧说法——实现为 XmlReader→XDocument 解码、XmlWriter 编码,从未使用 XmlSerializer;② 删除 `ConnectionMetrics` 关于 netstandard2.0 指标 no-op 兜底的陈旧注释(实际经 `System.Diagnostics.DiagnosticSource` 包提供同款 API,与 csproj/README 一致);③ XML 注释统一全中文——移除 #69 批次遗留的 11 处英文复述行(IStreamConnection/StreamConnectionOptions/IFramer/IStreamingFramer/ICodec),并去掉 `StxEtxFramer` 文档中的内部项目名引用;④ `IFrameDiscardReporting` 措辞修正(该接口由定界器实现,"第三方 codec"→"第三方定界器");⑤ DESIGN 项目结构树补齐现有文件(Diagnostics/Compatibility/RetryDelayScheduler/会话感知类型/bench 与两个 samples),demo 场景数三→五;⑥ CHANGELOG 2.1.0 段去重(新增/变更四处重复条目;经 tag 区间核实 LOGO 随本版本生效为正确说法,错挂的 social preview 句删除——该资产实为 2.2.1 引入)并移除空「暂无」段;⑦ README.en 的 JSON 最简驱动示例从「安装」移至「Codec」小节,与中文版结构对齐。不改变任何代码行为。
- 依赖升级(Dependabot,[PR #60](https://github.com/CSJ608/StreamFrame/pull/60)、[PR #91](https://github.com/CSJ608/StreamFrame/pull/91)、[PR #98](https://github.com/CSJ608/StreamFrame/pull/98),重复的 [#92](https://github.com/CSJ608/StreamFrame/pull/92) 已关闭):① CI Actions——soak 工作流 `actions/checkout` v4→v7、`actions/upload-artifact` v4→v7(全部工作流对齐 Node 24 世代,Node 20 弃用告警清零);② 进包依赖(patch)——`System.IO.Pipelines` / `System.Threading.Channels` / `Microsoft.Bcl.AsyncInterfaces` / `Microsoft.Extensions.Logging.Abstractions` / `System.Diagnostics.DiagnosticSource` 10.0.11→10.0.12;③ 工具链(不进包)——`Meziantou.Analyzer` 3.0.189→3.0.294、`coverlet.collector` 10.0.1→10.1.0、`Microsoft.NET.Test.Sdk` 18.9.0→18.10.1。
- **会话对象化与拆除分段([#68](https://github.com/CSJ608/StreamFrame/issues/68) PR-2)**:会话五元组(socket/Pipe/取消源/接收、解码、发送三任务/编号纪元/建立时刻)收敛为不可变的内部 `Session` 对象——会话任务只操作所属会话的资源(接收循环与发送 worker 绑定本会话 socket,不再读"当前值"字段),直连迁移与拆除竞速中新旧会话互不干扰;拆除只关本会话的 socket(`_socket` 字段降级为诊断/交接镜像)。会话拆除分段:锁内仅快速中止(取消令牌 + **先关 socket 解锁阻塞 I/O**——netfx 上取消令牌无法中止已提交的接收),有界等待(≤2s)移到锁外——高并发重连/停机不再在会话锁上排队,netfx 拆除收敛加快;停机/重连的总阻塞时长与公共 API 的同步/异步行为不变。全套测试(139+139+131)多轮全绿;同机同口径基准抽检(OneWayThroughput vs issue-70 基线):框架路径与 DirectTcp 对照同向漂移(环境噪声),未观测框架特有劣化,每操作分配无系统性增长。
- **状态机与所有权规范([#68](https://github.com/CSJ608/StreamFrame/issues/68) PR-1)**:DESIGN 增补权威清单——状态转换全表与发布段/通知段规则、资源所有权表(会话 socket/监听器/会话五元组/消息通道)、锁与同步原语清单、允许重入点(回调内 Reconnect/Dispose、停机后调用、迟到旧会话任务)及已知限制(拆除同步等待持锁、`_socket` 字段语义——指向后续内部 Session 对象重构);与新增的 `StateOwnershipBaselineTests` 共同构成 #68 会话所有权重构的行为基线。
Expand Down Expand Up @@ -108,9 +109,6 @@
- **XML 文档随 NuGet 包发布**(`GenerateDocumentationFile`):此前全量中文 XML 注释从未进包,用户在 IDE 中没有 IntelliSense;现以 `<inheritdoc />` 补齐全部接口实现成员的文档引用,并由 `TreatWarningsAsErrors` 强制今后不出现未文档化的公共 API。
- 基准测试扩容:`CodecBenchmarks`(XmlDocumentCodec 典型报文编解码开销)与 `EndToEndBenchmarks`(真实 TCP 回环的单向吞吐(双 framer)与往返延迟);基准项目纳入解决方案(IDE 可见,随解决方案构建;`dotnet test` 依 IsTestProject 机制自动跳过)。

### 修复
- 暂无

## [2.1.0] - 2026-08-27

### 新增
Expand All @@ -123,13 +121,6 @@
### 变更
- CI Actions 升级到 Node 24 运行时:`actions/checkout@v7`、`actions/setup-dotnet@v6`、`softprops/action-gh-release@v3`(消除 Node 20 弃用告警)。
- `GetMessages`/发送 worker 改用 `WaitToReadAsync+TryRead` 等价循环(ns2.0 的 Channels 包无 `ReadAllAsync`;各目标行为一致)。
- 帧定界基准测试 `bench/StreamFrame.Benchmarks`(BenchmarkDotNet):LengthPrefix/StxEtx 的流式 vs 纯函数编码对比与切帧吞吐,为"流式零拷贝"说法提供可复现数据。
- 项目 LOGO(`docs/logo/`,SVG 母版 + PNG),README 双语版接入展示、两个 NuGet 包启用包图标(随下个版本发布生效)。
- README 新增 CI 构建状态徽章;main 分支保护(必须走 PR 且 CI 通过、禁 force push/删除、强制线性历史)。
- 英文版 README(`README.en.md`,与中文版互链),便于国际用户检索与阅读。

### 修复
- 暂无

## [2.0.0] - 2026-08-27

Expand Down
44 changes: 21 additions & 23 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,6 @@ XML message driver (optional):
dotnet add package StreamFrame.Protocols.Xml
```

Minimal JSON driver (System.Text.Json, span-written, AOT-safe):

```csharp
sealed class SystemTextJsonCodec : ICodec<JsonElement>
{
public static readonly SystemTextJsonCodec Instance = new();

public JsonElement Decode(in ReadOnlySequence<byte> frame, CancellationToken ct = default)
{
using var document = JsonDocument.Parse(frame.ToArray());
return document.RootElement.Clone(); // Own data beyond document disposal.
}

public void Encode(JsonElement message, IBufferWriter<byte> writer, CancellationToken ct = default)
{
var raw = message.GetRawText(); // serialized form (non-ASCII already escaped)
var span = writer.GetSpan(Encoding.UTF8.GetMaxByteCount(raw.Length));
writer.Advance(Encoding.UTF8.GetBytes(raw, span));
}
}
```

## Quick start

This is a **single-connection device integration library**. A passive listener serves one client at a time; `AcceptFirstClientOnly = false` keeps the listener open without adding multi-client handling. The snippets below represent separate processes/roles, with a host-provided `ct`. See the production configuration below and dispose connections with `await using`.
Expand Down Expand Up @@ -111,7 +89,27 @@ public interface ICodec<TMessage>
}
```

**Onboarding a new device = writing one driver**: implement `ICodec<TMessage>`, define your message type, pick a framer — everything else is reused. See `StreamFrame.Protocols.Xml` for the official example.
**Onboarding a new device = writing one driver**: implement `ICodec<TMessage>`, define your message type, pick a framer — everything else is reused. See `StreamFrame.Protocols.Xml` for the official example. A minimal JSON-message driver (System.Text.Json, span-written, AOT-safe):

```csharp
sealed class SystemTextJsonCodec : ICodec<JsonElement>
{
public static readonly SystemTextJsonCodec Instance = new();

public JsonElement Decode(in ReadOnlySequence<byte> frame, CancellationToken ct = default)
{
using var document = JsonDocument.Parse(frame.ToArray());
return document.RootElement.Clone(); // Own data beyond document disposal.
}

public void Encode(JsonElement message, IBufferWriter<byte> writer, CancellationToken ct = default)
{
var raw = message.GetRawText(); // serialized form (non-ASCII already escaped)
var span = writer.GetSpan(Encoding.UTF8.GetMaxByteCount(raw.Length));
writer.Advance(Encoding.UTF8.GetBytes(raw, span));
}
}
```

### Connection (`IStreamConnection<T>`) — the transport

Expand Down
55 changes: 34 additions & 21 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,41 @@
```
StreamFrame/
├─ StreamFrame.slnx
├─ src/StreamFrame/ # 核心库(无业务依赖);所有公共类型都在 StreamFrame 命名空间
│ ├─ ConnectionState.cs # Connecting / Connected / Retry / Disconnected
│ ├─ IStreamConnection.cs # IStreamConnection<TMessage>
├─ src/StreamFrame/ # 核心库(无业务依赖);所有公共类型都在 StreamFrame 命名空间
│ ├─ ConnectionState.cs # Connecting / Connected / Retry / Disconnected
│ ├─ IStreamConnection.cs # IStreamConnection<TMessage>
│ ├─ ISessionAwareStreamConnection.cs # 会话感知收发(可选能力接口)
│ ├─ StreamConnectionOptions.cs
│ ├─ DecodeErrorPolicy.cs # 帧内容解码失败策略(Disconnect / SkipFrame)
│ ├─ FrameErrorKind.cs / FrameErrorEventArgs.cs # 帧层诊断事件
│ ├─ SessionExpiredException.cs # 会话绑定发送失效异常
│ ├─ SessionMessage.cs # 带会话编号的接收元素
│ ├─ Framing/
│ │ ├─ IFramer.cs # 帧定界接口(EncodeFrame/TryDecodeFrame/MaxPayloadBytes)
│ │ ├─ LengthPrefixFramer.cs # 4 字节大端长度头
│ │ ├─ StxEtxFramer.cs # STX 0x02 … ETX 0x03(plain,不做转义)
│ │ ├─ IStreamingFramer.cs # 可选:流式单缓冲编码(Begin/EndFrame)
│ │ ├─ IFrameDiscardReporting.cs # 可选:重同步丢弃字节上报
│ │ └─ IWrittenBufferWriter.cs # 流式编码用的可回写缓冲抽象
│ │ ├─ IFramer.cs # 帧定界接口(EncodeFrame/TryDecodeFrame/MaxPayloadBytes)
│ │ ├─ LengthPrefixFramer.cs # 4 字节大端长度头
│ │ ├─ StxEtxFramer.cs # STX 0x02 … ETX 0x03(plain,不做转义)
│ │ ├─ IStreamingFramer.cs # 可选:流式单缓冲编码(Begin/EndFrame)
│ │ ├─ IFrameDiscardReporting.cs # 可选:重同步丢弃字节上报
│ │ └─ IWrittenBufferWriter.cs # 流式编码用的可回写缓冲抽象
│ ├─ Codec/
│ │ ├─ ICodec.cs # ICodec<TMessage>(驱动实现)
│ │ ├─ BufferWriterStream.cs # IBufferWriter→Stream,供 XmlWriter 直写
│ │ └─ PooledBufferWriter.cs # 内部:ArrayPool 租用的 IWrittenBufferWriter
│ └─ Connection/
│ ├─ StreamConnection.cs # 连接实现(双模式 + 状态机 + 重连 + 生命周期)
│ ├─ FrameDecoder.cs # 解码循环(切帧 + codec 解码)
│ └─ SessionFaultException.cs # 会话级故障(内部)
├─ src/StreamFrame.Protocols.Xml/ # 官方示例驱动:XML 报文
├─ test/StreamFrame.Tests/ # xUnit 单测
└─ samples/StreamFrame.Demo/ # 控制台端到端 demo
│ │ ├─ ICodec.cs # ICodec<TMessage>(驱动实现)
│ │ ├─ BufferWriterStream.cs # IBufferWriter→Stream,供 XmlWriter 直写
│ │ └─ PooledBufferWriter.cs # 内部:ArrayPool 租用的 IWrittenBufferWriter
│ ├─ Connection/
│ │ ├─ StreamConnection.cs # 连接实现(双模式 + 状态机 + 重连 + 生命周期 + Session 对象)
│ │ ├─ FrameDecoder.cs # 解码循环(切帧 + codec 解码)
│ │ ├─ RetryDelayScheduler.cs # 内部:重连退避调度(倍增封顶 + 抖动)
│ │ └─ SessionFaultException.cs # 会话级故障(内部)
│ ├─ Diagnostics/
│ │ └─ ConnectionMetrics.cs # 内部:内置指标(System.Diagnostics.Metrics)
│ └─ Compatibility/
│ └─ NetStandard20Shims.cs # netstandard2.0 兼容层(Task.WaitAsync / Socket Memory 重载)
├─ src/StreamFrame.Protocols.Xml/ # 官方示例驱动:XML 报文
├─ test/StreamFrame.Tests/ # xUnit 单测
├─ samples/StreamFrame.Demo/ # 控制台端到端 demo(五场景)
├─ samples/StreamFrame.Ownership/ # 缓冲所有权示例(源文件纳入测试工程三目标编译)
├─ samples/StreamFrame.AotSmoke/ # NativeAOT 发布/运行冒烟(CI 非必需检查)
└─ bench/StreamFrame.Benchmarks/ # BenchmarkDotNet 基准
```

命名空间约定:公共类型统一收敛在根命名空间 `StreamFrame`(2.0 起不再有 `StreamFrame.Abstractions`),用户只需一个 `using StreamFrame;`;目录仅按角色组织文件。
Expand Down Expand Up @@ -72,7 +85,7 @@ public interface ICodec<TMessage>
}
```

驱动=实现 codec + 定义业务消息类。官方示例:`StreamFrame.Protocols.Xml.XmlDocumentCodec`(XmlSerializer 解码 / BufferWriterStream+XmlWriter 编码)。用户按此骨架替换即可支持自定义二进制协议。
驱动=实现 codec + 定义业务消息类。官方示例:`StreamFrame.Protocols.Xml.XmlDocumentCodec`(XmlReader→XDocument 解码 / BufferWriterStream+XmlWriter 编码)。用户按此骨架替换即可支持自定义二进制协议。

### 3. 连接核心(客户端/服务端双模式)

Expand Down Expand Up @@ -209,6 +222,6 @@ socket --ReceiveAsync--> pipe.Writer --[Pipe]--> FrameDecoder.RunAsync
- XML codec:往返、DTD 拒绝(XXE 防御)
- 连接端到端(真实 TCP 回环):断线重连后消息送达、解码失败断线/跳帧、调试钩子抛异常会话存活、接收空闲超时、发送失败重连、未完成帧超限断线、单客户端拒绝
- 易用性与生命周期:等待连接就绪(跨重试/挂起/Dispose 取消)、Start 重入防护、选项构造校验、慢消费者有界接收队列、Start 令牌取消停机(Disconnected 终态/通道关闭)、对端地址语义、双栈监听 IPv4 接入
- 端到端:`samples/StreamFrame.Demo` 演示 XML+LengthPrefix、文本+STX/ETX、断线重连三场景
- 端到端:`samples/StreamFrame.Demo` 演示五场景:XML+LengthPrefix、文本+STX/ETX、断线自动重连、心跳保活+接收空闲超时、会话感知收发

运行:`dotnet build StreamFrame.slnx`、`dotnet test`、`dotnet run --project samples/StreamFrame.Demo`
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<PropertyGroup>
<PackageId>StreamFrame.Protocols.Xml</PackageId>
<Version>2.6.0</Version>
<Description>StreamFrame 的 XML 报文示例驱动:XmlDocumentCodec(XmlSerializer / XmlWriter 编解码)。</Description>
<Description>StreamFrame 的 XML 报文示例驱动:XmlDocumentCodec(XmlReader/XDocument 解码、XmlWriter 编码)。</Description>
<Authors>Marci</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>icon.png</PackageIcon>
Expand Down
9 changes: 1 addition & 8 deletions src/StreamFrame/Codec/ICodec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,9 @@ namespace StreamFrame;
/// 由驱动实现——例如 XML 驱动把负载解析为强类型消息,自定义二进制协议驱动手写字节布局。
/// </summary>
/// <remarks>
/// 并发 / Concurrency: 同一会话的发送 worker 串行编码,但 Encode 与 Decode 可并行。
/// 并发:同一会话的发送 worker 串行编码,但 Encode 与 Decode 可并行。
/// 会话拆除只作有界等待,旧任务可能与新会话重叠(包括 Decode/Decode,阻塞的 Encode 也不能假定已退出)。
/// 实现应无共享可变状态或自行同步;跨连接共享实例的调用方负责确认实现支持并发,框架不提供实例级锁。
/// A send worker serializes its own calls, not the codec instance. Encode/Decode and old/new
/// session calls can overlap. Use stateless implementations or synchronization; callers sharing
/// an instance across connections must ensure it is safe. Avoid blocking synchronous methods.
/// </remarks>
/// <typeparam name="TMessage">业务消息类型,贯穿一条连接。</typeparam>
public interface ICodec<TMessage>
Expand All @@ -23,17 +20,13 @@ public interface ICodec<TMessage>
/// frame 仅供同步调用期间借用;返回对象(包括嵌套字段)必须独立拥有数据。
/// 不得未经拷贝保留 frame、其切片或底层 Memory;管线随后推进/释放,异步消费者可能更晚读取。
/// ReadOnlyMemory 只限制写入接口,不转移所有权。本接口不转移管线内存所有权。
/// Borrow frame only during this synchronous call. Copy retained bytes into message-owned storage;
/// never return pipeline slices, even as ReadOnlyMemory. No pipeline ownership transfer is provided.
/// </remarks>
TMessage Decode(in ReadOnlySequence<byte> frame, CancellationToken ct = default);

/// <summary>把一条业务消息编码为帧内负载字节,写入 <paramref name="writer"/>。</summary>
/// <remarks>
/// 必须同步完成写入;不得保留 writer 或其缓冲供返回后/异步使用,也不得释放框架拥有的 writer。
/// 调用方应保持已入队消息及其底层数据不变,普通 SendAsync 完成仅表示入队,编码可能尚未开始。
/// Complete writes synchronously. Do not retain/dispose the writer or retain its buffers after return.
/// Queued messages must remain unchanged: SendAsync completion does not mean encoding has finished.
/// </remarks>
void Encode(TMessage message, IBufferWriter<byte> writer, CancellationToken ct = default);
}
2 changes: 1 addition & 1 deletion src/StreamFrame/Diagnostics/ConnectionMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace StreamFrame;
/// 连接级内置指标(<see cref="Meter"/> 名 "StreamFrame",零外部依赖):
/// 帧收发计数、字节收发计数、重连次数、会话时长、发送队列水位。生产部署用
/// MeterListener / OpenTelemetry 订阅 "StreamFrame" 即可观测,不接入则开销为每次记录
/// 数纳秒级;netstandard2.0 目标为 no-op 兜底(无 Metrics API,记录全部空操作)。
/// 数纳秒级;netstandard2.0 目标经 System.Diagnostics.DiagnosticSource 包获得同款 API(netfx 运行时可用)。
///
/// 标签:单一 "endpoint"(构造时的 ip:port)——每条连接的地址固定,标签基数受控。
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/StreamFrame/Framing/IFrameDiscardReporting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace StreamFrame;
/// 正常切帧时 <c>discarded</c> 为空。
///
/// 连接层检测到本接口后,会把这些字节通过 FrameError 事件(Kind=DiscardedByResync)
/// 交给上层调试;不实现本接口的第三方 codec 不受影响,只是没有丢弃上报。
/// 交给上层调试;不实现本接口的第三方定界器不受影响,只是没有丢弃上报。
/// </summary>
public interface IFrameDiscardReporting
{
Expand Down
Loading
Loading