5.2 KiB
SignalR Binary Protocol
AcBinaryHubProtocol (unsealed base) — custom IHubProtocol (name: "acbinary") replacing SignalR JSON+Base64 with AcBinarySerializer. AyCodeBinaryHubProtocol (derived) adds ArrayPool-backed SignalData creation via CreateByteArrayResult hook.
Architecture (tag system, dispatch, request/response):
SIGNALR.mdOutput 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:
- Protocol BWO (standalone): framing — message type, strings, headers. Cached chunk, zero dispatch.
FlushAndReset(): commits bytes to pipe, invalidates chunk.- Serializer BWO (context mode):
Serialize()creates internal BWO, acquires fresh chunk, writes, flushes. - 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:
- Size upfront:
1 (BinaryTypeCode) + VarUIntSize(length) + length - INT32 prefix written with actual value (no patching)
BinaryTypeCode.ByteArray(68)+ VarUInt length + raw bytes via BWO
Read side mirrors: if first byte is ByteArray(0x44), deserializer bypassed → direct SpanReader → CreateByteArrayResult(span, targetType). Base returns data.ToArray(). AyCodeBinaryHubProtocol overrides: if targetType == typeof(SignalData), rents from ArrayPool and returns SignalData(rented, length, isRented: true). Detection is wire-format only (no targetType check for the marker) — ByteArray marker is unambiguous since no AcBinary object starts with 0x44 (version=1).
Write side: WriteArgument handles both byte[] and SignalData via the same ByteArray wire format. SignalData.Span is written directly — same marker + VarUInt length + raw bytes.
Read Path
SpanReader — ref struct for sequential ReadOnlySpan<byte> reading:
- Read INT32 length. If
input.Length < total→ false (incomplete). - Multi-segment
ReadOnlySequence→ rent contiguous buffer fromArrayPool. - Parse message type → type-specific parser.
- Fields via
SpanReadermethods (ReadByte,ReadString,ReadVarUInt,ReadInt32,ReadInt64,ReadSpan). - 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 (base), AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs (derived, ArrayPool)