AyCode.Core/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL.md

4.6 KiB

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<byte>, no intermediate byte[].

WriteMessage(HubMessage, IBufferWriter<byte> 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

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 first byte is ByteArray(0x44), deserializer bypassed → direct SpanReader. Detection is wire-format only (no targetType check) — ByteArray marker is unambiguous since no AcBinary object starts with 0x44 (version=1).

Read Path

SpanReaderref struct for sequential ReadOnlySpan<byte> 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