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
52 changes: 52 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
root = true

# 基础格式
[*]
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true

[*.cs]
indent_style = space
indent_size = 4

[*.{csproj,slnx,yml,yaml,json,xml,props,targets}]
indent_style = space
indent_size = 2

[*.md]
trim_trailing_whitespace = false

# ---------------------------------------------------------------------------
# .NET 分析器(SDK 内置 NetAnalyzers + Meziantou.Analyzer)
# ---------------------------------------------------------------------------
[*.{cs,vb}]

# 安全类问题一律视为错误
dotnet_analyzer_diagnostic.category-Security.severity = error

# 逐条调优:与本项目刻意设计冲突的规则降为建议,其余保持默认(警告为主)
# 库代码大量"捕获一切异常并转为重连/隔离"是刻意设计(会话守护/回调隔离)
dotnet_diagnostic.CA1031.severity = suggestion

# ILogger 扩展调用未走 LoggerMessage 源生成(性能优化建议,非正确性)
dotnet_diagnostic.CA1848.severity = suggestion
dotnet_diagnostic.MA0032.severity = none # Use TimeSpan.Zero\TimeSpan.FromMilliseconds 等风格建议
dotnet_diagnostic.MA0051.severity = suggestion # 方法过长(StreamConnection 状态机刻意集中)

# 事件签名 EventHandler<ConnectionState> 用非 EventArgs 载荷:.NET 4.5+ 支持且对用户更直接,
# 不引入 EventArgs 包装类型
dotnet_diagnostic.MA0046.severity = none

# 选项校验用 nameof(属性) 作为 ArgumentOutOfRangeException 参数名:校验对象是属性而非方法参数
dotnet_diagnostic.MA0015.severity = none

