# 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 server.GetClients(); IQuicClient server.GetClient(string sessionId); // null if not found IEnumerable server.GetGroupClients(string group); IEnumerable 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 obj, Func ser); // extension SendResult client.SendStruct(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` 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"); ```