diff --git a/README.md b/README.md
index 40e9f04..65db113 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ Userspace networking primitives.
- Zero scheduling required. No goroutines/channels use in Lneto. Can be run in event loop.
- Heapless packet processing
- [`httpraw`](https://github.com/soypat/lneto/tree/main/http/httpraw) is likely the most performant HTTP/1.1 processing package in the Go ecosystem. Based on [`fasthttp`](https://github.com/valyala/fasthttp) but simpler and more thoughtful memory use.
+ - [`httφ`](https://github.com/soypat/lneto/tree/main/http/httphi) - Heapless HTTP router and zero-copy response writing with Go's standard library API.
- Lean memory footprint
- HTTP header struct is 80 bytes with no runtime usage nor heap usage other than buffer
- Entire Ethernet+IPv4+UDP+DHCP+DNS+NTP stack in ~2kB RAM.
diff --git a/arp/stringers.go b/arp/stringers.go
index cd8d0c7..376de7a 100644
--- a/arp/stringers.go
+++ b/arp/stringers.go
@@ -17,9 +17,9 @@ const _Operation_name = "requestreply"
var _Operation_index = [...]uint8{0, 7, 12}
func (i Operation) String() string {
- i -= 1
- if i >= Operation(len(_Operation_index)-1) {
- return "Operation(" + strconv.FormatInt(int64(i+1), 10) + ")"
+ idx := int(i) - 1
+ if i < 1 || idx >= len(_Operation_index)-1 {
+ return "Operation(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _Operation_name[_Operation_index[i]:_Operation_index[i+1]]
+ return _Operation_name[_Operation_index[idx]:_Operation_index[idx+1]]
}
diff --git a/dhcp/dhcpv4/stringers.go b/dhcp/dhcpv4/stringers.go
index 2141e9e..8dcce7b 100644
--- a/dhcp/dhcpv4/stringers.go
+++ b/dhcp/dhcpv4/stringers.go
@@ -106,10 +106,11 @@ const _Op_name = "undefinedrequestreply"
var _Op_index = [...]uint8{0, 9, 16, 21}
func (i Op) String() string {
- if i >= Op(len(_Op_index)-1) {
+ idx := int(i) - 0
+ if i < 0 || idx >= len(_Op_index)-1 {
return "Op(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _Op_name[_Op_index[i]:_Op_index[i+1]]
+ return _Op_name[_Op_index[idx]:_Op_index[idx+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -131,10 +132,11 @@ const _MessageType_name = "undefineddiscoverofferrequestdeclineacknakreleaseinfo
var _MessageType_index = [...]uint8{0, 9, 17, 22, 29, 36, 39, 42, 49, 55}
func (i MessageType) String() string {
- if i >= MessageType(len(_MessageType_index)-1) {
+ idx := int(i) - 0
+ if i < 0 || idx >= len(_MessageType_index)-1 {
return "MessageType(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _MessageType_name[_MessageType_index[i]:_MessageType_index[i+1]]
+ return _MessageType_name[_MessageType_index[idx]:_MessageType_index[idx+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -155,9 +157,9 @@ const _ClientState_name = "initselectingrequestingboundrenewingrebindinginit-reb
var _ClientState_index = [...]uint8{0, 4, 13, 23, 28, 36, 45, 56, 65}
func (i ClientState) String() string {
- i -= 1
- if i >= ClientState(len(_ClientState_index)-1) {
- return "ClientState(" + strconv.FormatInt(int64(i+1), 10) + ")"
+ idx := int(i) - 1
+ if i < 1 || idx >= len(_ClientState_index)-1 {
+ return "ClientState(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _ClientState_name[_ClientState_index[i]:_ClientState_index[i+1]]
+ return _ClientState_name[_ClientState_index[idx]:_ClientState_index[idx+1]]
}
diff --git a/dhcp/dhcpv6/stringers.go b/dhcp/dhcpv6/stringers.go
index c7c29bc..0145ead 100644
--- a/dhcp/dhcpv6/stringers.go
+++ b/dhcp/dhcpv6/stringers.go
@@ -28,11 +28,11 @@ const _MsgType_name = "solicitadvertiserequestconfirmrenewrebindreplyreleasedecl
var _MsgType_index = [...]uint8{0, 7, 16, 23, 30, 35, 41, 46, 53, 60, 71, 85, 95, 105}
func (i MsgType) String() string {
- i -= 1
- if i >= MsgType(len(_MsgType_index)-1) {
- return "MsgType(" + strconv.FormatInt(int64(i+1), 10) + ")"
+ idx := int(i) - 1
+ if i < 1 || idx >= len(_MsgType_index)-1 {
+ return "MsgType(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _MsgType_name[_MsgType_index[i]:_MsgType_index[i+1]]
+ return _MsgType_name[_MsgType_index[idx]:_MsgType_index[idx+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -51,11 +51,11 @@ const _ClientState_name = "initsolicitingrequestingboundrenewingrebinding"
var _ClientState_index = [...]uint8{0, 4, 14, 24, 29, 37, 46}
func (i ClientState) String() string {
- i -= 1
- if i >= ClientState(len(_ClientState_index)-1) {
- return "ClientState(" + strconv.FormatInt(int64(i+1), 10) + ")"
+ idx := int(i) - 1
+ if i < 1 || idx >= len(_ClientState_index)-1 {
+ return "ClientState(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _ClientState_name[_ClientState_index[i]:_ClientState_index[i+1]]
+ return _ClientState_name[_ClientState_index[idx]:_ClientState_index[idx+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -134,10 +134,11 @@ const _StatusCode_name = "successunspec-failno-addrs-availno-bindingnot-on-linku
var _StatusCode_index = [...]uint8{0, 7, 18, 32, 42, 53, 66}
func (i StatusCode) String() string {
- if i >= StatusCode(len(_StatusCode_index)-1) {
+ idx := int(i) - 0
+ if i < 0 || idx >= len(_StatusCode_index)-1 {
return "StatusCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _StatusCode_name[_StatusCode_index[i]:_StatusCode_index[i+1]]
+ return _StatusCode_name[_StatusCode_index[idx]:_StatusCode_index[idx+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -153,9 +154,9 @@ const _DUIDType_name = "duid-lltduid-enduid-ll"
var _DUIDType_index = [...]uint8{0, 8, 15, 22}
func (i DUIDType) String() string {
- i -= 1
- if i >= DUIDType(len(_DUIDType_index)-1) {
- return "DUIDType(" + strconv.FormatInt(int64(i+1), 10) + ")"
+ idx := int(i) - 1
+ if i < 1 || idx >= len(_DUIDType_index)-1 {
+ return "DUIDType(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _DUIDType_name[_DUIDType_index[i]:_DUIDType_index[i+1]]
+ return _DUIDType_name[_DUIDType_index[idx]:_DUIDType_index[idx+1]]
}
diff --git a/dns/stringers.go b/dns/stringers.go
index f30e888..42f7947 100644
--- a/dns/stringers.go
+++ b/dns/stringers.go
@@ -119,10 +119,11 @@ const _RCode_name = "successformat errorserver failurename errornot implementedr
var _RCode_index = [...]uint8{0, 7, 19, 33, 43, 58, 65}
func (i RCode) String() string {
- if i >= RCode(len(_RCode_index)-1) {
+ idx := int(i) - 0
+ if i < 0 || idx >= len(_RCode_index)-1 {
return "RCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _RCode_name[_RCode_index[i]:_RCode_index[i+1]]
+ return _RCode_name[_RCode_index[idx]:_RCode_index[idx+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -138,8 +139,9 @@ const _OpCode_name = "Standard queryInverse queryServer status request"
var _OpCode_index = [...]uint8{0, 14, 27, 48}
func (i OpCode) String() string {
- if i >= OpCode(len(_OpCode_index)-1) {
+ idx := int(i) - 0
+ if i < 0 || idx >= len(_OpCode_index)-1 {
return "OpCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
- return _OpCode_name[_OpCode_index[i]:_OpCode_index[i+1]]
+ return _OpCode_name[_OpCode_index[idx]:_OpCode_index[idx+1]]
}
diff --git a/examples/berkeley-listener/berkeley_server.go b/examples/berkeley-listener/berkeley_server.go
index f3cee5f..310dffb 100644
--- a/examples/berkeley-listener/berkeley_server.go
+++ b/examples/berkeley-listener/berkeley_server.go
@@ -288,7 +288,7 @@ func handleConnNet(conn net.Conn) error {
}
}
method := string(hdr.Method())
- uri := string(hdr.RequestURI())
+ uri := string(hdr.RequestTarget())
fmt.Printf("< %s %s\n", method, uri)
var resp httpraw.Header
@@ -370,7 +370,7 @@ func mockClient(stack *xnet.StackAsync, port uint16, subnet netip.Prefix) {
var hdr httpraw.Header
hdr.SetMethod("GET")
- hdr.SetRequestURI("/")
+ hdr.SetRequestTarget("/")
hdr.SetProtocol("HTTP/1.1")
hdr.Set("Host", string(ipv4.AppendFormatAddr(nil, stack.Addr4())))
hdr.Set("User-Agent", "lneto-mock")
diff --git a/examples/http-linux/conn-linux.go b/examples/http-linux/conn-linux.go
deleted file mode 100644
index 9da6143..0000000
--- a/examples/http-linux/conn-linux.go
+++ /dev/null
@@ -1,96 +0,0 @@
-//go:build !tinygo && linux
-
-package main
-
-import (
- "net/netip"
- "syscall"
-)
-
-// Conn wraps an accepted TCP connection from a raw Linux socket file descriptor.
-// It implements io.Reader/io.Writer/io.Closer over syscall.Read/Write/Close.
-type Conn struct {
- fd int
- remote netip.AddrPort
-}
-
-// Read reads bytes from the connection into b.
-func (c *Conn) Read(b []byte) (int, error) {
- if len(b) == 0 {
- return 0, nil
- }
- n, err := syscall.Read(c.fd, b)
- if err != nil {
- return 0, err
- }
- if n == 0 {
- return 0, syscall.ECONNRESET // Peer closed.
- }
- return n, nil
-}
-
-// Write writes b to the connection, looping until all bytes are sent.
-func (c *Conn) Write(b []byte) (int, error) {
- total := 0
- for total < len(b) {
- n, err := syscall.Write(c.fd, b[total:])
- if err != nil {
- return total, err
- }
- total += n
- }
- return total, nil
-}
-
-// Close closes the underlying file descriptor.
-func (c *Conn) Close() error {
- return syscall.Close(c.fd)
-}
-
-// RemoteAddr returns the peer address of the connection.
-func (c *Conn) RemoteAddr() netip.AddrPort { return c.remote }
-
-// Listener wraps a listening TCP socket bound to a local port.
-type Listener struct {
- fd int
-}
-
-// Listen creates a listening TCP socket bound to port on all interfaces.
-func Listen(port uint16) (*Listener, error) {
- fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
- if err != nil {
- return nil, err
- }
- // Allow quick rebind after restart.
- if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil {
- syscall.Close(fd)
- return nil, err
- }
- addr := &syscall.SockaddrInet4{Port: int(port)}
- if err = syscall.Bind(fd, addr); err != nil {
- syscall.Close(fd)
- return nil, err
- }
- if err = syscall.Listen(fd, syscall.SOMAXCONN); err != nil {
- syscall.Close(fd)
- return nil, err
- }
- return &Listener{fd: fd}, nil
-}
-
-// Accept blocks until an incoming connection arrives and returns it as a Conn.
-func (l *Listener) Accept(conn *Conn) error {
- nfd, sa, err := syscall.Accept(l.fd)
- if err != nil {
- return err
- }
-
- conn.fd = nfd
- if sa4, ok := sa.(*syscall.SockaddrInet4); ok {
- conn.remote = netip.AddrPortFrom(netip.AddrFrom4(sa4.Addr), uint16(sa4.Port))
- }
- return nil
-}
-
-// Close closes the listening socket.
-func (l *Listener) Close() error { return syscall.Close(l.fd) }
diff --git a/examples/http-linux/main-httplinux.go b/examples/http-linux/main-httplinux.go
index b243fea..a4548d9 100644
--- a/examples/http-linux/main-httplinux.go
+++ b/examples/http-linux/main-httplinux.go
@@ -3,42 +3,77 @@
package main
import (
+ "log/slog"
+ "net"
"os"
"strconv"
"sync/atomic"
"time"
- "github.com/soypat/lneto/http/httpraw"
+ "github.com/soypat/lneto/http/httphi"
)
-const listenPort = 8080
+const (
+ kB = 1 << 10
+ listenPort = 8080
+ bufferSizes = 2 * kB
+ // A browser sends around twenty header fields; a request carrying more
+ // than this is answered 431 rather than parsed into memory it was not
+ // given. Each field costs 8 bytes of table.
+ numHeaderFields = 32
+ numGoroutines = 4
+ readTimeout = 2 * time.Second
+)
func main() {
if err := run(); err != nil {
- println("Error: ", err)
+ println("Error:", err.Error())
os.Exit(1)
}
println("DONE")
}
func run() error {
- ln, err := Listen(listenPort)
+ ln, err := net.Listen("tcp", ":"+strconv.Itoa(listenPort))
if err != nil {
return err
}
defer ln.Close()
- println("listening on port", listenPort)
- conn := new(Conn)
+ print("listening on http://localhost:", listenPort, "\n")
+
+ var mux httphi.MuxSlice
+ mux.Handle("GET /", homepage)
+
+ var router httphi.Router
+ err = router.Configure(httphi.RouterConfig{
+ FixedNumGoroutines: numGoroutines,
+ RequestHeaderBufferSize: bufferSizes,
+ RequestNumHeaderKVCap: numHeaderFields,
+ ResponseHeaderMinBufferSize: bufferSizes,
+ MaxAwaitingConns: 256,
+ Mux: &mux,
+ Logger: slog.Default(),
+ })
+ if err != nil {
+ return err
+ }
+ defer router.Shutdown()
+
for {
- err := ln.Accept(conn)
+ conn, err := ln.Accept()
if err != nil {
return err
}
- visits.Add(1)
- if err := handle(conn); err != nil {
- println("handle:", conn.RemoteAddr().String(), err.Error())
+ // The connection owns the idle policy: a peer that opens a socket and
+ // then stalls fails its read instead of holding a router goroutine.
+ conn.SetReadDeadline(time.Now().Add(readTimeout))
+ err = router.Handle(conn)
+ if err != nil {
+ // Every goroutine is busy and the queue is full. Dropping the
+ // connection is the backpressure: memory stays bounded.
+ slog.Warn("dropped connection", slog.String("remote", conn.RemoteAddr().String()), slog.String("err", err.Error()))
+ conn.Close()
}
- conn.Close()
}
}
@@ -50,58 +85,22 @@ const (
htmlTail = `!` +
`Sign my guestbook!` +
`