diff --git a/examples/http-linux/main-httplinux.go b/examples/http-linux/main-httplinux.go index 82e4e66..2b260da 100644 --- a/examples/http-linux/main-httplinux.go +++ b/examples/http-linux/main-httplinux.go @@ -10,7 +10,7 @@ import ( "time" "github.com/soypat/lneto/http/httphi" - "github.com/soypat/lneto/internal/rawsock" + "github.com/soypat/lneto/x/rawsock" ) const ( @@ -34,7 +34,8 @@ func main() { } func run() error { - ln, err := rawsock.Listen(listenPort) + var ln rawsock.Listener + err := ln.Listen(listenPort) if err != nil { return err } diff --git a/internal/rawsock/rawsock_linux.go b/x/rawsock/rawsock_linux.go similarity index 89% rename from internal/rawsock/rawsock_linux.go rename to x/rawsock/rawsock_linux.go index 1bcd7da..5d285f5 100644 --- a/internal/rawsock/rawsock_linux.go +++ b/x/rawsock/rawsock_linux.go @@ -17,6 +17,7 @@ import ( "net/netip" "syscall" "time" + "unsafe" ) // The interfaces this package exists to satisfy: an http.Server and a heapless @@ -173,35 +174,36 @@ type Listener struct { // Listen creates a listening TCP socket bound to port on all interfaces. A // zero port lets the kernel choose one, see [Listener.Addr]. -func Listen(port uint16) (*Listener, error) { +func (l *Listener) Listen(port uint16) error { fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP) if err != nil { - return nil, err + return 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 + return err } addr := &syscall.SockaddrInet4{Port: int(port)} if err = syscall.Bind(fd, addr); err != nil { syscall.Close(fd) - return nil, err + return err } if err = syscall.Listen(fd, syscall.SOMAXCONN); err != nil { syscall.Close(fd) - return nil, err + return err } - l := &Listener{fd: fd} + l.fd = fd + l.local = Addr{} bound, err := syscall.Getsockname(fd) if err != nil { syscall.Close(fd) - return nil, err + return err } if sa4, ok := bound.(*syscall.SockaddrInet4); ok { l.local = Addr(netip.AddrPortFrom(netip.AddrFrom4(sa4.Addr), uint16(sa4.Port))) } - return l, nil + return nil } // Accept blocks until an incoming connection arrives and returns it. It @@ -218,14 +220,25 @@ func (l *Listener) Accept() (net.Conn, error) { // AcceptConn blocks until an incoming connection arrives and stores it in conn, // reusing whatever conn already held. Nothing is allocated. +// +// The syscall is made by hand because [syscall.Accept] allocates the +// [syscall.Sockaddr] it returns, one per accepted connection: the kernel is +// given address storage this call owns instead, and the address is read out of +// it into conn. func (l *Listener) AcceptConn(conn *Conn) error { - nfd, sa, err := syscall.Accept(l.fd) - if err != nil { - return err + var rsa syscall.RawSockaddrAny + salen := uint32(unsafe.Sizeof(rsa)) + nfd, _, errno := syscall.Syscall6(syscall.SYS_ACCEPT4, uintptr(l.fd), + uintptr(unsafe.Pointer(&rsa)), uintptr(unsafe.Pointer(&salen)), 0, 0, 0) + if errno != 0 { + return errno } - *conn = Conn{fd: nfd, local: l.local} - if sa4, ok := sa.(*syscall.SockaddrInet4); ok { - conn.remote = Addr(netip.AddrPortFrom(netip.AddrFrom4(sa4.Addr), uint16(sa4.Port))) + *conn = Conn{fd: int(nfd), local: l.local} + if rsa.Addr.Family == syscall.AF_INET { + sa4 := (*syscall.RawSockaddrInet4)(unsafe.Pointer(&rsa)) + // Port is in network byte order in the sockaddr the kernel filled. + port := uint16(sa4.Port<<8) | uint16(sa4.Port>>8) + conn.remote = Addr(netip.AddrPortFrom(netip.AddrFrom4(sa4.Addr), port)) } return nil } diff --git a/x/rawsock/rawsock_linux_test.go b/x/rawsock/rawsock_linux_test.go new file mode 100644 index 0000000..0bfc93f --- /dev/null +++ b/x/rawsock/rawsock_linux_test.go @@ -0,0 +1,64 @@ +//go:build !tinygo && linux + +package rawsock + +import ( + "net" + "runtime" + "testing" +) + +// TestAcceptConnDoesNotAllocate pins down what AcceptConn exists for. A server +// that owns its connection storage still pays an allocation per connection if +// accepting one allocates, which is what syscall.Accept does with the peer +// address it returns. +func TestAcceptConnDoesNotAllocate(t *testing.T) { + var ln Listener + err := ln.Listen(0) + if err != nil { + t.Fatal(err) + } + defer ln.Close() + addr := ln.Addr().String() + + const n = 32 + dialed := make([]net.Conn, 0, n) + defer func() { + for _, c := range dialed { + c.Close() + } + }() + for i := 0; i < n; i++ { + c, err := net.Dial("tcp", addr) + if err != nil { + t.Fatal(err) + } + dialed = append(dialed, c) + } + + var conn Conn + // The first accept warms whatever the runtime wants to warm, and is where + // the peer address is checked: reading it hands a value to a net.Addr + // interface, which allocates whatever the accept did. + if err = ln.AcceptConn(&conn); err != nil { + t.Fatal(err) + } + if conn.RemoteAddr().String() != dialed[0].LocalAddr().String() { + t.Errorf("accepted peer %s, dialer says %s", conn.RemoteAddr(), dialed[0].LocalAddr()) + } + conn.Close() + + var before, after runtime.MemStats + runtime.GC() + runtime.ReadMemStats(&before) + for i := 1; i < n; i++ { + if err = ln.AcceptConn(&conn); err != nil { + t.Fatal(err) + } + conn.Close() + } + runtime.ReadMemStats(&after) + if allocs := after.Mallocs - before.Mallocs; allocs != 0 { + t.Errorf("AcceptConn allocated %d times over %d accepts, want 0", allocs, n-1) + } +} diff --git a/internal/rawsock/rawsock_other.go b/x/rawsock/rawsock_other.go similarity index 100% rename from internal/rawsock/rawsock_other.go rename to x/rawsock/rawsock_other.go