mirror of
https://github.com/soypat/lneto.git
synced 2026-08-15 12:23:44 +00:00
add http/httphi (#171)
* add http/httphi * begin adding httphi tests * claude found neat bugs * add low level Handle function and more tests * more tests, run go generate * add Hijacker-like functionality * improve locking and acquisition of Exchanges in reconfiguring * several bugfixes, add internal.IntLen, round up http-linux example with new router API * small nit * add benchmarks * add query handling * remove ForEach pattern, allocates in TinyGo * massive documentation push and code reordering in files * Router.Handle returns error after being torn down * run go fix * rework Mux interface to receive a string request path * add MethodFrom * minor doc nit * fail on incomplete staging * add raw buffer access * add streaming API distinct from Exchange * begin adding multipart form logic * finish rounding up multipart form parsing * remove status type * first Multipart approach * begin adding readMultiPart * add Exchange.ReadMultiparts reimagining of clanker slop * ai insists with backoffs * simplify clanker slop * apply go fix * add a pattern argument to Mux * explicit header key/value alloc and add ExchangeConfig * fix tests after excplicit header alloc change * fix examples * run go fix * expose rawsock as experimental package (will use for external benchmarks) * remove backoff from form parsing * @MDr164 suggestions get potential fixes * apply go fix * add examples * add README.md * fix rawsock tinygo implementation * apply @MDr164 various fixes * update documentation on ContentLength methods and fix bug in Form reset on empty body * fix tests * add fuzz tests * run go fix * io.ErrNoProgress on parsing form spin * run go fix * remove backoff assumption from Router * httphi.Handle rejects unsupported protocols * go format router.go * add kvbuffer * rewrite Cookie with KVBuffer * mid refactor of KVBuffer into Header * work on KVBuffer exhausted semantics * add Go's ServeMux Request.PathValue access semantics to Exchange, Mux and MuxSlice * add PathValue example * document all the things; improve req Query semantics; add Form.EnableBufferGrowth * unexport kvBuffer * add Exchange.PathValueAppend * use stdlib in example instead of rawsock * remove rawsock from http example * add darwin arch rawsock * fix example * rename Router.TeardownGoroutines to Shutdown matching http.Server.Shutdown * rename types and identifiers * @MDr164 Content-Type and Transfer-Encoding bug catches
This commit is contained in:
committed by
Patricio Whittingslow
parent
a3f2742abf
commit
5c54030f19
@@ -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) }
|
||||
@@ -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 = `!</blink></h1>` +
|
||||
`<font color="#FF00FF">Sign my guestbook!</font>` +
|
||||
`<br><hr>Best viewed in Netscape Navigator</center></body></html>`
|
||||
// maxPage bounds the rendered page: both halves plus the visitor number.
|
||||
maxPage = len(htmlHead) + 20 + len(htmlTail)
|
||||
)
|
||||
|
||||
const maxHTTPHeader = 1024
|
||||
// visits counts served requests. Handlers run on the router's goroutines, so
|
||||
// every visitor gets their own number.
|
||||
var visits atomic.Uint64
|
||||
|
||||
var (
|
||||
hdr httpraw.Header
|
||||
httpbuf [maxHTTPHeader]byte
|
||||
htmlbuf [512]byte
|
||||
visits atomic.Uint64
|
||||
)
|
||||
func homepage(exch *httphi.Exchange) {
|
||||
var page [maxPage]byte
|
||||
n := copy(page[:], htmlHead)
|
||||
n += len(strconv.AppendUint(page[n:n], visits.Add(1), 10))
|
||||
n += copy(page[n:], htmlTail)
|
||||
|
||||
func handle(conn *Conn) error {
|
||||
hdr.Reset(httpbuf[:0])
|
||||
hdr.EnableBufferGrowth(false) // Limit memory to buffer capacity.
|
||||
const incomingIsResponse = false // We get HTTP requests from clients.
|
||||
deadline := time.Now().Add(50 * time.Millisecond)
|
||||
for time.Until(deadline) > 0 {
|
||||
if _, err := hdr.ReadFromLimited(conn, maxHTTPHeader); err != nil {
|
||||
return err
|
||||
}
|
||||
needmoredata, err := hdr.TryParse(incomingIsResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if needmoredata {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
println("\n\n================\n\n", hdr.String())
|
||||
if time.Since(deadline) > 0 {
|
||||
print("DEADLINE EXCEED: ", hdr.BufferParsed(), "/", hdr.BufferReceived(), " bytes parsed/read\n")
|
||||
return nil
|
||||
}
|
||||
// Prepare tacky HTML response.
|
||||
n := copy(htmlbuf[:], htmlHead)
|
||||
n += len(strconv.AppendUint(htmlbuf[n:n], visits.Load(), 10))
|
||||
n += copy(htmlbuf[n:], htmlTail)
|
||||
contentLen := n
|
||||
hdr.Reset(httpbuf[:0])
|
||||
hdr.SetProtocol("HTTP/1.1")
|
||||
hdr.SetStatus("200", "OK")
|
||||
hdr.Set("Content-Type", "text/html")
|
||||
hdr.SetInt("Content-Length", int64(contentLen), 10)
|
||||
// Here we do some buffer juggling. We use remaining space
|
||||
// of HTTP Header buffer to write the response that will be written over the wire.
|
||||
respbuf := httpbuf[hdr.BufferUsed():]
|
||||
header, err := hdr.AppendResponse(respbuf[:0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn.Write(header)
|
||||
_, err = conn.Write(htmlbuf[:contentLen])
|
||||
return err
|
||||
exch.StageHeader("Content-Type", "text/html")
|
||||
exch.StageHeaderInt("Content-Length", int64(n), 10)
|
||||
exch.WriteHeader(int(httphi.StatusOK))
|
||||
exch.WriteBody(page[:n])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user