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

9.4 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 (object data) travel as separate hub argumentsSignalParams is AcBinary serialized normally, data is serialized directly to the pipe via AcBinarySerializer (zero-copy write) or passed through as byte[] via protocol fast-path.

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 write via BufferWriterBinaryOutput standalone mode + AcBinarySerializer.Serialize(value, output) directly to pipe. Zero-copy read via SequenceReader<byte> from pipe's ReadOnlySequence.

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.

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.
SignalDataType string? AssemblyQualifiedName of response object type. Server sets before sending. Protocol uses this for eager type-aware deserialization. Null for raw byte[] responses.
IsRawBytesData bool Client sets true when T == byte[] (e.g. DataSource populate/merge). Protocol returns raw byte[] without deserialization.

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[].

object data (4th hub argument) — protocol handles three cases on read:

  1. byte[] fast-path: first byte is BinaryTypeCode.ByteArray(0x44) → skip tag, rest is raw payload bytes. No VarUInt (argLength implies size). No deserializer.
  2. IsRawBytesData (AyCodeBinaryHubProtocol): SignalParams.IsRawBytesData == true → return entire argSlice as raw byte[]. No deserialization. Consumer handles deserialization.
  3. Typed deserialization (AyCodeBinaryHubProtocol): resolve type from SignalParams.SignalDataTypeAcBinaryDeserializer.Deserialize(sequence, type) → return typed object.

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 typed object or byte[] server returns result
Request + data byte[] typed object client responds to server with data
Signal null null/empty ping, status change, broadcast trigger

SignalResponseDataMessage is an internal DTO for client-side callback routing and stream wire format — constructed in-memory from signalParams + data, never serialized as envelope on wire. RawResponseData is object? (typed object or byte[]). GetResponseData<T>() performs direct cast.

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[], IsRawBytesData = (typeof(T) == typeof(byte[])) }
4. SendCoreAsync → 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, object data)
├─ Construct SignalResponseDataMessage in-memory:
│  └─ { MessageTag, Status, DataSerializerType, RawResponseData = data }
├─ Matching requestId in pending dict:
│  ├─ Route: null→sync wait, Action→invoke, Func<Task>→await
│  └─ GetResponseData<T>(): direct cast (T)RawResponseData
│     Protocol already deserialized to correct type via SignalDataType
└─ No match (broadcast):
   └─ abstract MessageReceived(tag, signalParams, object 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 call. 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
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