193 lines
9.6 KiB
Markdown
193 lines
9.6 KiB
Markdown
# 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, 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:
|
||
|
||
```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<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, single flush), `Segment` (per-chunk flush via `AsyncPipeWriterOutput`), `AsyncSegment` (reserved).
|
||
|
||
`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.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 via `ToBinary()` → `byte[][]` → `byte[]`
|
||
- **Server**: `GetParameterValues(ParameterInfo[])` — unpacks `byte[]` → `byte[][]` → per-element `BinaryTo(targetType)`
|
||
- Protocol never sees `byte[][]` — only `byte[]`.
|
||
|
||
`object data` (4th hub argument) — protocol handles three cases on read:
|
||
1. **byte[] fast-path**: first byte is `BinaryTypeCode.ByteArray(0x44)` → skip tag, rest is raw payload bytes. No VarUInt (argLength implies size). No deserializer.
|
||
2. **IsRawBytesData** (AyCodeBinaryHubProtocol): `SignalParams.IsRawBytesData == true` → return entire argSlice as raw `byte[]`. No deserialization. Consumer handles deserialization.
|
||
3. **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.
|
||
|
||
**Perf concern:** Per-parameter `ToBinary()`/`BinaryTo(Type)` = N× context pool acquire/release + N× type-dispatch (ThreadLocal + ConcurrentDictionary cache). For many small primitives (int, bool, string) the per-call overhead may exceed a single bulk serialization call. Complex objects benefit clearly. If benchmarks show regression vs old JSON path, a batch fast-path (single serialization context for all params) should be added.
|
||
|
||
**Limitation:** Parameter serialization/deserialization is currently AcBinary only (`ToBinary()`/`BinaryTo()`). JSON support would require dispatching on serializer type in `SignalParams` methods + AcJsonSerializer reference.
|
||
|
||
## 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` |
|