# 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 ├─ HubConnection (WebSocket) ├─ Hub ├─ AcBinaryHubProtocol ├─ DynamicMethodRegistry ├─ Pending request tracking ├─ Parameter deserialization └─ Response callbacks └─ Broadcast to other clients ``` ## Tag System Base tags in AyCode.Core: ```csharp 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): ```csharp 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:** ```csharp var orders = await client.PostAsync>(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: `ToJson` → `GzipHelper.Compress`. ## Request/Response Flow ### Client → Server ``` 1. PostAsync(tag, postData) / PostDataAsync(tag, data, callback) 2. CreatePostMessage(postData): ├─ Primitives/strings/enums/value types → IdMessage └─ Complex → SignalPostJsonDataMessage ⚠️ 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→await │ └─ GetResponseData(): Binary→BinaryTo(), 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(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` |