From 83350e43f694bbb64280366f167c8454cf487aa0 Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 10 Apr 2026 16:10:28 +0200 Subject: [PATCH] Refactor: clarify and implement protocol serialization modes Refactored binary protocol to support three explicit serialization/transport strategies via BinaryProtocolMode: Bytes (byte[]), Segment (zerocopy PipeWriter), and AsyncSegment (async PipeWriter with pipeline parallelism). Updated AcBinaryHubProtocol and AyCodeBinaryHubProtocol to select serialization/deserialization paths based on mode. Improved documentation and XML comments to describe each mode's behavior and performance. DI registration now explicitly selects AsyncSegment mode for AyCodeBinaryHubProtocol. Default remains Bytes mode. These changes clarify protocol mechanics and enable better performance tuning. --- AyCode.Core/docs/BINARY_WRITERS.md | 2 +- .../SignalRs/AcBinaryHubProtocol.cs | 20 +++++++++-- .../SignalRs/AcSignalRClientBase.cs | 2 +- .../SignalRs/AyCodeBinaryHubProtocol.cs | 7 ++++ .../SignalRs/BinaryProtocolMode.cs | 36 ++++++++++++++++--- AyCode.Services/docs/SIGNALR.md | 2 +- .../docs/SIGNALR_BINARY_PROTOCOL.md | 14 ++++---- 7 files changed, 67 insertions(+), 16 deletions(-) diff --git a/AyCode.Core/docs/BINARY_WRITERS.md b/AyCode.Core/docs/BINARY_WRITERS.md index 746f0ec..5264672 100644 --- a/AyCode.Core/docs/BINARY_WRITERS.md +++ b/AyCode.Core/docs/BINARY_WRITERS.md @@ -120,7 +120,7 @@ Same cached chunk pattern (`GetMemory` → `TryGetArray` → direct array writes ### Usage -Selected via `BinaryProtocolMode.Segment` in `AcBinaryHubProtocol`. The protocol casts `IBufferWriter output` to `PipeWriter` (safe — SignalR always provides `PipeWriter`). +Selected via `BinaryProtocolMode.AsyncSegment` in `AcBinaryHubProtocol`. The protocol casts `IBufferWriter output` to `PipeWriter` (safe — SignalR always provides `PipeWriter`). ```csharp AcBinarySerializer.Serialize(value, (PipeWriter)output, options) // AsyncPipeWriterOutput path diff --git a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs index cd8b8a1..fee8746 100644 --- a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs +++ b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs @@ -412,14 +412,23 @@ public class AcBinaryHubProtocol : IHubProtocol return; } - // Flush BWO to pipe, then serialize directly to the pipe via AcBinarySerializer + // Bytes mode: serialize to byte[], write through BWO (no FlushAndReset needed) + if (_protocolMode == BinaryProtocolMode.Bytes) + { + var serialized = AcBinarySerializer.Serialize(value, _options); + bw.WriteRaw(serialized.Length); + bw.WriteBytes(serialized); + return; + } + + // Segment / AsyncSegment: serialize directly to the pipe bw.FlushAndReset(); // Reserve arg length prefix directly on the pipe var argLenSpan = output.GetSpan(LengthPrefixSize); output.Advance(LengthPrefixSize); - var argBytes = _protocolMode == BinaryProtocolMode.Segment + var argBytes = _protocolMode == BinaryProtocolMode.AsyncSegment ? AcBinarySerializer.Serialize(value, (System.IO.Pipelines.PipeWriter)output, _options) : AcBinarySerializer.Serialize(value, output, _options); @@ -482,6 +491,13 @@ public class AcBinaryHubProtocol : IHubProtocol return SequenceToByteArray(argSlice.Slice(1)); } + // Bytes mode: linearize to byte[] → ArrayBinaryInput (fastest deser, no segment overhead) + if (_protocolMode == BinaryProtocolMode.Bytes) + { + var bytes = SequenceToByteArray(argSlice); + return AcBinaryDeserializer.Deserialize(bytes, targetType, _options); + } + return DeserializeFromSequence(argSlice, targetType, _options); } diff --git a/AyCode.Services/SignalRs/AcSignalRClientBase.cs b/AyCode.Services/SignalRs/AcSignalRClientBase.cs index f963f3b..24fabbc 100644 --- a/AyCode.Services/SignalRs/AcSignalRClientBase.cs +++ b/AyCode.Services/SignalRs/AcSignalRClientBase.cs @@ -75,7 +75,7 @@ namespace AyCode.Services.SignalRs var binaryOptions = AcBinarySerializerOptions.Default; binaryOptions.BufferWriterChunkSize = 4096; - return new AyCodeBinaryHubProtocol(binaryOptions); + return new AyCodeBinaryHubProtocol(binaryOptions, BinaryProtocolMode.AsyncSegment); }); //Vagy ha az options-t is DI-ből: diff --git a/AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs b/AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs index f8312bf..41406bb 100644 --- a/AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs +++ b/AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs @@ -60,6 +60,13 @@ public class AyCodeBinaryHubProtocol : AcBinaryHubProtocol targetType = dataType; } + // Bytes mode: linearize to byte[] → ArrayBinaryInput (fastest deser, no segment overhead) + if (_protocolMode == BinaryProtocolMode.Bytes) + { + var bytes = SequenceToByteArray(argSlice); + return AcBinaryDeserializer.Deserialize(bytes, targetType, Options); + } + return DeserializeFromSequence(argSlice, targetType, Options); } } diff --git a/AyCode.Services/SignalRs/BinaryProtocolMode.cs b/AyCode.Services/SignalRs/BinaryProtocolMode.cs index a11e80e..9e30520 100644 --- a/AyCode.Services/SignalRs/BinaryProtocolMode.cs +++ b/AyCode.Services/SignalRs/BinaryProtocolMode.cs @@ -1,16 +1,44 @@ namespace AyCode.Services.SignalRs; /// -/// Controls how the binary protocol transports serialized data over the network. +/// Controls how the binary protocol serializes and transports data over the network. +/// +/// Bytes: Serialize via ArrayBinaryOutput → single contiguous byte[], +/// written to the pipe as a raw blob. Deserialize via SequenceReader.ToArray() → +/// ArrayBinaryInput (single buffer, TryAdvanceSegment always false → JIT-eliminated). +/// Fastest individual ser/deser, no zerocopy, no pipeline overlap. +/// +/// +/// Segment: Serialize via BufferWriterBinaryOutput directly to the PipeWriter, +/// chunk-by-chunk with a single Flush at the end. Deserialize via SequenceBinaryInput +/// from multi-segment ReadOnlySequence<byte> (lazy TryGet iteration, cross-boundary scratch). +/// Zerocopy write, but no pipeline overlap. +/// +/// +/// AsyncSegment: Serialize via AsyncPipeWriterOutput directly to the PipeWriter, +/// per-chunk FlushAsync sends data to the network during serialization. Deserialize via +/// PipeReaderBinaryInput with on-demand ReadAsync (processes chunks as they arrive). +/// Zerocopy write + pipeline parallelism (ser/network/deser overlap), highest roundtrip potential +/// for large payloads. +/// /// public enum BinaryProtocolMode { - /// Standard: serialize → egyben küld/fogad. + /// + /// ArrayBinaryOutput → byte[] → pipe. Deser: ToArray() → ArrayBinaryInput. + /// Fastest ser/deser, no zerocopy, no pipeline overlap. + /// Bytes = 0, - /// Szinkron segment streaming: flush Grow()-ban → chunk-onként hálózatra. + /// + /// BufferWriterBinaryOutput → PipeWriter, single Flush at end. Deser: SequenceBinaryInput (multi-segment). + /// Zerocopy write, no pipeline overlap. + /// Segment = 1, - /// Async segment streaming: async serializer + async output (jövő). + /// + /// AsyncPipeWriterOutput → PipeWriter, per-chunk FlushAsync. Deser: PipeReaderBinaryInput (on-demand ReadAsync). + /// Zerocopy write + pipeline parallelism (ser/network/deser overlap). + /// AsyncSegment = 2, } diff --git a/AyCode.Services/docs/SIGNALR.md b/AyCode.Services/docs/SIGNALR.md index 8c7aea5..4ae8f79 100644 --- a/AyCode.Services/docs/SIGNALR.md +++ b/AyCode.Services/docs/SIGNALR.md @@ -70,7 +70,7 @@ CRUD helpers (`PostAsync`, `GetByIdAsync`, `GetAllAsync`, `PostDataAsync`) are g ### AcBinaryHubProtocol / AyCodeBinaryHubProtocol -Custom `IHubProtocol` (`"acbinary"`), replaces JSON. Zero-copy write via `BufferWriterBinaryOutput` standalone mode + `AcBinarySerializer.Serialize(value, output)` directly to pipe. Zero-copy read via `SequenceReader` from pipe's `ReadOnlySequence`. `BinaryProtocolMode` constructor parameter selects transport strategy: `Bytes` (default, single flush), `Segment` (per-chunk flush via `AsyncPipeWriterOutput`), `AsyncSegment` (reserved). +Custom `IHubProtocol` (`"acbinary"`), replaces JSON. Zero-copy write via `BufferWriterBinaryOutput` standalone mode + `AcBinarySerializer.Serialize(value, output)` directly to pipe. Zero-copy read via `SequenceReader` from pipe's `ReadOnlySequence`. `BinaryProtocolMode` constructor parameter selects transport strategy: `Bytes` (default, ArrayBinaryOutput → byte[]), `Segment` (BWO zerocopy to PipeWriter, single flush), `AsyncSegment` (AsyncPipeWriterOutput, per-chunk FlushAsync + pipeline parallelism). `AcBinaryHubProtocol` is the base (unsealed) — general binary framing only. `AyCodeBinaryHubProtocol` derives from it with consumer-specific logic: `SignalParams` capture (via `OnArgumentRead` hook), `IsRawBytesData` path, `SignalDataType` type resolution. Register `AyCodeBinaryHubProtocol` in both client and server. diff --git a/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md b/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md index 803a8e4..61581c5 100644 --- a/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md +++ b/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md @@ -160,14 +160,14 @@ Typical overhead for 225KB payload with 4096-byte segments: ~224.5KB zero-copy, ## BinaryProtocolMode -`enum BinaryProtocolMode` — constructor parameter for `AcBinaryHubProtocol`, selects transport strategy: +`enum BinaryProtocolMode` — constructor parameter for `AcBinaryHubProtocol`, selects serialization + transport strategy: -| Value | Behavior | -|-------|----------| -| `Bytes` (default) | Standard: serialize to `BufferWriterBinaryOutput`, single flush at end. | -| `Segment` | Segment streaming: serialize to `AsyncPipeWriterOutput`, flush per 4096-byte chunk via `PipeWriter.FlushAsync().Forget()`. Network transfer overlaps with serialization. | -| `AsyncSegment` | Reserved for future async serializer. | +| Value | Serialize | Deserialize | Characteristics | +|-------|-----------|-------------|-----------------| +| `Bytes` (default) | `ArrayBinaryOutput` → `byte[]` → write to pipe as raw blob | `SequenceReader.ToArray()` → `ArrayBinaryInput` (single contiguous buffer, `TryAdvanceSegment` → false, JIT-eliminated) | Fastest individual ser/deser. No zerocopy. No pipeline overlap. | +| `Segment` | `BufferWriterBinaryOutput` → directly to `PipeWriter`, chunk-by-chunk, single `Flush` at end | `SequenceBinaryInput` → multi-segment `ReadOnlySequence` (lazy `TryGet` iteration, cross-boundary scratch) | Zerocopy write. No pipeline overlap. | +| `AsyncSegment` | `AsyncPipeWriterOutput` → directly to `PipeWriter`, per-chunk `FlushAsync().Forget()` with backpressure | `PipeReaderBinaryInput` → on-demand `ReadAsync`, processes chunks as they arrive from the network | Zerocopy write + pipeline parallelism (ser/network/deser overlap). Highest roundtrip potential for large payloads. | -In `Segment` mode, `WriteArgument` casts `IBufferWriter output` to `PipeWriter` and calls `AcBinarySerializer.Serialize(value, pipeWriter, options)` which uses `AsyncPipeWriterOutput` internally. The reader side currently uses the same `SequenceBinaryInput` path (SignalR delivers complete messages via `TryParseMessage`). `PipeReaderBinaryInput` is available for future direct-pipe deserialization. +In `AsyncSegment` mode, `WriteArgument` casts `IBufferWriter output` to `PipeWriter` and calls `AcBinarySerializer.Serialize(value, pipeWriter, options)` which uses `AsyncPipeWriterOutput` internally. In `Bytes` and `Segment` mode, the standard `AcBinarySerializer.Serialize(value, output, options)` path is used (BWO on `IBufferWriter`). **Source:** `AyCode.Services/SignalRs/AcBinaryHubProtocol.cs` (base), `AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs` (consumer logic), `AyCode.Services/SignalRs/BinaryProtocolMode.cs` (enum)