282 lines
12 KiB
Markdown
282 lines
12 KiB
Markdown
# SignalR Communication
|
|
|
|
The transport layer for AyCode real-time communication. Source: `AyCode.Services/SignalRs/` (client), `AyCode.Services.Server/SignalRs/` (server).
|
|
|
|
> For the change-tracked collection built on top of this transport, see [`SIGNALR_DATASOURCE.md`](SIGNALR_DATASOURCE.md).
|
|
|
|
## Design Overview
|
|
|
|
All communication flows through a **single hub method** with **tag-based dispatch**:
|
|
|
|
```
|
|
Client ──OnReceiveMessage(tag, bytes, requestId)──► Server
|
|
Client ◄──OnReceiveMessage(tag, bytes, requestId)── Server
|
|
```
|
|
|
|
`OnReceiveMessage` is the only transport channel — all calls go through it. The `tag` (int) determines **which server method to invoke**. The `DynamicMethodRegistry` maps the tag to a concrete `[SignalR(tag)]`-annotated method at runtime. Projects call any endpoint by specifying the tag; the server dispatches accordingly.
|
|
|
|
```
|
|
Client side: Server side:
|
|
───────────────────── ─────────────────────
|
|
AcSignalRClientBase AcWebSignalRHubBase<TTags, TLogger>
|
|
├─ HubConnection (WebSocket) ├─ Hub<IAcSignalRHubItemServer>
|
|
├─ AcBinaryHubProtocol ├─ DynamicMethodRegistry
|
|
├─ Pending request tracking ├─ Parameter deserialization
|
|
└─ Response callbacks └─ Broadcast to other clients
|
|
```
|
|
|
|
## Tag System
|
|
|
|
### Tag Definition
|
|
|
|
AyCode.Core defines only the base class and built-in infrastructure tags:
|
|
|
|
```csharp
|
|
public class AcSignalRTags
|
|
{
|
|
public const int None = 0;
|
|
public const int PingTag = 90001;
|
|
public const int EchoTag = 90002;
|
|
}
|
|
```
|
|
|
|
**Consuming projects** define their own tags by inheriting `AcSignalRTags`:
|
|
|
|
```csharp
|
|
public abstract class MyProjectTags : AcSignalRTags
|
|
{
|
|
public const int GetOrders = 100;
|
|
public const int GetOrderById = 101;
|
|
public const int SaveOrder = 102;
|
|
// ... project-specific tags
|
|
}
|
|
```
|
|
|
|
Tags are plain `int` constants. The project decides the numbering. Tags must be unique across all registered services within a hub.
|
|
|
|
### Tag Attributes
|
|
|
|
Three attribute levels:
|
|
|
|
```csharp
|
|
// Base: maps an integer tag to a method
|
|
[Tag(42)]
|
|
|
|
// Server method: tag + optional broadcast behavior
|
|
[SignalR(messageTag: 100, sendToOtherClientTag: 100, sendToOtherClientType: SendToClientType.Others)]
|
|
// messageTag: incoming tag this method handles
|
|
// sendToOtherClientTag: tag used when broadcasting result to other clients (can differ from messageTag)
|
|
// sendToOtherClientType: who receives the broadcast
|
|
|
|
// Client receive: marks method for server→client dispatch
|
|
[SignalRSendToClient(100)]
|
|
```
|
|
|
|
**SendToClientType:** `None` (no broadcast), `Others` (all except caller), `Caller` (response only), `All` (everyone).
|
|
|
|
### How Projects Use Tags
|
|
|
|
Projects call the SignalR transport directly — not only through DataSource:
|
|
|
|
```csharp
|
|
// Direct call: project code calls any endpoint by tag
|
|
var orders = await signalRClient.PostAsync<List<Order>>(MyTags.GetOrders, companyId);
|
|
var order = await signalRClient.GetByIdAsync<Order>(MyTags.GetOrderById, orderId);
|
|
await signalRClient.PostDataAsync(MyTags.SaveOrder, order);
|
|
|
|
// Async callback (non-blocking)
|
|
await signalRClient.PostDataAsync(MyTags.SaveOrder, order, async response => { ... });
|
|
|
|
// Fire-and-forget
|
|
await signalRClient.PostDataAsync(MyTags.SaveOrder, order, response => { ... });
|
|
```
|
|
|
|
The `AcSignalRClientBase` CRUD helpers (`PostAsync`, `GetByIdAsync`, `GetAllAsync`, `PostDataAsync`) are all generic transport methods that work with any tag — they are not tied to DataSource.
|
|
|
|
## Dynamic Method Dispatch
|
|
|
|
See also: [`AyCode.Models.Server/DynamicMethods/README.md`](../AyCode.Models.Server/DynamicMethods/README.md)
|
|
|
|
### Server-Side Lookup
|
|
|
|
```
|
|
1. OnReceiveMessage(tag=100, bytes, requestId)
|
|
|
|
2. DynamicMethodRegistry.GetMethodByMessageTag(100)
|
|
├─ Check static ConcurrentDictionary<int, (Type, AcMethodInfoModel)?> cache
|
|
├─ Hit? → find instance of cached Type from registered instances
|
|
├─ Miss? → scan all registered instances' methods for [SignalR(100)]
|
|
│ cache the result (including negative = null)
|
|
└─ Return (instance, methodInfoModel) or null
|
|
|
|
3. AcMethodInfoModel contains:
|
|
├─ MethodInfo (the method to invoke)
|
|
├─ SignalRAttribute (tag, sendToOtherClientTag, sendToOtherClientType)
|
|
└─ ParamInfos[] (ParameterInfo for deserialization)
|
|
```
|
|
|
|
The `DynamicMethodRegistry` uses a static `ConcurrentDictionary` for the global tag→method cache. Note: `AcDynamicMethodCallModel` uses a `FrozenDictionary` per-type cache internally, but the registry's own lookup path does direct method scanning with `ConcurrentDictionary` caching.
|
|
|
|
### Registration
|
|
|
|
The hub registers service instances during initialization:
|
|
|
|
```csharp
|
|
DynamicMethodRegistry.Register(myService); // scans [SignalR(tag)] methods lazily
|
|
DynamicMethodRegistry.Register(anotherService);
|
|
```
|
|
|
|
Reflection runs lazily per tag on first request, then results are cached statically.
|
|
|
|
## Wire Protocol
|
|
|
|
### AcBinaryHubProtocol
|
|
|
|
Custom `IHubProtocol` (name: `"acbinary"`), replaces default JSON. Frame format:
|
|
|
|
```
|
|
[4 bytes: payload length, little-endian] [1 byte: message type] [payload...]
|
|
```
|
|
|
|
Message types: Invocation(1), StreamItem(2), Completion(3), Ping(6), Close(7), Ack(8), Sequence(9).
|
|
|
|
Arguments serialized individually with VarUInt length prefix. Direct write to `IBufferWriter` via `BufferWriterBinaryOutput`.
|
|
|
|
### Response Message
|
|
|
|
`SignalResponseDataMessage` carries:
|
|
|
|
| Field | Type | Purpose |
|
|
|-------|------|---------|
|
|
| `MessageTag` | int | Tag identifying the operation |
|
|
| `Status` | SignalResponseStatus | Success/Error |
|
|
| `ResponseData` | byte[] | Binary-serialized payload (or GZip+JSON fallback) |
|
|
| `DataSerializerType` | AcSerializerType | Binary or Json |
|
|
|
|
**Binary mode** (default): `AcBinarySerializer.ToBinary(data)` → raw bytes.
|
|
**JSON fallback**: `ToJson(data)` → `GzipHelper.Compress(json)` → bytes.
|
|
|
|
## Request/Response Flow
|
|
|
|
### Client → Server
|
|
|
|
```
|
|
1. PostAsync<T>(tag, postData) or PostDataAsync(tag, data, callback)
|
|
2. CreatePostMessage(postData):
|
|
├─ Primitives/strings/enums/value types → IdMessage (JSON array of serialized values)
|
|
└─ Complex objects → SignalPostJsonDataMessage<T> ⚠️ tech debt: JSON-in-Binary
|
|
3. SerializeToBinary(message) wraps in Binary envelope
|
|
4. HubConnection.SendAsync("OnReceiveMessage", tag, bytes, requestId)
|
|
5. AcBinaryHubProtocol frames it on the wire
|
|
```
|
|
|
|
### Server Processing
|
|
|
|
```
|
|
6. OnReceiveMessage(tag, bytes, requestId)
|
|
7. DynamicMethodRegistry.GetMethodByMessageTag(tag) ← ConcurrentDictionary lookup
|
|
8. DeserializeParameters(bytes):
|
|
├─ DeserializeFromBinary<SignalPostJsonMessage>() ← unwrap Binary envelope
|
|
├─ IdMessage format? → parse each Ids[i] as JSON per parameter type
|
|
└─ Complex object? → json.JsonTo(paramType) ⚠️ tech debt: JSON parse
|
|
9. MethodInfo.InvokeMethod(instance, params) ← unwraps Task/ValueTask
|
|
10. CreateResponseMessage(tag, Success, result) ← pure Binary serialization
|
|
11. ResponseToCaller(tag, message, requestId)
|
|
12. If SendToOtherClientType != None:
|
|
└─ SendMessageToOthers(sendToOtherClientTag, result) ← uses sendToOtherClientTag, not messageTag
|
|
```
|
|
|
|
### Server → Client
|
|
|
|
```
|
|
13. Client.OnReceiveMessage(tag, bytes, requestId)
|
|
14. Has matching requestId in pending ConcurrentDictionary?
|
|
├─ YES (response to own request):
|
|
│ 15. DeserializeFromBinary<SignalResponseDataMessage>(bytes)
|
|
│ 16. Route based on pending request type:
|
|
│ ├─ null (sync wait) → set ResponseByRequestId, WaitToAsync completes
|
|
│ ├─ Action<SignalResponseDataMessage> → invoke directly
|
|
│ └─ Func<SignalResponseDataMessage, Task> → invoke and await
|
|
│ 17. GetResponseData<T>():
|
|
│ ├─ Binary: ResponseData.BinaryTo<T>()
|
|
│ └─ JSON: GzipDecompress → AcJsonDeserializer.Deserialize<T>()
|
|
│
|
|
└─ NO (broadcast from another client's action):
|
|
18. abstract MessageReceived(tag, bytes).Forget()
|
|
└─ Consuming project overrides to handle server push
|
|
```
|
|
|
|
**Broadcast receive:** When the server broadcasts to `Others`/`All`, receiving clients have no matching `requestId`. The message falls through to `MessageReceived(int messageTag, byte[] messageBytes)` — an abstract method the consuming project overrides to handle unsolicited server pushes (e.g., refresh UI, update local state).
|
|
|
|
**Request model pooling:** `SignalRRequestModel` instances are managed via `SignalRRequestModelPool` (ObjectPool<T> + IResettable) to avoid allocations per request.
|
|
|
|
### Response Patterns
|
|
|
|
| Pattern | Method | Blocking |
|
|
|---------|--------|----------|
|
|
| Sync wait | `PostAsync<T>(tag, data)` | Yes, polls up to `TransportSendTimeout` (60s) |
|
|
| Async callback | `PostDataAsync(tag, data, async msg => {...})` | No, `Func<SignalResponseDataMessage, Task>` |
|
|
| Fire-and-forget | `PostDataAsync(tag, data, msg => {...})` | No, `Action<SignalResponseDataMessage>` |
|
|
|
|
## Session Management
|
|
|
|
`AcSessionService<TSessionItem, TSessionItemId>` tracks connected clients:
|
|
|
|
```csharp
|
|
ConcurrentDictionary<TSessionItemId, TSessionItem> Sessions
|
|
```
|
|
|
|
`IAcSessionItem<TSessionItemId>` requires `SessionId` property. Used for targeting messages to specific users/connections.
|
|
|
|
## Broadcast Service
|
|
|
|
`AcSignalRSendToClientService<THub, TTags, TLogger>` provides server-push methods:
|
|
|
|
| Method | Target |
|
|
|--------|--------|
|
|
| `SendMessageToAllClients` | All connected |
|
|
| `SendMessageToConnection(connectionId)` | Single connection |
|
|
| `SendMessageToUser(userId)` | User (all connections) |
|
|
| `SendMessageToUsers(userIds)` | Multiple users |
|
|
|
|
All messages wrapped in `SignalResponseDataMessage` → binary serialized → `OnReceiveMessage`.
|
|
|
|
## Connection Lifecycle
|
|
|
|
**Client configuration:**
|
|
- Transport: WebSockets only (`SkipNegotiation = true`)
|
|
- Buffer: 30MB transport + 30MB application
|
|
- Reconnection: `WithAutomaticReconnect()` + `WithStatefulReconnect()`
|
|
- Keepalive: 60s interval, 180s server timeout
|
|
|
|
**Hub events:**
|
|
- `OnConnectedAsync()` — log connection
|
|
- `OnDisconnectedAsync(exception)` — log disconnection, cleanup session
|
|
|
|
## Diagnostics
|
|
|
|
Enable with `AcSignalRClientBase.EnableBinaryDiagnostics = true` or `AcWebSignalRHubBase.EnableBinaryDiagnostics = true`.
|
|
|
|
Logs: hex dump (500 byte sample), header parsing (version, marker), property count + names via VarUInt reading.
|
|
|
|
`SignalResponseDataMessage.DiagnosticLogger` — per-response logging: target type info, property list, inheritance chain, hex dump.
|
|
|
|
## Known Technical Debt
|
|
|
|
**JSON-in-Binary request parameters:** Client→server requests currently wrap parameters in JSON inside the binary envelope (`SignalPostJsonDataMessage`). This adds an unnecessary serialization round-trip. Responses are already pure binary. Do NOT attempt to fix as a side effect — requires coordinated changes across all consuming projects.
|
|
|
|
## Key Source Files
|
|
|
|
| Component | Path |
|
|
|-----------|------|
|
|
| Hub base | `AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs` |
|
|
| Client base | `AyCode.Services/SignalRs/AcSignalRClientBase.cs` |
|
|
| Binary protocol | `AyCode.Services/SignalRs/AcBinaryHubProtocol.cs` |
|
|
| Message types | `AyCode.Services/SignalRs/IAcSignalRHubClient.cs` |
|
|
| Serialization | `AyCode.Services/SignalRs/SignalRSerializationHelper.cs` |
|
|
| Tag attributes | `AyCode.Services/SignalRs/SignalMessageTagAttribute.cs` |
|
|
| Base tags | `AyCode.Services/SignalRs/AcSignalRTags.cs` |
|
|
| Session service | `AyCode.Services.Server/SignalRs/AcSessionService.cs` |
|
|
| Broadcast service | `AyCode.Services.Server/SignalRs/AcSignalRSendToClientService.cs` |
|
|
| Dynamic dispatch | `AyCode.Models.Server/DynamicMethods/AcDynamicMethodRegistry.cs` |
|