AyCode.Core/AyCode.Services/docs/SIGNALR.md

8.7 KiB
Raw Blame History

SignalR Client

Client-side SignalR transport: custom binary protocol, tag-based dispatch. Source: SignalRs/

Server-side hub, session, broadcast: AyCode.Services.Server/docs/SIGNALR_SERVER.md DataSource collection: AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md

Design

Single hub method, tag-based dispatch:

Client ──OnReceiveMessage(tag, requestId, signalParams, data)──► Server
Client ◄──OnReceiveMessage(tag, requestId, signalParams, data)── Server

Tag (int) determines server method. All calls go through OnReceiveMessage. Metadata (SignalParams) and payload (SignalData) travel as separate hub argumentsSignalData wraps pooled byte[] from ArrayPool via AyCodeBinaryHubProtocol (zero-copy fast-path), metadata is AcBinary serialized normally.

Client:                               Server:
AcSignalRClientBase                   AcWebSignalRHubBase<TTags, TLogger>
  ├─ HubConnection (WebSocket)          ├─ Hub<IAcSignalRHubItemServer>
  ├─ AyCodeBinaryHubProtocol             ├─ DynamicMethodRegistry
  ├─ Pending request tracking           ├─ Parameter deserialization
  └─ Response callbacks                 └─ Broadcast to other clients

Tag System

Base tags in AyCode.Core:

public class AcSignalRTags
{
    public const int None = 0;
    public const int PingTag = 90001;
    public const int EchoTag = 90002;
}

Consuming projects inherit and define own tags (plain int constants, must be unique per hub):

public abstract class MyProjectTags : AcSignalRTags
{
    public const int GetOrders = 100;
    public const int GetOrderById = 101;
}

Attributes:

  • [Tag(42)] — base: maps int tag → method
  • [SignalR(messageTag, sendToOtherClientTag, sendToOtherClientType)] — server routing + broadcast
  • [SignalRSendToClient(100)] — client receive dispatch

SendToClientType: None | Others | Caller | All

Usage:

var orders = await client.PostAsync<List<Order>>(MyTags.GetOrders, companyId);        // single param
var result = await client.PostAsync<T>(MyTags.Query, [companyId, filter]);            // object[] params
await client.PostDataAsync(MyTags.SaveOrder, order);
await client.PostDataAsync(MyTags.SaveOrder, order, async response => { ... });       // async callback

CRUD helpers (PostAsync, GetByIdAsync, GetAllAsync, PostDataAsync) are generic transport, not tied to DataSource.

Wire Protocol

AcBinaryHubProtocol / AyCodeBinaryHubProtocol

Custom IHubProtocol ("acbinary"), replaces JSON. Zero-copy via BufferWriterBinaryOutput standalone mode. byte[] and SignalData args bypass serializer.

AcBinaryHubProtocol is the base (unsealed, generic). AyCodeBinaryHubProtocol derives from it and uses ArrayPool for SignalData arguments — the CreateByteArrayResult hook rents from pool instead of .ToArray(). Register AyCodeBinaryHubProtocol in both client and server.

Wire format, argument framing, dual BWO pattern, length prefix patching: SIGNALR_BINARY_PROTOCOL.md

SignalParams + Payload Separation

SignalParams (separate hub argument, [AcBinarySerializable]):

Field Type Purpose
Status SignalResponseStatus Success/Error
DataSerializerType AcSerializerType Binary or JsonGZip — tells client how to deserialize response data
Parameters byte[]? Serialized byte[][] as single byte[] (protocol fast-path). Null when no parameters.

Typed access via methods (PostDataJson pattern):

  • Client: SetParameterValues(object[]) — packs each param via ToBinary()byte[][]byte[]
  • Server: GetParameterValues(ParameterInfo[]) — unpacks byte[]byte[][] → per-element BinaryTo(targetType)
  • Protocol never sees byte[][] — only byte[].

SignalData data (separate hub argument, protocol fast-path, ArrayPool-backed via AyCodeBinaryHubProtocol).

SignalData wraps pooled byte[] with IDisposable lifecycle. Consumer accesses via Span (zero-copy) or ToArray() (copy, rare). Dispose() returns rented buffer to ArrayPool with clearArray: true.

Parameters and data are independent — both can be null or filled in any direction (SignalR is bidirectional).

