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
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
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
// 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
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
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
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. ConcurrentDictionaryused for sessions and groups — lock-free reads.- Send uses a per-client
lockonly during the actual socket write; FrameReaderis 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
HeartbeatIntervalInMillisecondsper 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)
// 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");
Description
Languages
C#
100%