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

190 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 (`SignalData`) travel as **separate hub arguments**`SignalData` wraps pooled `byte[]` from `ArrayPool` via `AyCodeBinaryHubProtocol` (zero-copy fast-path), metadata is AcBinary serialized normally.
```
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 via `BufferWriterBinaryOutput` standalone mode. `byte[]` and `SignalData` args bypass serializer.
`AcBinaryHubProtocol` is the base (unsealed, generic). `AyCodeBinaryHubProtocol` derives from it and uses `ArrayPool` for `SignalData` arguments — the `CreateByteArrayResult` hook rents from pool instead of `.ToArray()`. 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. |
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[]`.
`SignalData data` (separate hub argument, protocol fast-path, ArrayPool-backed via `AyCodeBinaryHubProtocol`).
`SignalData` wraps pooled `byte[]` with `IDisposable` lifecycle. Consumer accesses via `Span` (zero-copy) or `ToArray()` (copy, rare). `Dispose()` returns rented buffer to `ArrayPool` with `clearArray: true`.
`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 | SignalData (response payload) | server returns result |
| Request + data | `byte[]` | SignalData | client responds to server with data |
| Signal | null | null/empty | ping, status change, broadcast trigger |
`SignalResponseDataMessage` remains as **internal DTO** for callback routing — constructed in-memory from `signalParams` + `data`, never serialized as envelope on wire. `ResponseData` is `SignalData?`. `GetResponseData<T>()` dispatches on `DataSerializerType`: Binary → `AcBinaryDeserializer.Deserialize<T>(Span)`, JsonGZip → decompress → `JsonTo<T>()`. `Dispose()` returns both SignalData and JSON decompression buffers to ArrayPool.
## 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[] }
4. 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, data)
├─ Construct SignalResponseDataMessage in-memory (no envelope deser):
│ └─ { Status, DataSerializerType, ResponseData (SignalData) } from signalParams + data
├─ Matching requestId in pending dict:
│ ├─ Route: null→sync wait, Action→invoke, Func<Task>→await
│ └─ GetResponseData<T>(): dispatches on DataSerializerType
│ Binary→Deserialize<T>(Span), JsonGZip→Decompress→JsonTo<T>()
└─ No match (broadcast):
└─ abstract MessageReceived(tag, signalParams, SignalData 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. 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` |
| Signal data wrapper | `SignalRs/SignalData.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` |