From 85f517e337b443ca8e789236c5491eaf0299e557 Mon Sep 17 00:00:00 2001 From: Patricio Whittingslow Date: Tue, 28 Jul 2026 14:39:08 -0300 Subject: [PATCH] add README.md --- http/httphi/README.md | 51 +++++++++++++++++++++++++++++++++++++++ http/httphi/bench_test.go | 2 +- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 http/httphi/README.md diff --git a/http/httphi/README.md b/http/httphi/README.md new file mode 100644 index 0000000..1b6ac35 --- /dev/null +++ b/http/httphi/README.md @@ -0,0 +1,51 @@ +# httφ + +Heapless HTTP/1.1 router with `net/http`-shaped handlers. Benchmarked against `net/http` and friends at [**soypat/httpbench**](https://github.com/soypat/httpbench). + +## Why + +`net/http` allocates per request: `Request`, header map, response buffer, goroutine. Fine on a +server, fatal on a microcontroller. httφ pays that cost once, at `Router.Configure`: + +- Exchanges and goroutines are fixed there. Serving allocates nothing; memory does not grow with load. +- No free exchange means `Handle` refuses the connection unclosed, rather than allocating one more of everything. +- `Handle` takes an `io.ReadWriteCloser`, so the router runs over a raw socket, an [`lneto`](https://github.com/soypat/lneto) TCP stack or a test pipe. No listener, no OS, no clock. + +Parsing is [`httpraw`](../httpraw). + +## Example + +```go +var mux httphi.MuxSlice +mux.Handle("GET /", func(ex *httphi.Exchange) { + ex.WriteBody([]byte("hello world")) +}) + +var router httphi.Router +err := router.Configure(httphi.RouterConfig{ + FixedNumGoroutines: 4, // 4 workers, 4 exchanges, allocated here and never again. + MaxAwaitingConns: 8, // Queue depth. Full queue drops connections. + RequestHeaderBufferSize: 1024, + ResponseHeaderMinBufferSize: 32, // Shares the request buffer. + RequestNumHeaderKVCap: 32, + Backoff: func(uint) time.Duration { return time.Millisecond }, + Mux: &mux, +}) +if err != nil { + log.Fatal(err) +} +for { + conn, err := listener.Accept() // Accepting is the caller's job. + if err != nil { + log.Fatal(err) + } + if err = router.Handle(conn); err != nil { + conn.Close() // Refused: never blocks, never queues unboundedly. + } +} +``` + +`FixedNumGoroutines: -1` gives the unbounded flavor, a goroutine and an exchange per connection. + +Runnable server over raw Linux sockets, plus query, form and multipart handlers: +[`example_test.go`](./example_test.go). diff --git a/http/httphi/bench_test.go b/http/httphi/bench_test.go index 8d8b463..dc67e1f 100644 --- a/http/httphi/bench_test.go +++ b/http/httphi/bench_test.go @@ -125,7 +125,7 @@ func BenchmarkRequestParseForm(b *testing.B) { b.ReportAllocs() b.SetBytes(int64(len(request))) b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { conn.rewind() exch.Release() exch.Acquire(conn)