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

192 lines
9.4 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 (`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`.
`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` |
| 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` |