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

6.3 KiB

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, receiveParams, data)──► Server
Client ◄──OnReceiveMessage(tag, requestId, receiveParams, data)── Server

Tag (int) determines server method. All calls go through OnReceiveMessage. Metadata (SignalReceiveParams) and payload (byte[]) travel as separate hub arguments — the byte[] uses the protocol's zero-copy fast-path, metadata is AcBinary serialized normally.

Client:                               Server:
AcSignalRClientBase                   AcWebSignalRHubBase<TTags, TLogger>
  ├─ HubConnection (WebSocket)          ├─ Hub<IAcSignalRHubItemServer>
  ├─ AcBinaryHubProtocol                ├─ 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

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

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

Metadata + Payload Separation

SignalReceiveParams (separate hub argument, AcBinary serialized):

Field Type Purpose
Status SignalResponseStatus Success/Error
DataSerializerType AcSerializerType Binary or JsonGZip — tells client how to deserialize response data

byte[] data (separate hub argument, protocol fast-path, zero-copy).

SignalResponseDataMessage remains as internal DTO for callback routing — constructed in-memory from receiveParams + data, never serialized as envelope on wire. GetResponseData<T>() dispatches on DataSerializerType: Binary → BinaryTo<T>(), JsonGZip → decompress → JsonTo<T>().

Request/Response Flow

Client → Server

1. PostAsync<T>(tag, param) / PostAsync<T>(tag, params[]) / PostDataAsync(tag, data, callback)
2. SerializeParametersToBinary(object[] parameters):
   [VarUInt count] [for each: INT32 length + AcBinary bytes]
   Each parameter individually binary-serialized with length prefix.
3. SignalReceiveParams { Status = Success }
4. HubConnection.SendAsync("OnReceiveMessage", tag, requestId, receiveParams, bytes)
5. AcBinaryHubProtocol frames on wire (byte[] via fast-path, receiveParams via AcBinary)

Server → Client

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

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

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.

Parameter Serialization

Client→server parameters use length-prefixed per-parameter binary format via SignalRSerializationHelper:

SerializeParametersToBinary(object[]):
  [VarUInt count] [for each: INT32 length + AcBinary bytes]

DeserializeParametersFromBinary(byte[], ParameterInfo[]):
  Server reads each segment and deserializes with known target type from method signature.
  Trailing parameters with defaults are auto-filled.

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

Source Files

Component Path
Client base SignalRs/AcSignalRClientBase.cs
Binary protocol SignalRs/AcBinaryHubProtocol.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 interface SignalRs/ISignalParams.cs
Serialization SignalRs/SignalRSerializationHelper.cs