Skip to content

std.net

API summary (generated from the Beans source by npm run coverage): 6 types · 1 constructor · 8 static methods · 39 instance methods · 4 public fields.

std.net provides TCP and UDP sockets and name resolution. It is the readable layer over the raw socket syscalls in std.sock. The source is stdlib/std/net/net.b.

import std.net
  • Sockets are made with a named static, not a constructor. Construction that can fail returns a Result, so you write TcpStream.connect(...), TcpListener.bind(...), and UdpSocket.bind(...), the same shape as File.open.
  • Every socket is move-only and Send. It closes itself in deinit when it goes out of scope. Move it into a send fn worker when one thread should own the connection. close() exists for when you want to see the close error; otherwise scope exit handles it.
  • The address family is resolved, never chosen. Every entry point runs the host through getaddrinfo, so "localhost", "127.0.0.1", and "::1" all work with no family flag.
  • Reads and writes are partial by contract. read returns what has arrived and write reports what went out. An empty Bytes from read means the peer closed. write_all and read_exact loop for you.
  • Blocking calls retry on EINTR. When a configured timeout expires, the call returns an err with kind timeout.

Error kinds you may see: refused, in_use, timeout, reset, unreachable, not_found, closed, eof, invalid, permission, io.

An ordinary value: a numeric host and a port. Copy it freely.

pub class Address
new Address(host: string, port: int)
pub host: string
pub port: int
pub static fn resolve(host: string, port: int) -> Result<List<Address>>
pub fn to_string() -> string
pub fn is_ipv6() -> bool
pub fn is_loopback() -> bool
  • resolve returns every distinct numeric address a name maps to, in resolver order. A name that does not resolve is an err with kind not_found.
  • to_string prints host:port, or [host]:port for IPv6.

One received UDP message: the sender’s address and the bytes.

pub class Datagram
pub from: Address
pub data: Bytes

The transport shape shared by raw TCP and TLS. HTTP/2 and WebSocket use this interface, so the same protocol code works over either transport.

pub interface ByteStream {
fn write_all(data: Bytes) -> Result<int>
fn read(max: int) -> Result<Bytes>
fn shutdown_write() -> Result<bool>
fn close() -> Result<bool>
fn poll_handle() -> int
}

A connected TCP socket. Move-only; pass it with move, take it out of a Result with ?.

pub unique class TcpStream implements ByteStream, Send
pub static fn connect(host: string, port: int) -> Result<TcpStream>
pub static fn connect_timeout(host: string, port: int, ms: int) -> Result<TcpStream>
pub fn write(data: Bytes) -> Result<int>
pub fn write_all(data: Bytes) -> Result<int>
pub fn write_text(text: string) -> Result<int>
pub fn write_from(data: Bytes, offset: int) -> Result<int>
pub fn try_write_from(data: Bytes, offset: int) -> Result<Option<int>>
pub fn write_vectored(head: Bytes, body: Bytes, offset: int) -> Result<int>
pub fn write_vectored_text(head: Bytes, body: string, offset: int) -> Result<int>
pub fn read(max: int) -> Result<Bytes>
pub fn read_into(buffer: Bytes) -> Result<int>
pub fn read_into_waiting(buffer: Bytes) -> Result<int>
pub fn try_read_into(buffer: Bytes) -> Result<Option<int>>
pub fn read_exact(count: int) -> Result<Bytes>
pub fn read_to_end(limit: int) -> Result<Bytes>
pub fn peer_address() -> Result<Address>
pub fn local_address() -> Result<Address>
pub fn set_timeouts(read_ms: int, write_ms: int) -> Result<bool>
pub fn set_nonblocking(on: bool) -> Result<bool>
pub fn set_nodelay(on: bool) -> Result<bool>
pub fn into_raw() -> Result<int>
pub fn shutdown_write() -> Result<bool>
pub fn shutdown_read() -> Result<bool>
pub fn close() -> Result<bool>
pub fn poll_handle() -> int
  • connect waits as long as the OS does; connect_timeout gives up after ms milliseconds with kind timeout.

  • write_vectored sends two buffers as one write, without joining them first. For an HTTP response, this avoids copying the body into a buffer with the head. write_vectored_text does the same for a body held as a string.

    offset counts into the concatenation of the two buffers, so a short write resumes correctly whether it stopped inside the head or inside the body and you never have to work out which. Pass 0 first, add what came back, and call again until offset reaches head.len() + body.len(); at that point it answers ok(0).

  • write may send less than all of data and returns the count. write_all loops until everything is sent. read returns up to max bytes; an empty result means the peer closed. read_exact fails with kind eof if the peer closes before count bytes arrive. read_exact and read_to_end grow one result buffer instead of joining copied chunks. write_text sends string storage directly.

  • read_into writes into an existing non-empty Bytes and returns the number of bytes written. Zero means EOF. The buffer keeps its length and only 0..count belongs to that read, so one Bytes.filled(...) allocation can serve the whole connection.

  • read_into_waiting reads like read_into, but on a fiber it waits for readability before the first recv. A caller that just drained the socket knows the next recv would only report would-block, so this form spends one poller wait instead of that wasted syscall. Off a fiber it behaves exactly like read_into.

  • write_from writes starting at offset without slicing or copying data — the offset-aware form an output queue needs to resume a short write.

  • The try_ pair serves nonblocking streams: try_write_from and try_read_into return ok(none) when the socket would block, instead of an error. For try_read_into, ok(some(0)) is EOF, so a quiet socket and a closed peer remain different facts.

  • set_nodelay(true) disables Nagle’s algorithm (and false restores it). Disabling it can reduce delays for small request/response messages.

  • into_raw transfers the descriptor to a lower-level transport. The stream stops owning it; the new owner must close it.

  • shutdown_write sends EOF to the peer while keeping the read half open.

  • poll_handle returns the descriptor borrowed, for registering with a poller. It does not transfer ownership; do not close it.

