2026-02-27 20:47:19 +01:00
2026-02-27 20:47:19 +01:00
2026-02-27 20:47:19 +01:00
2026-02-27 19:32:57 +00:00
2026-02-27 20:47:19 +01:00
2026-02-27 20:47:19 +01:00
2026-02-27 20:47:19 +01:00
2026-02-27 20:47:19 +01:00

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.
  • 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)

// 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
EonaCat.QuicNet
https://EonaCat.com
Readme 148 KiB
Languages
C# 100%