Combination Parameters data Example
Request byte[] (packed params) null/empty client calls server method
Response null SignalData (response payload) server returns result
Request + data byte[] SignalData client responds to server with data
Signal null null/empty ping, status change, broadcast trigger

SignalResponseDataMessage remains as internal DTO for callback routing — constructed in-memory from signalParams + data, never serialized as envelope on wire. ResponseData is SignalData?. GetResponseData<T>() dispatches on DataSerializerType: Binary → AcBinaryDeserializer.Deserialize<T>(Span), JsonGZip → decompress → JsonTo<T>(). Dispose() returns both SignalData and JSON decompression buffers to ArrayPool.

Request/Response Flow

Client → Server

1. PostAsync<T>(tag, param) / PostAsync<T>(tag, params[]) / PostDataAsync(tag, data, callback)
2. signalParams.SetParameterValues(object[]):
   Each param ToBinary() → byte[][] → ToBinary() → byte[] (single wire blob)
3. SignalParams { Status = Success, Parameters = byte[] }
4. HubConnection.SendAsync("OnReceiveMessage", tag, requestId, signalParams, null)
5. AyCodeBinaryHubProtocol frames on wire (signalParams via AcBinary, data = null for requests)

Server → Client

OnReceiveMessage(tag, requestId, signalParams, data)
├─ Construct SignalResponseDataMessage in-memory (no envelope deser):
│  └─ { Status, DataSerializerType, ResponseData (SignalData) } from signalParams + data
├─ Matching requestId in pending dict:
│  ├─ Route: null→sync wait, Action→invoke, Func<Task>→await
│  └─ GetResponseData<T>(): dispatches on DataSerializerType
│     Binary→Deserialize<T>(Span), JsonGZip→Decompress→JsonTo<T>()
└─ No match (broadcast):
   └─ abstract MessageReceived(tag, signalParams, SignalData data).Forget()

Request pooling: SignalRRequestModel via SignalRRequestModelPool (ObjectPool + IResettable).

Parameter Deserialization (Server)

Server calls signalParams.GetParameterValues(paramInfos):

GetParameterValues(ParameterInfo[]):
  1. byte[] → BinaryTo<byte[][]>() (cached in _parameterValues)
  2. For each: byte[][i].BinaryTo(paramInfos[i].ParameterType)
  3. Trailing optional params filled with defaults
  Hub validates: missing required params throw ArgumentException.

Type-guided deserialization — each parameter is individually serialized/deserialized with its concrete type, avoiding the object[] → dictionary problem of untyped binary deserialization.

Perf concern: Per-parameter ToBinary()/BinaryTo(Type) = N× context pool acquire/release + N× type-dispatch (ThreadLocal + ConcurrentDictionary cache). For many small primitives (int, bool, string) the per-call overhead may exceed a single bulk serialization. Complex objects benefit clearly. If benchmarks show regression vs old JSON path, a batch fast-path (single serialization context for all params) should be added.

Limitation: Parameter serialization/deserialization is currently AcBinary only (ToBinary()/BinaryTo()). JSON support would require dispatching on serializer type in SignalParams methods + AcJsonSerializer reference.

Response Patterns

Pattern Method Blocking
Sync wait PostAsync<T>(tag, data) Yes (60s timeout)
Async callback PostDataAsync(tag, data, async msg => {...}) No
Fire-and-forget PostDataAsync(tag, data, msg => {...}) No

Connection

  • WebSockets only (SkipNegotiation = true)
  • 30MB transport + 30MB application buffer
  • WithAutomaticReconnect() + WithStatefulReconnect()
  • Keepalive 60s, server timeout 180s

Diagnostics

AcSignalRClientBase.EnableBinaryDiagnostics = true — hex dump, header parsing, property enumeration.

Source Files

Component Path
Client base SignalRs/AcSignalRClientBase.cs
Binary protocol (base) SignalRs/AcBinaryHubProtocol.cs
Binary protocol (derived) SignalRs/AyCodeBinaryHubProtocol.cs
Signal data wrapper SignalRs/SignalData.cs
Tag attributes SignalRs/SignalMessageTagAttribute.cs
Base tags SignalRs/AcSignalRTags.cs
CRUD tags SignalRs/SignalRCrudTags.cs
SendToClientType SignalRs/SendToClientType.cs
Message types SignalRs/IAcSignalRHubClient.cs
Params class SignalRs/ISignalParams.cs
Serialization SignalRs/SignalRSerializationHelper.cs