std/net
TCP sockets and DNS resolution. The TLS socket (TlsStream) and the HTTP client live in std/fetch, kept out of std/net so a plain-TCP program links without OpenSSL.
from "std/net" import { resolve, TcpStream, TcpListener, NetError }Example
A client, speaking just enough HTTP to show a round trip:
let ip = resolve("example.com")!
let conn = TcpStream.connect(ip, 80)!
let _ = conn.send("HEAD / HTTP/1.0\r\nHost: example.com\r\n\r\n")!
print(conn.recv()!)A server binds a port (0 asks the OS for a free one) and accepts connections. Inside a green task accept parks instead of blocking the thread:
let listener = TcpListener.bind(0)!
print($"listening on {listener.port()}")API reference
IpAddr
pub enum IpAddrAn address of either family, as resolveAll returns it. V4 holds the address as ip4 builds it; V6 holds the 16 raw bytes (as ip6 parses them) and the scope id, the interface index a link-local peer (fe80::/10) needs, 0 otherwise.
Variants:
V4(u32)V6([u8; 16], u32)
IpAddr.toString
fn IpAddr.toString(self: &IpAddr): stringText form: a dotted quad, or RFC 5952 for v6 (lowercase hex, the longest run of two or more zero groups as ::), with %scope when the scope id is not 0.
NetError
pub enum NetErrorWhy a network operation failed. The payload is a short description; for DnsFailure it is the hostname.
Variants:
DnsFailure(string)ConnectionFailed(string)TlsError(string)SendFailed(string)Timeout(string): A deadline passed; the payload names the stage that was waiting (std/fetch's fetchWithTimeout: "connect", "tls handshake", "send", "response").TooLarge(string): A response body past the cap (std/fetch's FetchOptions.maxBodyBytes); the payload says which limit was hit. The body is dropped, never returned cut short.Other(string)
NetError.message
fn NetError.message(self: &NetError): stringWhat went wrong, prefixed by the stage that failed. Makes NetError an Error, so ? turns it into a string error or boxes it as Heap<Error>.
TcpListener
pub struct TcpListenerListening TCP socket. Accepts inbound connections; closes the fd on drop. Server counterpart to TcpStream.connect — no raw syscalls required in user code.
TcpListener.accept
fn TcpListener.accept(self: &TcpListener): Result<TcpStream, NetError>Accept one connection. Inside the green scheduler this yields (via the event loop) until a client is ready rather than blocking the whole runtime; outside it, it blocks.
TcpListener.acceptNb
fn TcpListener.acceptNb(self: &TcpListener): Result<TcpStream, NetError>Accept without parking: Ok(stream) only when a connection is already pending, Err otherwise. For callers that multiplex accept with other event sources — accept() parks the calling task on the listener, which stalls everything else that task drives until a client connects.
TcpListener.addr4
fn TcpListener.addr4(self: &TcpListener): Option<u32>The bound IPv4 address, or None for an IPv6 socket (or a failed getsockname).
TcpListener.bind
fn TcpListener.bind(port: u16): Result<TcpListener, NetError>Bind to 0.0.0.0:port (every interface) and start listening. SO_REUSEADDR is set so a quick restart doesn't fail with "address already in use". Pass port 0 to let the OS choose a free port (recover it via the accepted peer or getsockname).
TcpListener.bind6
fn TcpListener.bind6(addr: [u8; 16], port: u16): Result<TcpListener, NetError>Bind an IPv6 listener. Added alongside bind(); pass the 16 raw bytes (ip6("::1"), or all-zero for the v6 wildcard ":😊.
TcpListener.bindAddr
fn TcpListener.bindAddr(addr: u32, port: u16): Result<TcpListener, NetError>Bind to one IPv4 address (see ip4). bindAddr(ip4(127, 0, 0, 1), port) is the loopback-only listener a local dev server wants: bind takes every interface, so anything it serves is reachable from the network.
Binds exactly addr:port. Started under systemd socket activation for this process, it errs naming inheritedOr, the constructor that adopts the passed socket.
TcpListener.inheritedOr
fn TcpListener.inheritedOr(addr: u32, port: u16): Result<TcpListener, NetError>The socket systemd socket activation passed this process (LISTEN_FDS/LISTEN_PID, the first descriptor), or, when it was not started that way, a fresh bind of addr:port like bindAddr. The opt-in a socket-activated service writes at the call site; isInherited() says which happened, and the unit file, not addr/port, decides where an inherited socket listens.
TcpListener.isInherited
fn TcpListener.isInherited(self: &TcpListener): boolWhether this listener came from socket activation (inheritedOr) rather than a bind.
TcpListener.port
fn TcpListener.port(self: &TcpListener): i32The bound port — the point of bind(0), which asks the OS to pick one. Without this a caller that binds 0 has no way to learn what it got, so every test and every ephemeral server has to hardcode a port and hope it is free. TlsListener has had this; TcpListener did not.
-1 if getsockname fails, mirroring getSockPort rather than reporting a plausible-looking port number.
TcpStream
pub struct TcpStreamTCP connection handle. Automatically closes the fd when dropped.
TcpStream.close
fn TcpStream.close(self: &mut TcpStream): voidClose the socket now rather than at drop: the peer sees EOF here. Afterwards the stream is inert and its drop is a no-op.
TcpStream.connect
fn TcpStream.connect(ip: u32, port: u16): Result<TcpStream, NetError>Open a TCP connection to an IPv4 address (see ip4, resolve).
TcpStream.connect6
fn TcpStream.connect6(addr: [u8; 16], port: u16, scopeId: u32): Result<TcpStream, NetError>IPv6 connect. Added alongside connect() rather than replacing it: a u32 cannot hold a 128-bit address, so the v4 entry point keeps its shape. scopeId is the interface index for a link-local peer (fe80::/10), 0 otherwise.
TcpStream.connect6Wait
fn TcpStream.connect6Wait(addr: [u8; 16], port: u16, scopeId: u32, wait: (i32, bool) => bool): Result<TcpStream, NetError>connectWait for IPv6 (see connect6).
TcpStream.connectAddr
fn TcpStream.connectAddr(addr: &IpAddr, port: u16): Result<TcpStream, NetError>Connect to an address of either family (see resolveAll).
TcpStream.connectAddrWait
fn TcpStream.connectAddrWait(addr: &IpAddr, port: u16, wait: (i32, bool) => bool): Result<TcpStream, NetError>connectAddr whose in-progress connect waits through wait, as connectWait does.
TcpStream.connectHost
fn TcpStream.connectHost(hostname: &string, port: u16): Result<TcpStream, NetError>Connect to hostname, trying every address resolveAll returns, in order, until one accepts. A name that resolves to both families reaches a server listening on only one of them, which connect(resolve(host)!, port) cannot (it only tries the first IPv4 address). The error is the last attempt's.
TcpStream.connectWait
fn TcpStream.connectWait(ip: u32, port: u16, wait: (i32, bool) => bool): Result<TcpStream, NetError>connect() whose in-progress connect waits through wait(fd, forRead), which returns false to give up (std/fetch passes a deadline): Err(Timeout("connect")). The stream is left nonblocking, so read and write it with recvFdWait/sendFdWait.
TcpStream.incoming
fn TcpStream.incoming(self: &TcpStream): Channel<string>Stream inbound bytes as an iterable channel, pumped on a background green task — the uniform async-read API shared with pty/child/pipe. Iterate with for chunk in stream.incoming(); the channel closes when the peer does. (Plaintext only — TlsStream needs an SSL-aware pump.)
TcpStream.recv
fn TcpStream.recv(self: &TcpStream): Result<string, NetError>Read everything until the peer closes, as one string (blocks to EOF). Prefer incoming() for streaming/incremental consumption — it delivers chunks as they arrive instead of buffering the whole response.
TcpStream.recvOnce
fn TcpStream.recvOnce(self: &TcpStream): stringA single read of whatever is currently available, unlike recv() which loops until the peer closes. A keep-alive HTTP client never closes its write half while waiting for the response, so recv() would deadlock; this returns after one segment — enough for a request's headers.
TcpStream.send
fn TcpStream.send(self: &TcpStream, data: &string): Result<i64, NetError>Send data. Returns the number of bytes sent.
TcpStream.stream
fn TcpStream.stream(self: &TcpStream): FdStreamA non-owning std/io Reader+Writer view of this socket, so BufReader and BufWriter work over TCP. Borrowing rather than consuming the stream is the point: a request/response protocol needs to read and write the same socket, and a wrapper that owned it could only do one. The TcpStream must outlive the view — it still owns the fd and closes it on drop, and nothing checks that, so TcpStream.connect(...)!.stream() on a temporary is wrong. Bind the stream to a name first; see FdStream in std/io.
TcpStream.take
fn TcpStream.take(self: &mut TcpStream): i32Release the underlying fd to the caller. After this the stream no longer closes it on drop, and the number is the caller's to close exactly once. @unsafe because nothing tracks it from here; wsAccept and TlsStream.fromTcp take the stream itself, which is the safe hand-off.
Functions
activationPending
pub fn activationPending(): boolWhether systemd socket activation addressed THIS process: LISTEN_PID is our pid and LISTEN_FDS is at least 1, on a target that inherits descriptors, outside replay. Reads without consuming, so a plain bind can refuse instead of silently binding a second socket beside the one the unit file configured (the old behaviour, the reverse mistake, was to adopt it and ignore the address the caller passed).
activationRefusal
pub fn activationRefusal(what: &string): NetErrorThe error a plain bind returns while activation is pending, naming the opt-in.
deadlineIn
pub fn deadlineIn(ms: i64): i64A deadline ms from now as waitIo wants it: epoch ms, 0 for no limit. The clock is the unrecorded one for the same replay reason as in waitIo.
ignoreSigpipe
pub fn ignoreSigpipe(): voidip4
pub fn ip4(a: u8, b: u8, c: u8, d: u8): u32Construct an IPv4 address from four octets. Example: ip4(127, 0, 0, 1) for localhost.
ip6
pub fn ip6(text: &string): Option<[u8; 16]>Parse an IPv6 literal ("::1", "2001:db8::1") into its 16 raw bytes. None if the text isn't a valid v6 address — inet_pton is strict, and a v4 literal like "127.0.0.1" is NOT auto-mapped, so it returns None here rather than a v4-mapped address.
resolve
pub fn resolve(hostname: &string): Result<u32, NetError>DNS lookup: the first IPv4 address for hostname. Recorded as net.resolve. On a green task the lookup (getaddrinfo, which blocks) runs on a worker OS thread and parks only the calling task, so other tasks, and other lookups, keep running.
resolveAll
pub fn resolveAll(hostname: &string): Result<Vec<IpAddr>, NetError>DNS lookup: every address for hostname, IPv4 and IPv6, in the order getaddrinfo returns them. That order is RFC 6724's (which family and which address to try first for this host's routes), so a client connects by trying them front to back, as TcpStream.connectHost does. Never an empty list: no address is Err(DnsFailure). Recorded as net.resolveall. Off the scheduler thread on a green task, as resolve is.
resolveAllBy
pub fn resolveAllBy(hostname: &string, by: i64): Result<Vec<IpAddr>, NetError>resolveAll that stops waiting at by (epoch ms; 0 waits as long as it takes) with Err(Timeout("dns")). getaddrinfo cannot be cancelled, so a lookup past its deadline is abandoned: its thread finishes on its own and the result is dropped. The bound applies on a green task, where the lookup runs on a worker; off one (and under record/replay) the call is the plain blocking lookup.
takeInheritedListenFd
pub fn takeInheritedListenFd(): i32Socket activation (systemd's LISTEN_FDS protocol): the already-bound, already-listening socket the service manager passes in, or -1 when this process was not started that way. LISTEN_PID must name THIS process (the variables also reach children, which must not claim a socket meant for their parent). The variables are removed once read, as systemd asks, so a child we spawn never mistakes them for its own.
Only the opt-in constructors call this (TcpListener.inheritedOr, TlsListener.inheritedOr, and the serve variants through ServeOptions.socketActivation): a plain bind binds what it is told, and refuses to start while an activation meant for this process is pending (activationPending). Only the first listener of the process gets the socket: the variables are gone afterwards. Skipped under record/replay (descriptors are synthetic there) and on targets with no inheritance (Windows).
@unsafe: the caller takes ownership of the returned number and must wrap it in exactly one handle (TcpListener, TlsListener) that closes it.
waitIo
pub fn waitIo(fd: i32, forRead: bool, by: i64): boolWait for fd to be readable (or writable) until by (epoch ms; 0 waits as long as it takes). False when by came first.
Not a trace record of its own: the wait parks on a Select, and the scheduler poll that ends it is already one (sched.pick says whether the fd arm or the timer arm fired), so a replay times out exactly where the recording did, without waiting. A record wrapped around the wait came out AFTER the sched records written inside it, and the replay, asking for the wrapper first, diverged at the first wait.