Initial version

This commit is contained in:
2026-02-27 20:47:19 +01:00
parent 6b35bdf338
commit 2a32b559c1
13 changed files with 2557 additions and 42 deletions

227
README.md
View File

@@ -1,3 +1,228 @@
# EonaCat.QuicNet
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");
```