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.mdDataSource 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: ToJson → GzipHelper.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 |