# SignalR Binary Protocol `AcBinaryHubProtocol` — custom `IHubProtocol` (name: `"acbinary"`) replacing SignalR JSON+Base64 with `AcBinarySerializer`. > Architecture (tag system, dispatch, request/response): `SIGNALR.md` > Output writers (cached chunk, buffer states, chunk sizing): `AyCode.Core/docs/BINARY_WRITERS.md` ## Wire Format ``` [INT32 LE payload length] [1 byte message type] [message-specific fields] ``` | Type | Byte | Fields | |------|------|--------| | Invocation | 1 | nullable invocationId, target, arguments, streamIds, headers | | StreamItem | 2 | invocationId, argument, headers | | Completion | 3 | invocationId, nullable error, hasResult, optional result, headers | | StreamInvocation | 4 | invocationId, target, arguments, streamIds, headers | | CancelInvocation | 5 | invocationId, headers | | Ping | 6 | (empty) | | Close | 7 | nullable error, allowReconnect | | Ack | 8 | sequenceId (INT64) | | Sequence | 9 | sequenceId (INT64) | **Argument framing:** `[INT32 LE arg length] [serialized bytes]` — deferred deserialization (target parsed first → `IInvocationBinder` resolves types). **Strings:** `[VarUInt byte length] [UTF-8]`. Nullable: `0`=null, `1`=value follows. **Headers:** `[VarUInt count] [key-value pairs...]`. Count=0 → no headers. ## Zero-Copy Write Pipeline Writes directly to SignalR pipe's `IBufferWriter`, no intermediate `byte[]`. ``` WriteMessage(HubMessage, IBufferWriter output) ├─ Reserve 4-byte outer length prefix ├─ BWO = BufferWriterBinaryOutput(output) [standalone mode] │ ├─ WriteByte(messageType) │ ├─ WriteStringUtf8(invocationId, target) │ ├─ WriteVarUInt(argCount) │ ├─ Per argument: │ │ ├─ byte[] → write through BWO (size known, no patching) │ │ └─ object → FlushAndReset() → reserve INT32 arg prefix │ │ → AcBinarySerializer.Serialize(value, output) → patch prefix │ ├─ WriteStringArray(streamIds) │ └─ WriteHeaders(headers) ├─ Patch outer length = BWO.Position + externalBytes └─ BWO.Flush() ``` ### Dual BWO Pattern Protocol and serializer each create own `BufferWriterBinaryOutput` on the same `IBufferWriter`. Sequential, never concurrent: 1. **Protocol BWO** (standalone): framing — message type, strings, headers. Cached chunk, zero dispatch. 2. `FlushAndReset()`: commits bytes to pipe, invalidates chunk. 3. **Serializer BWO** (context mode): `Serialize()` creates internal BWO, acquires fresh chunk, writes, flushes. 4. Protocol BWO re-acquires chunk on next write (via `Grow`). **Cost:** one extra `GetMemory` per argument (nanoseconds). **Benefit:** zero-copy end-to-end, no intermediate `byte[]`, no wrapper class. Why two BWOs: serializer writes must live on `BinarySerializationContext` (sealed class) for JIT optimization — context owns its own BWO. See `AyCode.Core/docs/BINARY_WRITERS.md` § "Why Writes Are on the Context". ### Length Prefix Patching ```csharp var lengthSpan = output.GetSpan(4); output.Advance(4); // ... write payload ... Unsafe.WriteUnaligned(ref lengthSpan[0], payloadLength); ``` Safe for `PipeWriter` — segments writable until `FlushAsync`. **`GetMessageBytes` caveat:** `ArrayBufferWriter` initial capacity must include `LengthPrefixSize` to prevent resize after prefix reservation (stale span). ## byte[] Fast-Path When argument is `byte[]`, bypasses serializer: 1. Size upfront: `1 (BinaryTypeCode) + VarUIntSize(length) + length` 2. INT32 prefix written with actual value (no patching) 3. `BinaryTypeCode.ByteArray(68)` + VarUInt length + raw bytes via BWO Read side mirrors: if `targetType == typeof(byte[])` and first byte is `ByteArray`, deserializer bypassed → direct `SpanReader`. ## Read Path `SpanReader` — `ref struct` for sequential `ReadOnlySpan` reading: 1. Read INT32 length. If `input.Length < total` → false (incomplete). 2. Multi-segment `ReadOnlySequence` → rent contiguous buffer from `ArrayPool`. 3. Parse message type → type-specific parser. 4. Fields via `SpanReader` methods (`ReadByte`, `ReadString`, `ReadVarUInt`, `ReadInt32`, `ReadInt64`, `ReadSpan`). 5. Arguments: INT32 length → slice → `AcBinaryDeserializer.Deserialize(span, targetType)`. ## Config | Property | Default | Purpose | |----------|---------|---------| | `Options` | `AcBinarySerializerOptions.Default` | Serializer options (volatile, runtime-replaceable) | | `BufferWriterChunkSize` | 65536 | Chunk size for both BWOs | **Source:** `AyCode.Services/SignalRs/AcBinaryHubProtocol.cs`