From e8879a1f1ef721494623d13e80d8b30517650d0e Mon Sep 17 00:00:00 2001 From: Marci Date: Thu, 27 Aug 2026 13:40:58 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=8B=B1=E6=96=87=E7=89=88=20README?= =?UTF-8?q?=EF=BC=88README.en.md=EF=BC=8C=E4=B8=8E=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E7=89=88=E4=BA=92=E9=93=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en.md | 179 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 3 + 2 files changed, 182 insertions(+) create mode 100644 README.en.md diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..67c8a95 --- /dev/null +++ b/README.en.md @@ -0,0 +1,179 @@ +# StreamFrame + +[![NuGet](https://img.shields.io/nuget/v/StreamFrame.svg)](https://www.nuget.org/packages/StreamFrame) +[![NuGet Downloads](https://img.shields.io/nuget/dt/StreamFrame)](https://www.nuget.org/stats/packages/StreamFrame?groupby=Version) + +English | [简体中文](README.md) + +**A general-purpose socket communication library** that pluggably separates *frame delimiting* from *payload encoding/decoding*. Built for scenarios where peers exchange data over sockets (device communication, logistics/WMS integration, etc.): the common parts (connection management, reconnection, I/O, TCP packet fragmentation/reassembly, message dispatching) live in the core, while the parts that vary (framing, codec) are pluggable drivers. + +## Why StreamFrame + +| | Hand-rolled | StreamFrame | +|---|---|---| +| Onboarding a new device/protocol | Rewrite connection, delimiting, encoding | Write one `ICodec` driver | +| Frame boundaries (length-prefix / STX-ETX / custom) | Every project rolls its own | Two built-in + pluggable | +| Fragmented/partial TCP packets | Manual buffer stitching | Handled by Pipelines | +| Reconnection / state machine | Hand-rolled | Built-in auto-reconnect | +| Send performance | Multiple copies | Optional single-buffer streaming encode | + +## Installation + +``` +dotnet add package StreamFrame +``` + +XML message driver (optional): + +``` +dotnet add package StreamFrame.Protocols.Xml +``` + +## Quick start + +One connection = one **framer** + one **codec** + address/port/mode. Framing and codec are both **fixed per connection**: a connection carries exactly one message type in exactly one frame format. + +```csharp +using System.Net; +using System.Xml.Linq; +using StreamFrame; +using StreamFrame.Protocols.Xml; + +// Server (passive listener) +var server = new StreamConnection( + new LengthPrefixFramer(), // 4-byte big-endian length header; or StxEtxFramer + new XmlDocumentCodec(), // swap in your own ICodec for a custom protocol + IPAddress.Any, 5100, isActive: false); +server.Start(ct); + +await foreach (var doc in server.GetMessages(ct)) +{ + var id = doc.Root?.Element("Id")?.Value; + await server.SendAsync(XDocument.Parse($"{id}"), ct); +} + +// Client (active connector) +var client = new StreamConnection( + new LengthPrefixFramer(), + new XmlDocumentCodec(), + IPAddress.Parse("127.0.0.1"), 5100, isActive: true); +client.Start(ct); +await client.SendAsync(XDocument.Parse("1"), ct); +``` + +## Concepts + +### Framing (`IFramer`) — slicing frames out of a byte stream + +| Implementation | Delimiting | Suitable for | +|---|---|---| +| `LengthPrefixFramer` | 4-byte **big-endian** length header + payload | General purpose, binary-safe | +| `StxEtxFramer` | Wrapped in STX `0x02` … ETX `0x03` | XML / plain-text payloads known to be marker-safe | + +Both built-in implementations support **streaming single-buffer encoding** (optional, removes the payload memcpy on send). Implement `IFramer` to define a custom frame format. + +### Codec (`ICodec`) — parsing/writing the payload inside a frame + +```csharp +public interface ICodec +{ + TMessage Decode(in ReadOnlySequence frame, CancellationToken ct = default); + void Encode(TMessage message, IBufferWriter writer, CancellationToken ct = default); +} +``` + +**Onboarding a new device = writing one driver**: implement `ICodec`, define your message type, pick a framer — everything else is reused. See `StreamFrame.Protocols.Xml` for the official example. + +### Connection (`IStreamConnection`) — the transport + +- **Client/server dual mode**: `isActive: true` connects out, `false` listens; IPv4/IPv6 dual-stack (listening on `IPAddress.Any` is normalized to dual-stack; IPv4 client addresses are displayed as plain IPv4) +- **Auto-reconnect**: `Connecting → Connected → Retry` state machine; `GetMessages` is a stable stream across reconnections — received messages are not lost and enumeration does not break when the connection drops and recovers +- **Start & stop**: the `ct` passed to `Start(ct)` is the connection's **lifetime token** — cancelling it stops connection/reconnection and tears the link down (state enters the terminal `Disconnected`, `GetMessages` completes naturally; create a new connection afterwards). `DisposeAsync` does the same. After shutdown, `SendAsync` throws `ChannelClosedException` +- **Waiting for readiness**: `await conn.WaitForConnectedAsync(ct)` — completes immediately when connected; otherwise waits for the next successful connection (or cancellation/dispose). No state polling, no `Task.Delay` guessing +- **Robustness**: payload decode failures, incomplete-frame overflows, send failures, and receive idle timeouts all invalidate the session and rebuild it automatically (no more "connection looks alive while messages silently vanish") +- **Liveness detection (optional)**: TCP KeepAlive and receive idle timeout to catch half-open connections (peer power loss / unplugged cable) +- **Events**: `ConnectionChanged` state changes, `FrameError` frame-level diagnostics, `RawBytesReceived/Sent` raw bytes (HEX debugging) +- **Backpressure**: bounded send queue — `SendAsync` waits when full; the receive side buffers without limit by default, or set `ReceiveQueueCapacity` — decoding pauses when the consumer lags, propagating TCP backpressure to the peer and preventing unbounded memory growth + +## Diagnostics & debugging + +### FrameError — frame-level diagnostics + +When the peer sends bad data, the `FrameError` event hands you the **offending bytes** and the reason directly — no more eyeballing HEX dumps: + +```csharp +client.FrameError += (_, e) => +{ + // e.Kind: DecodeFailed (frame intact, payload parsing failed) + // DiscardedByResync (bytes discarded as noise by the framer) + // IncompleteFrameOverflow (incomplete-frame buffer over its limit) + // e.Bytes: copied; safe to retain long-term + // e.Exception: the original exception for DecodeFailed + Console.WriteLine($"[{e.Kind}] {Convert.ToHexString(e.Bytes.Span)} {e.Exception?.Message}"); +}; +``` + +The policy for payload decode failures is set by `StreamConnectionOptions.DecodeErrorPolicy`: + +| Policy | Behavior | +|---|---| +| `Disconnect` (default) | Reconnect — after content corruption the stream state is usually untrustworthy | +| `SkipFrame` | Drop the bad frame and continue; suits noisy lines | + +### RawBytesReceived / RawBytesSent — raw byte taps + +Full output from the socket layer (including noise bytes later discarded by the framer); the send side fires per chunk actually written (bytes already on the wire remain visible after a partial-send failure). **Memory contract**: callback arguments are slices of internal buffers, valid only during the synchronous callback — copy them if you need to retain; handler exceptions are isolated and never affect the session. + +### Incomplete-frame protection + +A peer that announces a huge frame but never completes it (or a stream with STX but no closing ETX) would hold the buffer forever. `MaxIncompleteFrameBufferBytes` (default = frame limit + 4 KB) puts a hard cap on half-frames: exceeding it disconnects and reports via `FrameError`. + +### Logging (optional) + +Pass an `ILogger` when constructing the connection to route internal events (connect retries, session faults, user-callback exceptions) to your logs — no more silence in production: + +```csharp +var conn = new StreamConnection(..., logger: loggerFactory.CreateLogger("StreamFrame")); +``` + +Without one, no logging happens (zero-dependency usage). + +### Liveness tips + +For production, enable `TcpKeepAlive = true`; for protocols with periodic traffic, also consider `ReceiveIdleTimeoutMs` (e.g., 3× the heartbeat period) as a second safety net against half-open connections. + +## Performance + +Measured with BenchmarkDotNet (details and how to reproduce in [bench/README.md](bench/README.md)): + +- **Streaming encode**: halves per-frame heap allocation (single vs. double buffer); 25–35% faster for small payloads, on par for large ones; +- **Frame decoding**: `LengthPrefixFramer` ≈10 ns/frame vs `StxEtxFramer` ≈1.1 µs/frame (byte-by-byte scanning) — prefer length-prefix for high-throughput scenarios. + +## Dependencies + +- [System.IO.Pipelines](https://www.nuget.org/packages/System.IO.Pipelines) +- [System.Threading.Channels](https://www.nuget.org/packages/System.Threading.Channels) +- [Microsoft.Extensions.Logging.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Abstractions) (logging abstractions only — no logging framework is pulled in) + +## Tests & samples + +```bash +dotnet build StreamFrame.slnx +dotnet test +dotnet run --project samples/StreamFrame.Demo # 3-scenario end-to-end demo +dotnet run -c Release --project bench/StreamFrame.Benchmarks # benchmarks (~5-10 min) +``` + +## Project layout + +``` +src/StreamFrame/ # core library (no business dependencies) +src/StreamFrame.Protocols.Xml/ # XML message driver (sample codec) +test/StreamFrame.Tests/ # xUnit tests +samples/StreamFrame.Demo/ # console end-to-end demo +bench/StreamFrame.Benchmarks/ # BenchmarkDotNet benchmarks +``` + +## License + +MIT diff --git a/README.md b/README.md index 9c42803..63d29fa 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![NuGet](https://img.shields.io/nuget/v/StreamFrame.svg)](https://www.nuget.org/packages/StreamFrame) [![NuGet Downloads](https://img.shields.io/nuget/dt/StreamFrame)](https://www.nuget.org/stats/packages/StreamFrame?groupby=Version) +[English](README.en.md) | 简体中文 + **通用 socket 通讯框架**:把"帧边界判定"与"帧内数据编解码"插件化,适用于通过 socket 进行数据互换的场景(设备通讯、物流 WMS 对接等)。把场景间的共性(连接管理、重连、读写、粘包/半包、消息分发)收敛到框架核心,把差异性(framing、codec)抽象成可插拔的驱动。 ## 为什么用它 @@ -169,6 +171,7 @@ src/StreamFrame/ # 核心库(无业务依赖) src/StreamFrame.Protocols.Xml/ # XML 报文驱动(示例 codec) test/StreamFrame.Tests/ # xUnit 单测 samples/StreamFrame.Demo/ # 控制台端到端 demo +bench/StreamFrame.Benchmarks/ # 性能基准 ``` ## 许可