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

5.6 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);
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

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.

Binary (default): AcBinarySerializer.ToBinary(data). JSON fallback: ToJsonGzipHelper.Compress.

Request/Response Flow

Client → Server

1. PostAsync<T>(tag, postData) / PostDataAsync(tag, data, callback)
2. CreatePostMessage(postData):
   ├─ Primitives/strings/enums/value types → IdMessage
   └─ Complex → SignalPostJsonDataMessage<T>   ⚠️ JSON-in-Binary tech debt
3. SerializeToBinary(message)
4. SignalReceiveParams { Status = Success }
5. HubConnection.SendAsync("OnReceiveMessage", tag, requestId, receiveParams, bytes)
6. 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 = receiveParams.Status, DataSerializerType = Binary, ResponseData = data }
├─ Matching requestId in pending dict:
│  ├─ Route: null→sync wait, Action→invoke, Func<Task>→await
│  └─ GetResponseData<T>(): Binary→BinaryTo<T>(), JSON→Decompress→Deserialize
└─ 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.

Tech Debt

JSON-in-Binary: client→server wraps params in JSON inside binary envelope (SignalPostJsonDataMessage). Do NOT fix as side effect — requires coordinated cross-project changes.

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