Design covers the minimal NATS server port: pub/sub with wildcards and queue groups over System.IO.Pipelines, targeting .NET 10.
141 lines
6.1 KiB
Markdown
141 lines
6.1 KiB
Markdown
# NATS .NET Base Server Design
|
|
|
|
**Date:** 2026-02-22
|
|
**Scope:** Minimal single-node NATS server — pub/sub, queue groups, wildcards. No auth, clustering, JetStream, or monitoring.
|
|
**Target:** .NET 10, C#
|
|
**Approach:** Bottom-up (Sublist → Parser → Client → Server)
|
|
|
|
## Decisions
|
|
|
|
- **Framework:** .NET 10
|
|
- **I/O:** System.IO.Pipelines for zero-copy protocol parsing
|
|
- **Structure:** Library (NATS.Server) + Host (NATS.Server.Host) + Tests (NATS.Server.Tests)
|
|
- **Test framework:** xUnit
|
|
- **Queue group selection:** Round-robin with atomic counter
|
|
|
|
## Solution Layout
|
|
|
|
```
|
|
natsdotnet/
|
|
├── NatsDotNet.sln
|
|
├── src/
|
|
│ ├── NATS.Server/ # Core library
|
|
│ │ ├── NatsServer.cs # Server orchestrator
|
|
│ │ ├── NatsClient.cs # Client connection
|
|
│ │ ├── NatsOptions.cs # Configuration
|
|
│ │ ├── Protocol/
|
|
│ │ │ ├── NatsProtocol.cs # Constants, op codes
|
|
│ │ │ └── NatsParser.cs # Pipeline-based parser
|
|
│ │ └── Subscriptions/
|
|
│ │ ├── Subscription.cs
|
|
│ │ ├── SubjectMatch.cs
|
|
│ │ └── SubList.cs # Trie + cache
|
|
│ └── NATS.Server.Host/ # Console app
|
|
│ └── Program.cs
|
|
└── tests/
|
|
└── NATS.Server.Tests/
|
|
```
|
|
|
|
## Component Designs
|
|
|
|
### 1. SubList (subject matching trie)
|
|
|
|
Reference: `golang/nats-server/server/sublist.go`
|
|
|
|
**Structures:**
|
|
- `SubList` — root trie + cache (`Dictionary<string, SubListResult>`) + `ReaderWriterLockSlim` + atomic `genId`
|
|
- `TrieLevel` — `Dictionary<string, TrieNode>` for literal tokens, nullable `pwc` (`*`) and `fwc` (`>`) pointers
|
|
- `TrieNode` — optional `next: TrieLevel`, `HashSet<Subscription>` for plain subs, `Dictionary<string, HashSet<Subscription>>` for queue subs
|
|
- `Subscription` — client ref, subject, queue, sid, atomic message count, max messages
|
|
- `SubListResult` — `Subscription[]` plain subs, `Subscription[][]` queue subs (grouped)
|
|
|
|
**Operations:**
|
|
- `Insert(Subscription)` — walk trie by `.`-split tokens, create nodes, add sub, increment genId (clears cache)
|
|
- `Remove(Subscription)` — reverse, prune empty nodes, increment genId
|
|
- `Match(ReadOnlySpan<byte>)` — cache check (convert to string key on miss), walk trie collecting matches from literal + `*` + `>` nodes
|
|
- Thread safety: read lock for Match, write lock for Insert/Remove
|
|
|
|
### 2. Protocol Parser
|
|
|
|
Reference: `golang/nats-server/server/parser.go`
|
|
|
|
Operates on `PipeReader`. Scans for `\r\n` to delimit control lines. Identifies command by first bytes. For `PUB`/`HPUB`, reads control line then exactly `size` bytes of payload + `\r\n`.
|
|
|
|
**Commands (base server):**
|
|
|
|
| Command | Direction | Control line format |
|
|
|---------|-----------|-------------------|
|
|
| INFO | S→C | `INFO {json}\r\n` |
|
|
| CONNECT | C→S | `CONNECT {json}\r\n` |
|
|
| PUB | C→S | `PUB subject [reply] size\r\n[payload]\r\n` |
|
|
| HPUB | C→S | `HPUB subject [reply] hdr_size total_size\r\n[hdrs+payload]\r\n` |
|
|
| SUB | C→S | `SUB subject [queue] sid\r\n` |
|
|
| UNSUB | C→S | `UNSUB sid [max_msgs]\r\n` |
|
|
| MSG | S→C | `MSG subject sid [reply] size\r\n[payload]\r\n` |
|
|
| HMSG | S→C | `HMSG subject sid [reply] hdr_size total_size\r\n[hdrs+payload]\r\n` |
|
|
| PING/PONG | Both | `PING\r\n` / `PONG\r\n` |
|
|
| +OK / -ERR | S→C | `+OK\r\n` / `-ERR 'msg'\r\n` |
|
|
|
|
**Design choices:**
|
|
- Parser returns parsed command structs, does not dispatch
|
|
- No string allocation on hot path — subject matching uses `Span<byte>`
|
|
- Max control line: 4096 bytes, max payload: 1MB default (configurable)
|
|
|
|
### 3. NatsClient (connection handler)
|
|
|
|
Reference: `golang/nats-server/server/client.go`
|
|
|
|
**Fields:** unique id, Socket, IDuplexPipe, back-ref to server, subs dict (sid→Subscription), ClientOptions (from CONNECT json), stats (Interlocked counters), SemaphoreSlim for write serialization.
|
|
|
|
**Lifecycle:**
|
|
1. Server accepts socket, creates NatsClient
|
|
2. `RunAsync(CancellationToken)`: send INFO → read loop (parse + dispatch commands)
|
|
3. Dispatch: CONNECT→validate, SUB→insert into SubList, UNSUB→remove, PUB→`server.ProcessMessage()`, PING→PONG
|
|
4. On disconnect: remove all subs, remove from server clients dict
|
|
|
|
**Writing:** `SendMessageAsync()` formats MSG/HMSG, acquires write lock, writes to PipeWriter, flushes.
|
|
|
|
### 4. NatsServer (orchestrator)
|
|
|
|
Reference: `golang/nats-server/server/server.go`
|
|
|
|
**Fields:** NatsOptions, TCP listener Socket, `ConcurrentDictionary<ulong, NatsClient>`, SubList, atomic next client ID, ServerInfo struct, CancellationTokenSource.
|
|
|
|
**Core methods:**
|
|
- `StartAsync()` — bind, accept loop
|
|
- `ProcessMessage(subject, reply, headers, payload, sender)` — SubList.Match → deliver to plain subs + pick one per queue group (round-robin)
|
|
- `RemoveClient(client)` — cleanup subs and client dict
|
|
- `Shutdown()` — cancel, close listener, drain
|
|
|
|
### 5. NatsOptions
|
|
|
|
Reference: `golang/nats-server/server/opts.go`
|
|
|
|
Minimal for base: Host, Port (default 4222), ServerName, MaxPayload (1MB), MaxControlLine (4096), MaxConnections, PingInterval, MaxPingsOut.
|
|
|
|
## Testing Strategy
|
|
|
|
**Unit tests:**
|
|
- SubList: insert/remove/match with literals, `*`, `>`, queue groups, cache invalidation. Port Go test cases.
|
|
- Parser: each command type, malformed input, partial reads, boundary sizes.
|
|
- Client protocol: CONNECT handshake, SUB/UNSUB lifecycle, PUB delivery, PING/PONG via loopback sockets.
|
|
|
|
**Integration tests (using NATS.Client.Core NuGet):**
|
|
- Basic pub/sub, wildcard matching, queue group distribution, fan-out, UNSUB + auto-unsub, PING/PONG.
|
|
|
|
## Module Boundaries (future)
|
|
|
|
These are explicitly excluded from this design and will be added as separate modules later:
|
|
|
|
| Module | Key files in Go reference |
|
|
|--------|--------------------------|
|
|
| Authentication | auth.go, auth_callout.go, nkey.go, jwt.go |
|
|
| Monitoring HTTP | monitor.go |
|
|
| Clustering/Routes | route.go |
|
|
| Gateways | gateway.go |
|
|
| Leaf Nodes | leafnode.go |
|
|
| JetStream | jetstream.go, stream.go, consumer.go, filestore.go, memstore.go, raft.go |
|
|
| WebSocket | websocket.go |
|
|
| MQTT | mqtt.go |
|
|
| TLS | TLS config in opts.go, various TLS setup in server.go |
|