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

7.8 KiB

SignalR Client

Client-side SignalR transport with custom binary protocol and tag-based dispatch. Source: SignalRs/ in this project.

For server-side hub, session, broadcast see AyCode.Services.Server/docs/SIGNALR_SERVER.md. For the DataSource collection see AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md.

Design Overview

All communication flows through a single hub method with tag-based dispatch:

Client ──OnReceiveMessage(tag, bytes, requestId)──► Server
Client ◄──OnReceiveMessage(tag, bytes, requestId)── Server

OnReceiveMessage is the only transport channel — all calls go through it. The tag (int) determines which server method to invoke. Projects call any endpoint by specifying the tag; the server dispatches accordingly.

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

Tag System

Tag Definition

AyCode.Core defines only the base class and built-in infrastructure tags:

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

Consuming projects define their own tags by inheriting AcSignalRTags:

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

Tags are plain int constants. The project decides the numbering. Tags must be unique across all registered services within a hub.

Tag Attributes

Three attribute levels:

// Base: maps an integer tag to a method
[Tag(42)]

// Server method: tag + optional broadcast behavior
[SignalR(messageTag: 100, sendToOtherClientTag: 100, sendToOtherClientType: SendToClientType.Others)]

// Client receive: marks method for server→client dispatch
[SignalRSendToClient(100)]

SendToClientType: None (no broadcast), Others (all except caller), Caller (response only), All (everyone).

How Projects Use Tags

Projects call the SignalR transport directly — not only through DataSource:

var orders = await signalRClient.PostAsync<List<Order>>(MyTags.GetOrders, companyId);
var order = await signalRClient.GetByIdAsync<Order>(MyTags.GetOrderById, orderId);
await signalRClient.PostDataAsync(MyTags.SaveOrder, order);

// Async callback (non-blocking)
await signalRClient.PostDataAsync(MyTags.SaveOrder, order, async response => { ... });

// Fire-and-forget
await signalRClient.PostDataAsync(MyTags.SaveOrder, order, response => { ... });

The CRUD helpers (PostAsync, GetByIdAsync, GetAllAsync, PostDataAsync) are all generic transport methods that work with any tag — they are not tied to DataSource.

Wire Protocol

AcBinaryHubProtocol

Custom IHubProtocol (name: "acbinary"), replaces default JSON. Frame format:

[4 bytes: payload length, little-endian] [1 byte: message type] [payload...]

Message types: Invocation(1), StreamItem(2), Completion(3), Ping(6), Close(7), Ack(8), Sequence(9).

Arguments serialized individually with VarUInt length prefix. Direct write to IBufferWriter via BufferWriterBinaryOutput.

Response Message

SignalResponseDataMessage carries:

Field Type Purpose
MessageTag int Tag identifying the operation
Status SignalResponseStatus Success/Error
ResponseData byte[] Binary-serialized payload (or GZip+JSON fallback)
DataSerializerType AcSerializerType Binary or Json

Binary mode (default): AcBinarySerializer.ToBinary(data) → raw bytes. JSON fallback: ToJson(data)GzipHelper.Compress(json) → bytes.

Request/Response Flow

Client → Server

1. PostAsync<T>(tag, postData) or PostDataAsync(tag, data, callback)
2. CreatePostMessage(postData):
   ├─ Primitives/strings/enums/value types → IdMessage (JSON array of serialized values)
   └─ Complex objects → SignalPostJsonDataMessage<T>     ⚠️ tech debt: JSON-in-Binary
3. SerializeToBinary(message)                            wraps in Binary envelope
4. HubConnection.SendAsync("OnReceiveMessage", tag, bytes, requestId)
5. AcBinaryHubProtocol frames it on the wire

Server → Client

13. Client.OnReceiveMessage(tag, bytes, requestId)
14. Has matching requestId in pending ConcurrentDictionary?
    ├─ YES (response to own request):
    │   15. DeserializeFromBinary<SignalResponseDataMessage>(bytes)
    │   16. Route based on pending request type:
    │       ├─ null (sync wait) → set ResponseByRequestId, WaitToAsync completes
    │       ├─ Action<SignalResponseDataMessage> → invoke directly
    │       └─ Func<SignalResponseDataMessage, Task> → invoke and await
    │   17. GetResponseData<T>():
    │       ├─ Binary: ResponseData.BinaryTo<T>()
    │       └─ JSON: GzipDecompress → AcJsonDeserializer.Deserialize<T>()
    │
    └─ NO (broadcast from another client's action):
        18. abstract MessageReceived(tag, bytes).Forget()
            └─ Consuming project overrides to handle server push

Broadcast receive: When the server broadcasts to Others/All, receiving clients have no matching requestId. The message falls through to MessageReceived(int messageTag, byte[] messageBytes) — an abstract method the consuming project overrides.

Request model pooling: SignalRRequestModel instances are managed via SignalRRequestModelPool (ObjectPool + IResettable) to avoid allocations per request.

Response Patterns

Pattern Method Blocking
Sync wait PostAsync<T>(tag, data) Yes, polls up to TransportSendTimeout (60s)
Async callback PostDataAsync(tag, data, async msg => {...}) No, Func<SignalResponseDataMessage, Task>
Fire-and-forget PostDataAsync(tag, data, msg => {...}) No, Action<SignalResponseDataMessage>

Connection Lifecycle

Client configuration:

  • Transport: WebSockets only (SkipNegotiation = true)
  • Buffer: 30MB transport + 30MB application
  • Reconnection: WithAutomaticReconnect() + WithStatefulReconnect()
  • Keepalive: 60s interval, 180s server timeout

Diagnostics

Enable with AcSignalRClientBase.EnableBinaryDiagnostics = true.

Logs: hex dump (500 byte sample), header parsing (version, marker), property count + names via VarUInt reading.

Known Technical Debt

JSON-in-Binary request parameters: Client→server requests currently wrap parameters in JSON inside the binary envelope (SignalPostJsonDataMessage). This adds an unnecessary serialization round-trip. Responses are already pure binary. Do NOT attempt to fix as a side effect — requires coordinated changes across all consuming projects.

Key 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
Serialization SignalRs/SignalRSerializationHelper.cs