# 测试项目:xUnit 无同步上下文,ConfigureAwait 与比较器重载建议均为噪音
[test/**.cs]
dotnet_diagnostic.MA0002.severity = none
dotnet_diagnostic.MA0004.severity = none
dotnet_diagnostic.MA0006.severity = none

# demo 控制台程序无需 ConfigureAwait 语义
[samples/**.cs]
dotnet_diagnostic.MA0004.severity = none
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- 明确 `Start(ct)` 的取消令牌语义(仅约束建立连接阶段):接口 XML 注释与 README 同步说明。

### 新增
- 代码质量棘轮:引入 Meziantou.Analyzer(SDK 自带 NetAnalyzers 之上的补充规则)与 `.editorconfig`,现存告警清零并把库项目的 `TreatWarningsAsErrors` 打开(告警即构建失败);`FrameErrorKind`/`DecodeErrorPolicy` 拆分为独立文件(一类型一文件)。
- `WaitForConnectedAsync`:等待连接进入 Connected(已连接立即完成;取消或 Dispose 以取消结束),替代轮询状态或 `Task.Delay` 盲等;demo 全部改用该 API。
- 可选 `ILogger` 构造参数:连接重试、会话故障、用户回调异常等内部事件输出到日志(此前 `Debug.WriteLine` 在 Release 构建中完全不可见,生产排障只能靠猜)。
- `ReceiveQueueCapacity` 选项:接收消息通道可设上限(默认 0 不限制);消费慢时解码暂停读取、TCP 背压自然传导到对端,封堵慢消费者导致的内存无限增长。
Expand Down
10 changes: 9 additions & 1 deletion src/StreamFrame.Protocols.Xml/StreamFrame.Protocols.Xml.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\StreamFrame\StreamFrame.csproj" />
Expand All @@ -8,10 +8,18 @@
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="3.0.187">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<PropertyGroup>
Expand Down
19 changes: 19 additions & 0 deletions src/StreamFrame/DecodeErrorPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace StreamFrame;

/// <summary>
/// 帧结构完整但内容解码失败(codec 抛异常)时的处理策略。
/// </summary>
public enum DecodeErrorPolicy
{
/// <summary>
/// 断开连接并走自动重连(默认)。协议内容错乱后流状态通常不可信,
/// 重连是对端与本地同时回到一致状态的最短路径。
/// </summary>
Disconnect,

/// <summary>
/// 丢弃坏帧、继续解析后续帧。适合线路噪声多、流定界自身可靠(如长度前缀)的场景;
/// 坏帧会通过 FrameError 事件上报。
/// </summary>
SkipFrame,
}
12 changes: 0 additions & 12 deletions src/StreamFrame/FrameErrorEventArgs.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
namespace StreamFrame;

/// <summary>帧层诊断事件的类别。</summary>
public enum FrameErrorKind
{
/// <summary>帧结构完整,但帧内负载解码失败(codec 抛出异常)。</summary>
DecodeFailed,

/// <summary>字节被帧定界器当作噪声/垃圾丢弃(如非法长度头重同步、STX/ETX 之外的杂散字节)。</summary>
DiscardedByResync,

/// <summary>未完成帧的已缓冲字节超过上限,判定流不可恢复。</summary>
IncompleteFrameOverflow,
}

/// <summary>
/// <see cref="IStreamConnection{TMessage}.FrameError"/> 事件的参数。
Expand Down
14 changes: 14 additions & 0 deletions src/StreamFrame/FrameErrorKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace StreamFrame;

/// <summary>帧层诊断事件的类别。</summary>
public enum FrameErrorKind
{
/// <summary>帧结构完整,但帧内负载解码失败(codec 抛出异常)。</summary>
DecodeFailed,

/// <summary>字节被帧定界器当作噪声/垃圾丢弃(如非法长度头重同步、STX/ETX 之外的杂散字节)。</summary>
DiscardedByResync,

/// <summary>未完成帧的已缓冲字节超过上限,判定流不可恢复。</summary>
IncompleteFrameOverflow,
}
17 changes: 0 additions & 17 deletions src/StreamFrame/StreamConnectionOptions.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,5 @@
namespace StreamFrame;

/// <summary>
/// 帧结构完整但内容解码失败(codec 抛异常)时的处理策略。
/// </summary>
public enum DecodeErrorPolicy
{
/// <summary>
/// 断开连接并走自动重连(默认)。协议内容错乱后流状态通常不可信,
/// 重连是对端与本地同时回到一致状态的最短路径。
/// </summary>
Disconnect,

/// <summary>
/// 丢弃坏帧、继续解析后续帧。适合线路噪声多、流定界自身可靠(如长度前缀)的场景;
/// 坏帧会通过 FrameError 事件上报。
/// </summary>
SkipFrame,
}

/// <summary>
/// <see cref="IStreamConnection{TMessage}"/> 的可调参数。
Expand Down
5 changes: 5 additions & 0 deletions src/StreamFrame/StreamFrame.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<PropertyGroup>
Expand All @@ -23,6 +24,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="3.0.187">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="System.IO.Pipelines" Version="8.0.0" />
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
Expand Down
4 changes: 4 additions & 0 deletions test/StreamFrame.Tests/StreamFrame.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Meziantou.Analyzer" Version="3.0.187">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
Expand Down
40 changes: 0 additions & 40 deletions test/StreamFrame.Tests/StreamingFrameEncodeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,43 +96,3 @@ public void StxEtx_StreamingRejectsPayloadContainingStxOrEtx()
Assert.Throws<InvalidOperationException>(() => framing.EndFrame(writer));
}
}

/// <summary>测试用 IWrittenBufferWriter 实现(内部用数组增长)。</summary>
internal sealed class TestWrittenBufferWriter : IWrittenBufferWriter, IDisposable
{
private byte[] _buffer = new byte[16];
private int _written;

public Span<byte> WrittenSpan => _buffer.AsSpan(0, _written);
public int WrittenCount => _written;

public void Advance(int count)
{
if (count < 0 || _written + count > _buffer.Length)
throw new ArgumentOutOfRangeException(nameof(count));
_written += count;
}

public Memory<byte> GetMemory(int sizeHint = 0)
{
EnsureCapacity(sizeHint);
return _buffer.AsMemory(_written);
}

public Span<byte> GetSpan(int sizeHint = 0)
{
EnsureCapacity(sizeHint);
return _buffer.AsSpan(_written);
}

private void EnsureCapacity(int sizeHint)
{
// GetSpan 不允许返回空 span,必须保证剩余空间 ≥ max(sizeHint, 1)
var required = _written + Math.Max(sizeHint, 1);
if (required <= _buffer.Length)
return;
Array.Resize(ref _buffer, Math.Max(required, _buffer.Length * 2));
}

public void Dispose() { }
}
43 changes: 43 additions & 0 deletions test/StreamFrame.Tests/TestWrittenBufferWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Buffers;

namespace StreamFrame.Tests;

/// <summary>测试用 IWrittenBufferWriter 实现(内部用数组增长)。</summary>
internal sealed class TestWrittenBufferWriter : IWrittenBufferWriter, IDisposable
{
private byte[] _buffer = new byte[16];
private int _written;

public Span<byte> WrittenSpan => _buffer.AsSpan(0, _written);
public int WrittenCount => _written;

public void Advance(int count)
{
if (count < 0 || _written + count > _buffer.Length)
throw new ArgumentOutOfRangeException(nameof(count));
_written += count;
}

public Memory<byte> GetMemory(int sizeHint = 0)
{
EnsureCapacity(sizeHint);
return _buffer.AsMemory(_written);
}

public Span<byte> GetSpan(int sizeHint = 0)
{
EnsureCapacity(sizeHint);
return _buffer.AsSpan(_written);
}

private void EnsureCapacity(int sizeHint)
{
// GetSpan 不允许返回空 span,必须保证剩余空间 ≥ max(sizeHint, 1)
var required = _written + Math.Max(sizeHint, 1);
if (required <= _buffer.Length)
return;
Array.Resize(ref _buffer, Math.Max(required, _buffer.Length * 2));
}

public void Dispose() { }
}
4 changes: 2 additions & 2 deletions test/StreamFrame.Tests/XmlDocumentCodecTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public void EncodeThenDecode_RoundTripsDocument()

// 编码结果应包含 XML 内容
var xmlText = Encoding.UTF8.GetString(writer.WrittenSpan);
Assert.Contains("<Message>", xmlText);
Assert.Contains("DeviceA", xmlText);
Assert.Contains("<Message>", xmlText, StringComparison.Ordinal);
Assert.Contains("DeviceA", xmlText, StringComparison.Ordinal);

// 解码还原
var decoded = codec.Decode(new ReadOnlySequence<byte>(writer.WrittenSpan.ToArray()));
Expand Down
Loading