9.2 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/README.mdDataSource collection:AyCode.Services.Server/docs/SIGNALR/SIGNALR_DATASOURCE.md
Design
Single hub method, tag-based dispatch:
Client ──OnReceiveMessage(tag, requestId, signalParams, data)──► Server
Client ◄──OnReceiveMessage(tag, requestId, signalParams, data)── Server
Tag (int) determines server method. All calls go through OnReceiveMessage.
Metadata (SignalParams) and payload (object data) travel as separate hub arguments — SignalParams is AcBinary serialized normally, data is serialized directly to the pipe via AcBinarySerializer (zero-copy write) or passed through as byte[] via protocol fast-path.
Client: Server:
AcSignalRClientBase AcWebSignalRHubBase<TTags, TLogger>
├─ HubConnection (WebSocket) ├─ Hub<IAcSignalRHubItemServer>
├─ AyCodeBinaryHubProtocol ├─ 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 / AyCodeBinaryHubProtocol
Custom IHubProtocol ("acbinary"), replaces JSON. Zero-copy write via BufferWriterBinaryOutput standalone mode + AcBinarySerializer.Serialize(value, output) directly to pipe. Zero-copy read via SequenceReader<byte> from pipe's ReadOnlySequence. BinaryProtocolMode constructor parameter selects transport strategy: Bytes (default, ArrayBinaryOutput → byte[]), Segment (BWO zerocopy to PipeWriter, single flush), AsyncSegment (AsyncPipeWriterOutput, per-chunk FlushAsync + pipeline parallelism).
AcBinaryHubProtocol is the base (unsealed) — general binary framing only. AyCodeBinaryHubProtocol derives from it with consumer-specific logic: SignalParams capture (via OnArgumentRead hook), IsRawBytesData path, SignalDataType type resolution. Register AyCodeBinaryHubProtocol in both client and server.
Wire format, argument framing, dual BWO pattern, length prefix patching:
../SIGNALR_BINARY_PROTOCOL/README.md
SignalParams + Payload Separation
SignalParams (separate hub argument, [AcBinarySerializable]):
| Field | Type | Purpose |
|---|---|---|
Status |
SignalResponseStatus | Success/Error |
DataSerializerType |
AcSerializerType | Binary or JsonGZip — tells client how to deserialize response data |
Parameters |
byte[]? |
Serialized byte[][] as single byte[] (protocol fast-path). Null when no parameters. |
SignalDataType |
string? |
AssemblyQualifiedName of response object type. Server sets before sending. Protocol uses this for eager type-aware deserialization. Null for raw byte[] responses. |
IsRawBytesData |
bool |
Client sets true when T == byte[] (e.g. DataSource populate/merge). Protocol returns raw byte[] without deserialization. |
Typed access via methods (PostDataJson pattern):
- Client:
SetParameterValues(object[])— packs each param viaToBinary()→byte[][]→byte[] - Server:
GetParameterValues(ParameterInfo[])— unpacksbyte[]→byte[][]→ per-elementBinaryTo(targetType) - Protocol never sees
byte[][]— onlybyte[].
object data (4th hub argument) — protocol handles three cases on read:
- byte[] fast-path: first byte is
BinaryTypeCode.ByteArray(0x44)→ skip tag, rest is raw payload bytes. No VarUInt (argLength implies size). No deserializer. - IsRawBytesData (AyCodeBinaryHubProtocol):
SignalParams.IsRawBytesData == true→ return entire argSlice as rawbyte[]. No deserialization. Consumer handles deserialization. - Typed deserialization (AyCodeBinaryHubProtocol): resolve type from
SignalParams.SignalDataType→AcBinaryDeserializer.Deserialize(sequence, type)→ return typed object.
Parameters and data are independent — both can be null or filled in any direction (SignalR is bidirectional).
| Combination | Parameters | data | Example |
|---|---|---|---|
| Request | byte[] (packed params) |
null/empty | client calls server method |
| Response | null | typed object or byte[] | server returns result |
| Request + data | byte[] |
typed object | client responds to server with data |
| Signal | null | null/empty | ping, status change, broadcast trigger |
SignalResponseDataMessage is an internal DTO for client-side callback routing and stream wire format — constructed in-memory from signalParams + data, never serialized as envelope on wire. RawResponseData is object? (typed object or byte[]). GetResponseData<T>() performs direct cast.
Request/Response Flow
Client → Server
1. PostAsync<T>(tag, param) / PostAsync<T>(tag, params[]) / PostDataAsync(tag, data, callback)
2. signalParams.SetParameterValues(object[]):
Each param ToBinary() → byte[][] → ToBinary() → byte[] (single wire blob)
3. SignalParams { Status = Success, Parameters = byte[], IsRawBytesData = (typeof(T) == typeof(byte[])) }
4. SendCoreAsync → HubConnection.SendAsync("OnReceiveMessage", tag, requestId, signalParams, null)
5. AyCodeBinaryHubProtocol frames on wire (signalParams via AcBinary, data = null for requests)
Server → Client
OnReceiveMessage(tag, requestId, signalParams, object data)
├─ Construct SignalResponseDataMessage in-memory:
│ └─ { MessageTag, Status, DataSerializerType, RawResponseData = data }
├─ Matching requestId in pending dict:
│ ├─ Route: null→sync wait, Action→invoke, Func<Task>→await
│ └─ GetResponseData<T>(): direct cast (T)RawResponseData
│ Protocol already deserialized to correct type via SignalDataType
└─ No match (broadcast):
└─ abstract MessageReceived(tag, signalParams, object data).Forget()
Request pooling: SignalRRequestModel via SignalRRequestModelPool (ObjectPool + IResettable).
Parameter Deserialization (Server)
Server calls signalParams.GetParameterValues(paramInfos):
GetParameterValues(ParameterInfo[]):
1. byte[] → BinaryTo<byte[][]>() (cached in _parameterValues)
2. For each: byte[][i].BinaryTo(paramInfos[i].ParameterType)
3. Trailing optional params filled with defaults
Hub validates: missing required params throw ArgumentException.
Type-guided deserialization — each parameter is individually serialized/deserialized with its concrete type, avoiding the object[] → dictionary problem of untyped binary deserialization.
Known concerns and limitations on parameter serialization (per-parameter overhead, AcBinary-only) are tracked in
SIGNALR_ISSUES.mdunderSIG-I-2andSIG-I-3.
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.
Source Files
| Component | Path |
|---|---|
| Client base | SignalRs/AcSignalRClientBase.cs |
| Binary protocol (base) | SignalRs/AcBinaryHubProtocol.cs |
| Binary protocol (derived) | SignalRs/AyCodeBinaryHubProtocol.cs |
| Protocol mode enum | SignalRs/BinaryProtocolMode.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 class | SignalRs/ISignalParams.cs |
| Serialization | SignalRs/SignalRSerializationHelper.cs |