A short request and reply over loopback:

import std.io
import std.net
fn main() {
let listener: net.TcpListener = net.TcpListener.bind("127.0.0.1", 0).expect("bind")
let port: int = listener.port().expect("port")
let client: net.TcpStream = net.TcpStream.connect("127.0.0.1", port).expect("connect")
let server: net.TcpStream = listener.accept().expect("accept")
client.write_text("ping").expect("write")
let got: Bytes = server.read(64).expect("read")
io.println(got.to_string())
client.close().expect("close")
server.close().expect("close")
}

A socket that accepts incoming TCP connections.

pub unique class TcpListener implements Send
pub static fn bind(host: string, port: int) -> Result<TcpListener>
pub static fn bind_with_backlog(host: string, port: int, depth: int) -> Result<TcpListener>
pub static fn bind_reuse_port(host: string, port: int) -> Result<TcpListener>
pub static fn bind_reuse_port_with_backlog(host: string, port: int, depth: int) -> Result<TcpListener>
pub fn accept() -> Result<TcpStream>
pub fn accept_timeout(ms: int) -> Result<TcpStream>
pub fn try_accept() -> Result<Option<TcpStream>>
pub fn local_address() -> Result<Address>
pub fn port() -> Result<int>
pub fn set_nonblocking(on: bool) -> Result<bool>
pub fn close() -> Result<bool>
pub fn poll_handle() -> int
  • bind uses a backlog of 128; bind_with_backlog sets the accept-queue depth.
  • bind_reuse_port lets independent listeners share one port. Whether that spreads load is the kernel’s decision, not the call’s. Linux hashes each connection’s four-tuple across the listening sockets, so N listeners serve N shares. macOS does not balance at all — the last socket to bind receives every connection and the rest stay idle, which is BSD behaviour, and Darwin has no equivalent of FreeBSD’s SO_REUSEPORT_LB. Use it for more cores on Linux, and for handing a port over without dropping connections anywhere; on macOS spread work by accepting on one listener and dealing the streams out to workers. Windows returns kind unsupported. The _with_backlog form also sets the queue depth.
  • Port 0 asks the system for a free port. Read it back with port(), which is how a test binds without guessing a number.
  • accept blocks until a connection arrives. accept_timeout(0) is a non-blocking check; a positive timeout that runs out is kind timeout.
  • try_accept accepts one connection without waiting: ok(none) means the accept queue is empty. This is the form a poller-driven accept loop uses after the listener’s descriptor reports readable.

A bound UDP socket. Each send is one message; each receive returns one message with the sender’s address attached.

pub unique class UdpSocket implements Send
pub static fn bind(host: string, port: int) -> Result<UdpSocket>
pub fn send_to(data: Bytes, to: Address) -> Result<int>
pub fn recv_from(max: int) -> Result<Datagram>
pub fn local_address() -> Result<Address>
pub fn port() -> Result<int>
pub fn set_timeouts(read_ms: int, write_ms: int) -> Result<bool>
pub fn set_nonblocking(on: bool) -> Result<bool>
pub fn close() -> Result<bool>
pub fn poll_handle() -> int
  • send_to reports how many bytes went. A datagram is sent whole or not at all, so a short count means the message was too large.
  • recv_from reads one datagram up to max bytes; anything past max in a single datagram is dropped by the OS, so size max to your protocol. The returned Datagram takes ownership of the received payload without joining and slicing one combined runtime buffer.

Two UDP sockets on loopback, one sending to the other:

import std.io
import std.net
fn main() {
let inbox: net.UdpSocket = net.UdpSocket.bind("127.0.0.1", 0).expect("bind")
let port: int = inbox.port().expect("port")
let sender: net.UdpSocket = net.UdpSocket.bind("127.0.0.1", 0).expect("bind")
let target: net.Address = new net.Address("127.0.0.1", port)
sender.send_to(Bytes.from("hello"), target).expect("send")
let note: net.Datagram = inbox.recv_from(64).expect("recv")
io.println("from {note.from.to_string()}: {note.data.to_string()}")
}
pub fn join_multicast(group: string) -> Result<bool>
pub fn leave_multicast(group: string) -> Result<bool>

Joins or leaves a multicast group. Joined sockets receive datagrams sent to the group. Use a numeric address, such as "239.1.2.3" or "ff02::1"; the socket must be bound to the same address family. Leaving a group that this socket has not joined returns an err from the OS.

To wait for a socket to become readable or writable without spinning, register the descriptor from poll_handle() with the std.poll poller and wait for its events.