Networking
The net module provides production-grade networking with non-blocking I/O, async futures, and full socket option control.
Socket types
Section titled “Socket types”| Type | Description |
|---|---|
TcpListener | Accept incoming TCP connections |
TcpStream | Bidirectional TCP connection |
TcpSocket | Socket builder for pre-connection configuration |
UdpSocket | Connectionless datagram socket |
UnixStream | Unix domain stream socket |
UnixListener | Unix domain socket listener |
UnixDatagram | Unix domain datagram socket |
Address | IPv4/IPv6 socket address |
TCP server
Section titled “TCP server”Quick start with convenience functions
Section titled “Quick start with convenience functions”const volt = @import("volt");
// Bind to an address stringvar listener = try volt.net.listen("0.0.0.0:8080");defer listener.close();
// Or bind to a port on all interfacesvar listener2 = try volt.net.listenPort(8080);defer listener2.close();Accept loop
Section titled “Accept loop”tryAccept returns an AcceptResult containing the new stream and the peer address:
while (true) { if (try listener.tryAccept()) |result| { var stream = result.stream; const peer = result.peer_addr; // Handle connection... handleClient(&stream, peer); }}Async accept
Section titled “Async accept”accept() returns an AcceptFuture for scheduler integration:
var future = listener.accept();// Poll through your runtime...// When ready, result contains the stream and peer address.TCP client
Section titled “TCP client”Quick connect
Section titled “Quick connect”var stream = try volt.net.connect("127.0.0.1:8080");defer stream.close();Connect with DNS resolution
Section titled “Connect with DNS resolution”var stream = try volt.net.connectHost(allocator, "example.com", 443);defer stream.close();Custom socket options
Section titled “Custom socket options”Use TcpSocket as a builder to configure options before connecting:
var socket = try volt.net.TcpSocket.newV4();
// Set buffer sizestry socket.setRecvBufferSize(65536);try socket.setSendBufferSize(65536);
// Enable TCP keepalivetry socket.setKeepalive(.{ .time = volt.time.Duration.fromSecs(60),});
// Enable TCP_NODELAY (disable Nagle's algorithm)try socket.setNodelay(true);
// Connectvar stream = try socket.connect(addr);defer stream.close();TCP stream I/O
Section titled “TCP stream I/O”Non-blocking read/write
Section titled “Non-blocking read/write”var buf: [4096]u8 = undefined;
// tryRead returns ?usize -- null means WouldBlockif (try stream.tryRead(&buf)) |n| { if (n == 0) { // Connection closed by peer return; } processData(buf[0..n]);}
// tryWrite returns ?usize -- null means WouldBlockif (try stream.tryWrite(response_data)) |n| { // n bytes were written}
// writeAll blocks (retries) until all data is writtentry stream.writeAll("HTTP/1.1 200 OK\r\n\r\nHello!");Async futures
Section titled “Async futures”// Read futurevar read_future = stream.read(&buf);
// Write futurevar write_future = stream.write(data);
// Write-all futurevar write_all_future = stream.writeAll(data);
// Readiness futures (wait for socket to be readable/writable)var readable_future = stream.readable();var writable_future = stream.writable();Read data without consuming it from the receive buffer:
const n = try stream.peek(&buf);if (n > 0) { // Peeked n bytes, data is still in the buffer}Shutdown
Section titled “Shutdown”Shut down one or both directions of the connection:
try stream.shutdown(.write); // No more writes (sends FIN)try stream.shutdown(.read); // No more readstry stream.shutdown(.both); // Both directionsSplitting a stream
Section titled “Splitting a stream”Split a TcpStream into independent read and write halves for concurrent I/O:
// Borrowed split -- halves reference the original streamconst halves = stream.split_halves();var reader = halves.read;var writer = halves.write;
// Owned split -- halves can be moved to different tasksconst owned = try stream.intoSplit(allocator);var owned_reader = owned.read; // Can move to reader taskvar owned_writer = owned.write; // Can move to writer taskOne-to-many (sendTo/recvFrom)
Section titled “One-to-many (sendTo/recvFrom)”var socket = try volt.net.UdpSocket.bind(volt.net.Address.fromPort(8080));defer socket.close();
var buf: [1024]u8 = undefined;
// Receive from any senderif (try socket.tryRecvFrom(&buf)) |result| { const data = buf[0..result.len]; const sender = result.addr;
// Echo back to sender _ = try socket.trySendTo(data, sender);}One-to-one (connect + send/recv)
Section titled “One-to-one (connect + send/recv)”var socket = try volt.net.UdpSocket.bind(volt.net.Address.fromPort(0));try socket.connect(server_addr);
_ = try socket.trySend("hello");const n = try socket.tryRecv(&buf) orelse 0;Socket options
Section titled “Socket options”try socket.setBroadcast(true);try socket.setMulticastLoop(true);try socket.setMulticastTtl(2);try socket.setTtl(64);
// Join a multicast grouptry socket.joinMulticast(multicast_addr, interface_addr);try socket.leaveMulticast(multicast_addr, interface_addr);Unix domain sockets
Section titled “Unix domain sockets”Unix sockets provide local inter-process communication, faster than TCP loopback.
Stream (connection-oriented)
Section titled “Stream (connection-oriented)”// Servervar listener = try volt.net.UnixListener.bind("/tmp/myapp.sock");defer listener.close();
if (try listener.tryAccept()) |result| { var stream = result.stream; defer stream.close(); // Handle connection...}
// Clientvar stream = try volt.net.UnixStream.connect("/tmp/myapp.sock");defer stream.close();try stream.writeAll("hello");Datagram (connectionless)
Section titled “Datagram (connectionless)”var sock = try volt.net.UnixDatagram.bind("/tmp/myapp-dgram.sock");defer sock.close();
_ = try sock.trySendTo("message", "/tmp/other.sock");
var buf: [1024]u8 = undefined;if (try sock.tryRecvFrom(&buf)) |result| { processMessage(buf[0..result.len]);}DNS resolution
Section titled “DNS resolution”DNS lookups are blocking operations. Use them from the blocking pool in production:
// Resolve all addressesvar result = try volt.net.resolve(allocator, "example.com", 443);defer result.deinit();
for (result.addresses) |addr| { // Try connecting to each address}
// Resolve first address onlyconst addr = try volt.net.resolveFirst(allocator, "example.com", 443);var stream = try volt.net.TcpStream.connect(addr);Address
Section titled “Address”The Address type wraps sockaddr_storage and supports both IPv4 and IPv6:
// Parse from stringconst addr = try volt.net.Address.parse("127.0.0.1:8080");
// From port only (binds to 0.0.0.0)const addr2 = volt.net.Address.fromPort(8080);
// Get componentsconst port = addr.port();const family = addr.family(); // AF.INET or AF.INET6Connection Lifecycle and Peer Disconnect
Section titled “Connection Lifecycle and Peer Disconnect”Understanding how TCP connections end is essential for writing robust servers.
Orderly shutdown (FIN)
Section titled “Orderly shutdown (FIN)”When the remote peer closes the connection gracefully, tryRead returns 0 bytes. This is not an error — it means the peer sent a FIN packet:
const n = stream.tryRead(&buf) catch return orelse continue;if (n == 0) { // Peer closed the connection (FIN received). // No more data will arrive. Clean up and return. return;}Connection reset (RST)
Section titled “Connection reset (RST)”If the remote peer crashes, the network drops, or the connection is forcibly closed, tryRead returns an error (typically ConnectionResetByPeer or BrokenPipe):
const n = stream.tryRead(&buf) catch |err| { // Connection was reset or aborted. // Log the error and clean up. std.debug.print("Connection error: {}\n", .{err}); return;};Write to a closed connection
Section titled “Write to a closed connection”Writing to a connection after the peer has closed it produces BrokenPipe. Always handle write errors:
stream.writeAll(response) catch |err| { // Peer may have disconnected between read and write. std.debug.print("Write failed: {}\n", .{err}); return;};Summary
Section titled “Summary”tryRead result | Meaning |
|---|---|
n > 0 | Data received |
null | Would block (no data ready, try again) |
n == 0 | Orderly shutdown — peer sent FIN |
error | Connection reset, abort, or network failure |
For a complete example that handles all these cases, see the Echo Server cookbook.
Async server pattern
Section titled “Async server pattern”A complete async TCP server skeleton:
const volt = @import("volt");
pub fn main() !void { try volt.run(server);}
fn server(io: volt.Io) void { var listener = volt.net.listen("0.0.0.0:8080") catch return; defer listener.close();
while (true) { if (listener.tryAccept() catch null) |result| { _ = io.@"async"(handleClient, .{result.stream}) catch continue; } }}
fn handleClient(conn: volt.net.TcpStream) void { var stream = conn; defer stream.close();
var buf: [4096]u8 = undefined; while (true) { const n = stream.tryRead(&buf) catch return orelse continue; if (n == 0) return; // Client disconnected stream.writeAll(buf[0..n]) catch return; }}