Repository navigation
3 cshttp server Engine
Source Folder:
src/cshttp.server/
Namespace:cshttp
Dependencies: Referencescshttpandcshttp.web(Zero external NuGet packages, C# 7.3,.NET Framework 4.8and.NET 8 / 10+)
cshttp.server 1.0 is a self-contained, production-ready HTTP/1.1 and HTTPS TCP server that embeds directly into any C# Console Application, Windows Service, or Linux systemd daemon.
As proven in our Concurrency & Stress-Test Benchmark Report, cshttp.server 1.0 solves the classic tension between idle keep-alive connection scalability and synchronous handler simplicity by dividing the connection lifecycle into two distinct stages:
-
Zero-Thread IOCP Idle State (
ArmIdleRead):- Whenever a newly accepted connection waits for its first request—or an existing HTTP/1.1 connection sits idle between keep-alive requests—
Connectionregisters an asynchronous OS-level I/O Completion Port (IOCP / epoll) wait via_stream.BeginRead(...)and releases the thread immediately. - 10,000 idle browser keep-alive connections consume 0 OS threads.
- A single lightweight background timer (
SweepIdleConnections, ticking every 200 ms) checksStopwatchdeadlines across all idle connections and aborts expired sockets without holding per-socket timers or threads.
- Whenever a newly accepted connection waits for its first request—or an existing HTTP/1.1 connection sits idle between keep-alive requests—
-
IOCP-to-Worker Hand-Off & Synchronous Burst Execution (
RunActiveWorker):- The instant the first packet of a request arrives, the IOCP callback completes
EndReadand immediately queuesRunActiveWorkerto a pre-warmed.NET ThreadPoolWorker thread (ThreadPool.QueueUserWorkItem), freeing the IOCP thread in nanoseconds so synchronous DB/file handlers never starve network I/O completions. - On the Worker thread, header parsing, body reading (with Slowloris minimum-data-rate enforcement),
HttpContext.Currentassignment, the application's synchronousAction<HttpContext>handler, and response flushing run straight through with zeroasync/awaitstate-machine allocations. - Before releasing the Worker thread,
RunActiveWorkerchecksif (_socket.Available > 0). If the client (or an Nginx upstream keep-alive pool) has already sent a follow-up request, the same hot worker thread drains it immediately with zero context switches.
- The instant the first packet of a request arrives, the IOCP callback completes
| File | Public Types | Role |
|---|---|---|
| HttpServer.cs | HttpServer |
Socket listener bindings, pre-warmed ThreadPool configuration, active connection/request counters, 200 ms idle-timeout sweeper, and graceful shutdown drain (Start, Run, Stop). |
| Connection.cs |
Connection (internal)
|
Per-socket state machine: TLS handshake (SslStream), Zero-Thread IOCP idle wait, synchronous worker request loop, 100-continue, chunked body decoding, Slowloris rate checking, X-Forwarded-* resolution, and socket linger drain. |
| ServerOptions.cs |
ServerOptions, ServerBinding
|
Endpoints (ListenHttp, ListenHttps), connection caps, timeouts, Slowloris thresholds, trusted proxy IPs, and error hooks. |
| WebApp.cs |
WebApp, Request, Response (static) |
Beginner-friendly entry point: WebApp.Run(port, handler) (single or multi-port) with auto-detected ContentRoot and MapPath, plus static Request.* / Response.* shortcuts over HttpContext.Current so handlers need no ctx parameter. |
using System;
using System.Net;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using cshttp;
var options = new ServerOptions
{
// --- Application Root (used by HostingEnvironment.MapPath & StaticFiles) ---
ContentRoot = AppDomain.CurrentDomain.BaseDirectory,
// --- Connection & Payload Limits ---
MaxConnections = 10000, // Concurrent open sockets (idle + active); extra are closed immediately
MaxRequestsPerConnection = 0, // 0 = unlimited keep-alive requests per connection
MaxRequestBodySize = 30L * 1024 * 1024, // 30 MB default (413 Content Too Large if exceeded)
// --- Wire & Form Limits ---
Parser = new ParserOptions
{
MaxRequestLineLength = 8 * 1024, // 8 KB (414 URI Too Long)
MaxHeaderBytes = 32 * 1024, // 32 KB total header block (431)
MaxHeaderCount = 100, // 100 headers max (431)
AllowBareLf = false // Strict CRLF framing
},
Request = new RequestSettings
{
MaxCollectionKeys = 1000, // Max QueryString / Form keys (400 if exceeded)
MaxMultipartParts = 1000, // Max multipart/form-data parts
MaxMultipartHeaderBytes = 16 * 1024 // Max headers per multipart part
},
// --- Timeouts & Slowloris Defense ---
KeepAliveTimeout = TimeSpan.FromSeconds(60), // Idle wait between keep-alive requests
RequestHeadersTimeout = TimeSpan.FromSeconds(30), // Max time to receive full request headers (408 if exceeded)
BodyReadTimeout = TimeSpan.FromSeconds(30), // Max stall on any single body read (408)
MinRequestBodyBytesPerSecond = 240, // Minimum upload rate after grace period (408 Slowloris defense)
MinDataRateGracePeriod = TimeSpan.FromSeconds(5), // Grace period before min body rate is enforced
SendTimeout = TimeSpan.FromSeconds(60), // Socket send timeout
TlsHandshakeTimeout = TimeSpan.FromSeconds(10), // Max time for SslStream.AuthenticateAsServer
ShutdownTimeout = TimeSpan.FromSeconds(30), // Graceful drain window on Stop() / Ctrl+C
// --- Reverse Proxy & Security ---
ServerHeader = null, // Omitted by default; set e.g. "my-server" if desired
DetailedErrors = false, // Set true only in development to include stack traces in 500 pages
SslProtocols = SslProtocols.None, // Lets the OS negotiate TLS 1.2 / TLS 1.3
// --- Diagnostics & Error Logging ---
Log = msg => Console.WriteLine("[cshttp] " + msg),
OnUnhandledException = (ctx, ex) => Console.Error.WriteLine("[500] " + ctx.Request.RawUrl + ": " + ex)
};
// Trust X-Forwarded-For and X-Forwarded-Proto from a local Nginx reverse proxy:
options.TrustedProxies.Add(IPAddress.Loopback);
options.TrustedProxies.Add(IPAddress.IPv6Loopback);
// Bind HTTP on loopback (behind Nginx) or IPAddress.Any (public/LAN):
options.ListenHttp(IPAddress.Loopback, 8080);
// Optional: Bind HTTPS directly with an X509Certificate2:
// var cert = new X509Certificate2("server.pfx", "password");
// options.ListenHttps(IPAddress.Any, 8443, cert);Below is a complete, production-ready Program.cs showing how cshttp.server replaces Global.asax.cs and web.config:
using System;
using System.Net;
using cshttp;
namespace MyStandaloneApp
{
public static class Program
{
public static int Main(string[] args)
{
var options = new ServerOptions
{
ContentRoot = AppDomain.CurrentDomain.BaseDirectory,
MaxRequestBodySize = 16L * 1024 * 1024,
Log = msg => Console.WriteLine("[cshttp] " + msg),
OnUnhandledException = (ctx, ex) => Console.Error.WriteLine("[error] " + ctx.Request.RawUrl + ": " + ex.Message)
};
options.TrustedProxies.Add(IPAddress.Loopback);
options.ListenHttp(IPAddress.Loopback, 8080);
// ---- Application_Start equivalent ----
HostingEnvironment.SetApplicationPhysicalPath(options.ContentRoot);
Console.WriteLine("Listening on http://localhost:8080/ (Press Ctrl+C to stop)");
// Blocks until Ctrl+C, SIGTERM, or ProcessExit, then performs a graceful connection drain
using (var server = new HttpServer(options))
{
server.Run(BeginRequest);
}
return 0;
}
// ---- Application_BeginRequest equivalent ----
static void BeginRequest(HttpContext ctx)
{
HttpRequest Request = ctx.Request;
HttpResponse Response = ctx.Response;
Response.AppendHeader("X-Content-Type-Options", "nosniff");
Response.AppendHeader("Referrer-Policy", "same-origin");
string path = Request.Path.ToLowerInvariant().TrimEnd('/');
// Serve static files if not offloaded to Nginx
if (path.StartsWith("/css/") && StaticFiles.TryServe(ctx, "/css/", "css")) return;
if (path.StartsWith("/js/") && StaticFiles.TryServe(ctx, "/js/", "js")) return;
// Direct O(1) switch routing
switch (path)
{
case "":
case "/home":
Response.Write("<h1>Welcome to cshttp.server 1.0</h1>");
return;
case "/api/status":
Response.ContentType = "application/json";
Response.Write("{\"ok\":true}");
return;
}
Response.StatusCode = 404;
Response.ContentType = "text/plain";
Response.Write("404 Not Found");
}
}
}When server.Stop() is triggered (automatically hooked to Console.CancelKeyPress and AppDomain.CurrentDomain.ProcessExit inside server.Run()):
- Stop Listening: All listening sockets are closed immediately so the OS stops routing new TCP handshakes to the process, and the idle-sweep timer is disposed.
-
Close Idle Connections Immediately: Every connection currently in the zero-thread
_idle == truestate is closed immediately viac.CloseIfIdle(). -
Drain In-Flight Requests: Any connection currently executing inside
RunActiveWorkeris given up tooptions.ShutdownTimeout(default 30 seconds) to finish writing its HTTP response. -
Drain Background Work: Remaining time in
ShutdownTimeoutis passed toHostingEnvironment.InitiateShutdown(left)to await any tasks queued viaHostingEnvironment.QueueBackgroundWorkItem(...).
When deploying cshttp.server in production behind Nginx (the 4-Pillar Stack), enable an upstream keepalive pool so Nginx reuses persistent loopback sockets to cshttp.server:
upstream cshttp_backend {
server 127.0.0.1:8080;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name example.com;
# TLS 1.3 certificates managed by Nginx
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Optional: Serve static assets directly from Nginx via kernel sendfile
location /css/ { root /var/www/myapp; expires 365d; }
location /js/ { root /var/www/myapp; expires 365d; }
location /assets/ { root /var/www/myapp; expires 365d; }
# Forward dynamic routes over persistent HTTP/1.1 loopback connections
location / {
proxy_pass http://cshttp_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Technical Specs
14 Days Tutorial