156 lines
5.6 KiB
Markdown
156 lines
5.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, 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:
|
|
|
|
```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);
|
|
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` |
|