|
1 | 1 | # Yllibed HttpServer |
2 | 2 |
|
3 | | -This is a versatile http server designed to be used in mobile/UWP applications and any applications which need to expose a simple web server. |
| 3 | + |
4 | 4 |
|
5 | | -## Packages and NuGet Statistics |
| 5 | +A small, self-contained HTTP server for desktop, mobile, and embedded apps that need to expose a simple web endpoint. |
6 | 6 |
|
7 | | -| Package | Downloads | Stable Version | Pre-release Version | |
8 | | -|-------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------| |
9 | | -| [**HttpServer**](https://www.nuget.org/packages/Yllibed.HttpServer/) |  |  |  | |
10 | | -| [**HttpServer.Json**](https://www.nuget.org/packages/Yllibed.HttpServer.Json/) |  |  |  | |
| 7 | +- Lightweight, no ASP.NET dependency |
| 8 | +- Great for OAuth2 redirect URIs, diagnostics, and local tooling |
| 9 | +- IPv4/IPv6, HTTP/1.1, custom handlers, static files, and SSE |
11 | 10 |
|
12 | | -## Quick start-up |
| 11 | +--- |
| 12 | + |
| 13 | +## Packages and NuGet |
| 14 | + |
| 15 | +| Package | Downloads | Stable | Pre-release | |
| 16 | +|---|---|---|---| |
| 17 | +| [Yllibed.HttpServer](https://www.nuget.org/packages/Yllibed.HttpServer/) |  |  |  | |
| 18 | +| [Yllibed.HttpServer.Json](https://www.nuget.org/packages/Yllibed.HttpServer.Json/) |  |  |  | |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## Quick start |
13 | 23 |
|
14 | 24 | 1. First install nuget package: |
15 | 25 | ```shell |
@@ -49,36 +59,44 @@ This is a versatile http server designed to be used in mobile/UWP applications a |
49 | 59 | ``` |
50 | 60 |
|
51 | 61 | ## What it is |
52 | | -* Simple web server which can be extended using custom code |
53 | | -* No dependencies on ASP.NET or other frameworks, self-contained |
| 62 | + |
| 63 | +- Simple web server that can be extended with custom code |
| 64 | +- No dependencies on ASP.NET or other frameworks; fully self-contained |
| 65 | +- Intended for small apps and utilities (e.g., OAuth2 redirect URL from an external browser) |
54 | 66 |
|
55 | 67 | ## What it is not |
56 | | -* This HTTP server is not designed for performance or high capacity |
57 | | -* It's perfect for small applications, or small need, like to act as _return url_ for OAuth2 authentication using external browser. |
| 68 | + |
| 69 | +- NOT designed for high performance or high concurrency |
| 70 | +- NOT appropriate for public-facing web services |
| 71 | +- NOT a full-featured web framework (no MVC, no Razor, no routing, etc.) |
| 72 | +- NOT a replacement for ASP.NET Core or Kestrel |
58 | 73 |
|
59 | 74 | ## Features |
60 | | -* Simple, lightweight, self-contained HTTP server |
61 | | -* Supports IPv4 and IPv6 |
62 | | -* Supports HTTP 1.1 (limited: no keep-alive, no chunked encoding) |
63 | | -* Supports GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE, PATCH - even custom methods |
64 | | -* Supports static files |
65 | | -* Supports custom headers |
66 | | -* Supports custom status codes |
67 | | -* Supports custom content types |
68 | | -* Supports custom content encodings |
69 | | -* Supports dependency injection and configuration via `IOptions<ServerOptions>` |
70 | | -* Configurable bind addresses and hostnames for IPv4/IPv6 |
71 | | -* Supports dynamic port assignment |
| 75 | + |
| 76 | +- Simple, lightweight, self-contained HTTP server |
| 77 | +- Supports IPv4 and IPv6 |
| 78 | +- Supports HTTP 1.1 (limited: no keep-alive, no chunked encoding) |
| 79 | +- Allows any HTTP method (GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE, PATCH, custom). Handlers decide how to handle them. |
| 80 | +- Simple static responses via StaticHandler (no built-in file/directory serving) |
| 81 | +- Supports custom headers |
| 82 | +- Supports custom status codes |
| 83 | +- Supports custom content types |
| 84 | +- Arbitrary response headers (incl. Content-Encoding); no automatic compression/encoding |
| 85 | +- Supports dependency injection and configuration via `IOptions<ServerOptions>` |
| 86 | +- Configurable bind addresses and hostnames for IPv4/IPv6 |
| 87 | +- Supports dynamic port assignment |
72 | 88 |
|
73 | 89 | ## Common use cases |
74 | | -* Return URL for OAuth2 authentication using external browser |
75 | | -* Remote diagnostics/monitoring on your app |
76 | | -* Building a headless Windows IoT app (for SSDP discovery or simply end-user configuration) |
77 | | -* Any other use case where you need to expose a simple web server |
| 90 | + |
| 91 | +- Return URL for OAuth2 authentication using external browser |
| 92 | +- Remote diagnostics/monitoring on your app |
| 93 | +- Building a headless Windows IoT app (for SSDP discovery or simply end-user configuration) |
| 94 | +- Any other use case where you need to expose a simple web server |
78 | 95 |
|
79 | 96 | ## Limitations |
80 | | -* There is no support for HTTP 2.0+ (yet) or WebSockets |
81 | | -* There is no support for HTTPS (TLS) |
| 97 | + |
| 98 | +- There is no support for HTTP/2+ (yet) or WebSockets |
| 99 | +- There is no support for HTTPS (TLS) |
82 | 100 |
|
83 | 101 | ## Security and Intended Use (No TLS) |
84 | 102 | This server uses plain HTTP with no transport encryption. It is primarily intended for: |
@@ -260,3 +278,106 @@ var serverOptions = new ServerOptions |
260 | 278 | Hostname6 = "::1" // IPv6 loopback |
261 | 279 | }; |
262 | 280 | ``` |
| 281 | + |
| 282 | + |
| 283 | +## Server-Sent Events (SSE) |
| 284 | +SSE lets your server push a continuous stream of text events over a single HTTP response. This project now provides a minimal SSE path without chunked encoding: headers are sent, then the connection stays open while your code writes events; closing the connection ends the stream. |
| 285 | + |
| 286 | +- Content-Type: text/event-stream |
| 287 | +- Cache-Control: no-cache is added by default |
| 288 | +- Connection: close is still set by the server; the connection remains open until your writer completes |
| 289 | + |
| 290 | +Quick example (application code): |
| 291 | + |
| 292 | +```csharp |
| 293 | +// Register a handler for /sse (very basic example) |
| 294 | +public sealed class SseDemoHandler : IHttpHandler |
| 295 | +{ |
| 296 | + public Task HandleRequest(CancellationToken ct, IHttpServerRequest request, string relativePath) |
| 297 | + { |
| 298 | + if (!string.Equals(request.Method, "GET", StringComparison.OrdinalIgnoreCase)) return Task.CompletedTask; |
| 299 | + if (!string.Equals(relativePath, "/sse", StringComparison.Ordinal)) return Task.CompletedTask; |
| 300 | + |
| 301 | + request.StartSseSession(RunSseSession, |
| 302 | + headers: new Dictionary<string, IReadOnlyCollection<string>> |
| 303 | + { |
| 304 | + ["Access-Control-Allow-Origin"] = new[] { "*" } // if you need CORS |
| 305 | + }, |
| 306 | + options: new SseOptions |
| 307 | + { |
| 308 | + HeartbeatInterval = TimeSpan.FromSeconds(30), |
| 309 | + HeartbeatComment = "keepalive", |
| 310 | + AutoFlush = true |
| 311 | + }); |
| 312 | + return Task.CompletedTask; |
| 313 | + } |
| 314 | + |
| 315 | + private async Task RunSseSession(ISseSession sse, CancellationToken ct) |
| 316 | + { |
| 317 | + // Optional: initial comment |
| 318 | + await sse.SendCommentAsync("start", ct); |
| 319 | + |
| 320 | + var i = 0; |
| 321 | + while (!ct.IsCancellationRequested && i < 10) |
| 322 | + { |
| 323 | + // Write an event every second |
| 324 | + await sse.SendEventAsync($"{DateTimeOffset.UtcNow:O}", eventName: "tick", id: i.ToString(), ct: ct); |
| 325 | + await Task.Delay(TimeSpan.FromSeconds(1), ct); |
| 326 | + i++; |
| 327 | + } |
| 328 | + } |
| 329 | +} |
| 330 | + |
| 331 | +// Usage during startup |
| 332 | +var server = new Server(); |
| 333 | +var ssePath = new RelativePathHandler("/updates"); |
| 334 | +ssePath.RegisterHandler(new SseDemoHandler()); |
| 335 | +server.RegisterHandler(ssePath); |
| 336 | +var (uri4, _) = server.Start(); |
| 337 | +Console.WriteLine($"SSE endpoint: {uri4}/updates/sse"); |
| 338 | +``` |
| 339 | + |
| 340 | +SseHandler convenience base class: |
| 341 | +```csharp |
| 342 | +public sealed class MySseHandler : SseHandler |
| 343 | +{ |
| 344 | + protected override bool ShouldHandle(IHttpServerRequest request, string relativePath) |
| 345 | + => base.ShouldHandle(request, relativePath) && relativePath is "/sse"; |
| 346 | + |
| 347 | + protected override Task HandleSseSession(ISseSession sse, CancellationToken ct) |
| 348 | + => RunSseSession(sse, ct); // Reuse the same private method as above |
| 349 | +} |
| 350 | + |
| 351 | +// Registration |
| 352 | +var server = new Server(); |
| 353 | +var ssePath = new RelativePathHandler("/updates"); |
| 354 | +ssePath.RegisterHandler(new MySseHandler()); |
| 355 | +server.RegisterHandler(ssePath); |
| 356 | +``` |
| 357 | + |
| 358 | +Client-side (browser): |
| 359 | +```html |
| 360 | +<script> |
| 361 | + const es = new EventSource('/updates/sse'); |
| 362 | + es.addEventListener('tick', e => console.log('tick', e.data)); |
| 363 | + es.onmessage = e => console.log('message', e.data); |
| 364 | + es.onerror = e => console.warn('SSE error', e); |
| 365 | +</script> |
| 366 | +``` |
| 367 | + |
| 368 | +Notes: |
| 369 | +- Heartbeats: send a comment frame (`: keepalive\n\n`) every 15–30s to prevent proxy timeouts. |
| 370 | +- Long-running streams: handle CancellationToken to stop cleanly when the client disconnects. |
| 371 | +- Browser connection limits: most browsers cap concurrent HTTP connections per hostname (often 6–15). Without HTTP/2 multiplexing, a single client cannot keep many SSE connections in parallel; this server is not intended for a large number of per-client connections. |
| 372 | +- Public exposure: there is no TLS; prefer localhost or internal networks, or place behind a TLS-terminating reverse proxy. |
| 373 | + |
| 374 | + |
| 375 | +### SSE Spec and Interop Notes |
| 376 | +- Accept negotiation: If a client sends an Accept header that explicitly excludes SSE (text/event-stream), the default SseHandler will reply 406 Not Acceptable. The following values are considered acceptable: text/event-stream, text/*, or */*. If no Accept header is present, requests are accepted. You can override this behavior by overriding ValidateHeaders in your handler (ShouldHandle is for method/path filtering). |
| 377 | +- Last-Event-ID: When a client reconnects, browsers may send a Last-Event-ID header. It is exposed via ISseSession.LastEventId so you can resume from the last delivered event. Set the id parameter in SendEventAsync to help clients keep position. |
| 378 | +- Heartbeats: You can configure periodic comment frames via SseOptions.HeartBeatInterval; this keeps intermediaries from timing out idle connections. |
| 379 | +- Framing: The server uses CRLF (\r\n) in headers and LF (\n) in the SSE body as recommended by typical SSE implementations. Data payloads are normalized to LF before framing each data: line. Each event ends with a blank line. |
| 380 | +- Connection and length: The server does not send Content-Length for streaming SSE responses and relies on connection close to delimit the body (HTTP/1.1 close-delimited). The response header includes Connection: close. |
| 381 | +- Caching: Cache-Control: no-cache is added by default for SSE responses unless you override it via headers. |
| 382 | +- Retry: The SSE spec allows the server to send a retry: <milliseconds> field to suggest a reconnection delay. This helper does not currently provide a dedicated API for retry frames. Most clients also implement their own backoff. If you need this, you can write raw lines through a custom handler or open an issue. |
| 383 | +- CORS: If you need cross-origin access, add appropriate headers (e.g., Access-Control-Allow-Origin) via the headers parameter when starting the SSE session. |
0 commit comments