229 lines
8.2 KiB
Markdown
229 lines
8.2 KiB
Markdown
# EonaCat.QuicNet
|
|
|
|
EonaCat.QuicNet is a High-performance C# TCP networking library with **QUIC-like semantics** — sessions, groups, heartbeats, auto-reconnect, and 100 000+ concurrent connections with a minimal memory footprint.
|
|
Compatible with **.NET Framework 4.8.1** and **.NET 6 / 7 / 8**.
|
|
|
|
---
|
|
|
|
## Features
|
|
|
|
| Feature | Detail |
|
|
|---|---|
|
|
| **100 k+ connections** | Pooled `ThreadPool` + `ConcurrentDictionary` |
|
|
| **Sessions** | Every client gets a `Guid`-based `SessionId` |
|
|
| **Nicknames** | Set on connect or at runtime, synced both ways |
|
|
| **Groups** | Server- or client-initiated join/leave |
|
|
| **Send variants** | `byte[]`, `string`, custom encoding, structs |
|
|
| **Targeting** | `SendTo`, `SendToGroup`, `Broadcast` |
|
|
| **Heartbeat** | Configurable ping/pong keeps connections alive |
|
|
| **Auto-reconnect** | Exponential back-off, configurable max attempts |
|
|
| **Events** | Connected, Disconnected, DataReceived, Error, GroupJoined, … |
|
|
| **Graceful shutdown** | Server and client both send disconnect frames |
|
|
|
|
---
|
|
|
|
## Quick Start
|
|
|
|
### Server
|
|
|
|
```csharp
|
|
var server = new QuicServer(new QuicServerOptions { Port = 9000 });
|
|
|
|
server.ClientConnected += (_, e) => Console.WriteLine($"+ {e.Client.Nickname}");
|
|
server.ClientDisconnected += (_, e) => Console.WriteLine($"- {e.Client.Nickname}: {e.Reason}");
|
|
server.DataReceived += (_, e) => Console.WriteLine($"[{e.Client.Nickname}] {e.Text}");
|
|
|
|
server.Start();
|
|
```
|
|
|
|
### Client
|
|
|
|
```csharp
|
|
var client = new QuicClient(new QuicClientOptions
|
|
{
|
|
Host = "127.0.0.1",
|
|
Port = 9000,
|
|
Nickname = "Alice",
|
|
AutoReconnect = true
|
|
});
|
|
|
|
client.Connected += (_, e) => Console.WriteLine("Connected: " + e.SessionId);
|
|
client.DataReceived += (_, e) => Console.WriteLine("Server says: " + e.Text);
|
|
|
|
client.Connect();
|
|
client.Send("Hello, world!");
|
|
```
|
|
|
|
---
|
|
|
|
## API Reference
|
|
|
|
### `QuicServerOptions`
|
|
|
|
| Property | Default | Description |
|
|
|---|---|---|
|
|
| `Port` | 9000 | TCP port to listen on |
|
|
| `BindAddress` | `0.0.0.0` | Network interface |
|
|
| `MaxConnections` | 100 000 | Hard cap |
|
|
| `BacklogSize` | 1000 | `Socket.Listen` backlog |
|
|
| `ReceiveBufferSize` | 8 192 | Per-socket receive buffer (bytes) |
|
|
| `SendBufferSize` | 8 192 | Per-socket send buffer (bytes) |
|
|
| `HeartbeatIntervalInMilliseconds` | 15 000 | How often to ping clients |
|
|
| `ClientTimeoutInMilliseconds` | 45 000 | Idle → disconnect |
|
|
| `MaxMessageSizeBytes` | 10 MB | Frame size limit |
|
|
| `NoDelay` | `true` | TCP_NODELAY (low latency) |
|
|
| `EnableHeartbeat` | `true` | Auto-ping all clients |
|
|
|
|
### `QuicClientOptions`
|
|
|
|
| Property | Default | Description |
|
|
|---|---|---|
|
|
| `Host` | `127.0.0.1` | Server host or IP |
|
|
| `Port` | 9000 | Server port |
|
|
| `Nickname` | `null` | Initial nickname |
|
|
| `ConnectTimeoutInMilliseconds` | 10 000 | Connect attempt timeout |
|
|
| `HeartbeatIntervalInMilliseconds` | 15 000 | Ping interval |
|
|
| `ServerTimeoutMs` | 45 000 | Idle server → disconnect |
|
|
| `AutoReconnect` | `true` | Re-connect on drop |
|
|
| `ReconnectMaxAttempts` | 5 | 0 = unlimited |
|
|
| `ReconnectBaseDelayMs` | 1 000 | Exponential back-off base |
|
|
| `NoDelay` | `true` | TCP_NODELAY |
|
|
|
|
---
|
|
|
|
### Server Methods
|
|
|
|
```csharp
|
|
// Lifecycle
|
|
server.Start();
|
|
server.Stop();
|
|
|
|
// Queries
|
|
IEnumerable<IQuicClient> server.GetClients();
|
|
IQuicClient server.GetClient(string sessionId); // null if not found
|
|
IEnumerable<IQuicClient> server.GetGroupClients(string group);
|
|
IEnumerable<string> server.GetGroups();
|
|
int server.ClientCount;
|
|
|
|
// Send to one
|
|
SendResult server.SendTo(string sessionId, byte[] data);
|
|
SendResult server.SendTo(string sessionId, string text, Encoding enc = null);
|
|
SendResult server.SendTo(IQuicClient client, byte[] data);
|
|
SendResult server.SendTo(IQuicClient client, string text, Encoding enc = null);
|
|
|
|
// Send to group
|
|
int server.SendToGroup(string group, byte[] data, string excludeSessionId = null);
|
|
int server.SendToGroup(string group, string text, Encoding enc = null, string excludeSessionId = null);
|
|
|
|
// Broadcast
|
|
int server.Broadcast(byte[] data, string excludeSessionId = null);
|
|
int server.Broadcast(string text, Encoding enc = null, string excludeSessionId = null);
|
|
|
|
// Group management
|
|
void server.AddToGroup(string sessionId, string group);
|
|
void server.RemoveFromGroup(string sessionId, string group);
|
|
|
|
// Disconnect / kick
|
|
bool server.Kick(string sessionId, string reason = null);
|
|
```
|
|
|
|
### Server Events
|
|
|
|
```csharp
|
|
server.Started += (s, e) => { /* e.Port */ };
|
|
server.Stopped += (s, e) => { };
|
|
server.ClientConnected += (s, e) => { /* e.Client */ };
|
|
server.ClientDisconnected += (s, e) => { /* e.Client, e.Reason, e.Message */ };
|
|
server.DataReceived += (s, e) => { /* e.Client, e.Data, e.Text */ };
|
|
server.ClientJoinedGroup += (s, e) => { /* e.Client, e.GroupName */ };
|
|
server.ClientLeftGroup += (s, e) => { /* e.Client, e.GroupName */ };
|
|
server.Error += (s, e) => { /* e.Exception, e.Context */ };
|
|
```
|
|
|
|
---
|
|
|
|
### Client Methods
|
|
|
|
```csharp
|
|
void client.Connect();
|
|
void client.Disconnect(string reason = null);
|
|
|
|
// Send
|
|
SendResult client.Send(byte[] data);
|
|
SendResult client.Send(string text);
|
|
SendResult client.Send(string text, Encoding encoding);
|
|
SendResult client.SendJson(string json); // alias for Send(string)
|
|
SendResult client.SendObject<T>(T obj, Func<T, byte[]> ser); // extension
|
|
SendResult client.SendStruct<T>(T value) where T : struct; // extension
|
|
```
|
|
|
|
### Client Events
|
|
|
|
```csharp
|
|
client.Connected += (s, e) => { /* e.SessionId */ };
|
|
client.Disconnected += (s, e) => { /* e.Client, e.Reason */ };
|
|
client.DataReceived += (s, e) => { /* e.Data, e.Text */ };
|
|
client.Reconnecting += (s, e) => { /* e.Attempt, e.MaxAttempts */ };
|
|
client.ReconnectFailed += (s, e) => { /* e.Exception */ };
|
|
client.Error += (s, e) => { /* e.Exception, e.Context */ };
|
|
```
|
|
|
|
---
|
|
|
|
## Wire Protocol
|
|
|
|
EonaCat.QuicNet uses a simple, efficient binary framing protocol — no XML, JSON, or text overhead on the wire:
|
|
|
|
```
|
|
┌──────────────┬──────────────────────┬──────────────────────────┐
|
|
│ Type (1 B) │ Payload length (4 B)│ Payload (N bytes) │
|
|
│ MessageType │ big-endian uint32 │ arbitrary bytes │
|
|
└──────────────┴──────────────────────┴──────────────────────────┘
|
|
```
|
|
|
|
`MessageType` values: `Handshake`, `HandshakeAck`, `Data`, `Ping`, `Pong`, `Disconnect`, `GroupJoin`, `GroupLeave`, `NicknameSet`, `System`.
|
|
|
|
---
|
|
|
|
## Architecture & Performance
|
|
|
|
- **Accept loop** runs on a dedicated background thread.
|
|
- **Each client** gets its own receive thread pulled from `ThreadPool.QueueUserWorkItem`.
|
|
At 100 k connections, this means 100 k lightweight ThreadPool work items — not 100 k OS threads.
|
|
- **`ConcurrentDictionary`** used for sessions and groups — lock-free reads.
|
|
- **Send** uses a per-client `lock` only during the actual socket write;
|
|
- **`FrameReader`** is a tiny stateful struct that handles partial reads with zero heap allocations per frame (beyond the payload buffer).
|
|
- **Heartbeat** is one thread that iterates all clients every `HeartbeatIntervalInMilliseconds` per cycle.
|
|
|
|
### Memory
|
|
|
|
| Item | Size |
|
|
|---|---|
|
|
| `ConnectedClient` object | ~200 bytes + socket buffers |
|
|
| Per-client receive buffer | `ReceiveBufferSize` (default 8 KB) |
|
|
| Per-client send buffer | `SendBufferSize` (default 8 KB) |
|
|
| Groups | `HashSet<string>` per client (only allocated if joined) |
|
|
|
|
With defaults, 100 000 connections use roughly **1.6 GB** of socket buffer memory (OS-level) plus ~20 MB of managed heap — well within modern server specs.
|
|
|
|
To reduce further: lower `ReceiveBufferSize` / `SendBufferSize` to 4096 or even 2048.
|
|
|
|
---
|
|
|
|
## Extension Methods (`QuicExtensions`)
|
|
|
|
```csharp
|
|
// Server
|
|
server.BroadcastText("hello");
|
|
server.SendToGroupText("vip", "VIP message");
|
|
server.SendSystem(sessionId, "system notice");
|
|
server.KickNotInGroup("authenticated"); // kicks everyone NOT in a group
|
|
|
|
// Client
|
|
client.SendObject(myObj, obj => Serialize(obj));
|
|
client.SendStruct(myValueTypeStruct);
|
|
|
|
// IQuicClient
|
|
client.IsInGroup("vip");
|
|
```
|