zig-skills/references/std-net.md

15 KiB

std.Io.net Reference (Zig 0.16.0)

Cross-platform networking abstractions for TCP/IP connections, address handling, and DNS resolution.

Primary Zig 0.16 release-note source: https://ziglang.org/download/0.16.0/release-notes.html

Zig 0.16 moves networking under std.Io.net. New networking code should accept/use std.Io, and stream/socket close/read/write operations should use the io parameter. Older std.net examples below are historical; translate them to std.Io.net before using them in new code.

High-level HTTP clients also store io:

var client: std.http.Client = .{
    .allocator = allocator,
    .io = io,
};
defer client.deinit();

Table of Contents

TCP Client

Connect by Hostname

const std = @import("std");
const net = std.net;

pub fn main() !void {
    var gpa: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Connect to host:port (handles DNS resolution)
    const stream = try net.tcpConnectToHost(allocator, "example.com", 80);
    defer stream.close();

    // Create buffered reader/writer
    var read_buf: [4096]u8 = undefined;
    var write_buf: [1024]u8 = undefined;

    var reader = stream.reader(&read_buf);
    var writer = stream.writer(&write_buf);

    // Write request
    try writer.interface.writeAll("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
    try writer.interface.flush();

    // Read response
    while (reader.interface().take(4096)) |chunk| {
        std.debug.print("{s}", .{chunk});
    } else |err| switch (err) {
        error.EndOfStream => {},
        else => return err,
    }
}

Connect by Address

// Parse and connect to IP address directly (no DNS)
const address = try net.Address.parseIp4("192.168.1.1", 8080);
const stream = try net.tcpConnectToAddress(address);
defer stream.close();

Connect with IPv6

// IPv6 address
const addr6 = try net.Address.parseIp6("::1", 8080);
const stream = try net.tcpConnectToAddress(addr6);
defer stream.close();

// IPv6 with scope ID (link-local)
const link_local = try net.Address.resolveIp6("fe80::1%eth0", 8080);

TCP Server

Basic Server

const std = @import("std");
const net = std.net;

pub fn main() !void {
    // Create address to listen on
    const address = net.Address.initIp4(.{ 0, 0, 0, 0 }, 8080);

    // Start listening
    var server = try address.listen(.{
        .reuse_address = true,
    });
    defer server.deinit();

    std.debug.print("Listening on port {d}\n", .{server.listen_address.getPort()});

    // Accept loop
    while (true) {
        const conn = try server.accept();
        defer conn.stream.close();

        // Handle connection
        try handleClient(conn.stream, conn.address);
    }
}

fn handleClient(stream: net.Stream, client_addr: net.Address) !void {
    var read_buf: [4096]u8 = undefined;
    var write_buf: [1024]u8 = undefined;

    var reader = stream.reader(&read_buf);
    var writer = stream.writer(&write_buf);

    // Read request
    const request = reader.interface().takeDelimiter('\n') catch |err| switch (err) {
        error.EndOfStream => return,
        else => return err,
    } orelse return;

    std.debug.print("Request from client: {s}\n", .{request});

    // Send response
    try writer.interface.writeAll("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello");
    try writer.interface.flush();
}

Listen Options

const server = try address.listen(.{
    // Allow address reuse (SO_REUSEADDR + SO_REUSEPORT on POSIX)
    .reuse_address = true,

    // Connection backlog (default 128)
    .kernel_backlog = 256,

    // Non-blocking accept (O_NONBLOCK)
    .force_nonblocking = false,
});

Server on Any Available Port

// Listen on port 0 to let OS assign an available port
const address = net.Address.initIp4(.{ 127, 0, 0, 1 }, 0);
var server = try address.listen(.{});
defer server.deinit();

// Get the assigned port
const port = server.listen_address.getPort();
std.debug.print("Listening on port {d}\n", .{port});

Address Types

Address Union

pub const Address = extern union {
    any: posix.sockaddr,
    in: Ip4Address,
    in6: Ip6Address,
    un: posix.sockaddr.un,  // Unix socket (if supported)
};

Creating Addresses

// IPv4 from bytes
const addr4 = net.Address.initIp4(.{ 127, 0, 0, 1 }, 8080);

// IPv6 from bytes
const addr6 = net.Address.initIp6(
    .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },  // ::1
    8080,      // port
    0,         // flowinfo
    0,         // scope_id
);

// Unix socket
const unix = try net.Address.initUnix("/tmp/my.sock");

Parsing Addresses

// Parse IPv4
const addr4 = try net.Address.parseIp4("192.168.1.1", 8080);

// Parse IPv6
const addr6 = try net.Address.parseIp6("2001:db8::1", 8080);

// Parse either (tries IPv4 first, then IPv6)
const addr = try net.Address.parseIp("::1", 8080);

// Parse IP:port format
// IPv4: "192.168.1.1:8080"
// IPv6: "[::1]:8080" (brackets required)
const addr_port = try net.Address.parseIpAndPort("[::1]:8080");

// Resolve with interface lookup (for link-local IPv6)
const resolved = try net.Address.resolveIp6("fe80::1%eth0", 8080);

Address Methods

var addr = net.Address.initIp4(.{ 127, 0, 0, 1 }, 8080);

// Get/set port (native endian)
const port = addr.getPort();  // 8080
addr.setPort(9090);

// Get socket length for syscalls
const socklen = addr.getOsSockLen();

// Compare addresses
if (addr.eql(other_addr)) {
    // addresses match
}

// Format for printing
var buf: [64]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try addr.format(&writer);
const formatted = writer.buffered();  // "127.0.0.1:8080"

Ip4Address

const Ip4Address = extern struct {
    sa: posix.sockaddr.in,

    pub fn parse(buf: []const u8, port: u16) !Ip4Address;
    pub fn init(addr: [4]u8, port: u16) Ip4Address;
    pub fn getPort(self: Ip4Address) u16;
    pub fn setPort(self: *Ip4Address, port: u16) void;
    pub fn format(self: Ip4Address, w: *std.Io.Writer) !void;
};

Ip6Address

const Ip6Address = extern struct {
    sa: posix.sockaddr.in6,

    pub fn parse(buf: []const u8, port: u16) !Ip6Address;
    pub fn resolve(buf: []const u8, port: u16) !Ip6Address;  // handles %interface
    pub fn init(addr: [16]u8, port: u16, flowinfo: u32, scope_id: u32) Ip6Address;
    pub fn getPort(self: Ip6Address) u16;
    pub fn setPort(self: *Ip6Address, port: u16) void;
    pub fn format(self: Ip6Address, w: *std.Io.Writer) !void;
};

Stream I/O

Stream Type

pub const Stream = struct {
    handle: Handle,  // fd on POSIX, SOCKET on Windows

    pub fn close(s: Stream) void;
    pub fn reader(stream: Stream, buffer: []u8) Reader;
    pub fn writer(stream: Stream, buffer: []u8) Writer;
};

Reading from Stream

const stream = try net.tcpConnectToHost(allocator, "example.com", 80);
defer stream.close();

var buf: [4096]u8 = undefined;
var reader = stream.reader(&buf);
const r = reader.interface();

// Read bytes
const data = r.take(100) catch |err| switch (err) {
    error.EndOfStream => &.{},
    error.ReadFailed => return reader.getError().?,
};

// Read until delimiter
const line = r.takeDelimiter('\n') catch |err| switch (err) {
    error.EndOfStream => null,
    error.StreamTooLong => return error.LineTooLong,
    error.ReadFailed => return reader.getError().?,
} orelse return;

// Discard bytes
_ = try r.discard(.limited(100));

// Stream to writer
_ = try r.streamRemaining(&output_writer);

Writing to Stream

var buf: [1024]u8 = undefined;
var writer = stream.writer(&buf);
const w = &writer.interface;

// Write bytes
try w.writeAll("Hello, World!");

// Formatted output
try w.print("Count: {d}\n", .{42});

// MUST flush before close
try w.flush();

Error Handling

var reader = stream.reader(&buf);
const r = reader.interface();

const data = r.take(100) catch |err| switch (err) {
    error.EndOfStream => {
        // Connection closed normally
        return;
    },
    error.ReadFailed => {
        // Get underlying error
        const read_err = reader.getError().?;
        switch (read_err) {
            error.ConnectionResetByPeer => return error.Disconnected,
            error.SocketNotConnected => return error.Disconnected,
            else => return read_err,
        }
    },
};

DNS Resolution

Get Address List

const std = @import("std");
const net = std.net;

pub fn main() !void {
    var gpa: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Resolve hostname to addresses
    const list = try net.getAddressList(allocator, "example.com", 80);
    defer list.deinit();

    // Canonical name (if available)
    if (list.canon_name) |name| {
        std.debug.print("Canonical name: {s}\n", .{name});
    }

    // Iterate addresses
    for (list.addrs) |addr| {
        var buf: [64]u8 = undefined;
        var w: std.Io.Writer = .fixed(&buf);
        try addr.format(&w);
        std.debug.print("Address: {s}\n", .{w.buffered()});
    }
}

Connect with Fallback

tcpConnectToHost automatically tries all resolved addresses:

// Tries each resolved address until one connects
const stream = net.tcpConnectToHost(allocator, "example.com", 80) catch |err| switch (err) {
    error.ConnectionRefused => return error.ServerDown,
    error.UnknownHostName => return error.DnsError,
    error.TemporaryNameServerFailure => return error.DnsError,
    else => return err,
};

Unix Sockets

Check Platform Support

if (net.has_unix_sockets) {
    // Unix sockets available
}

Connect to Unix Socket

const stream = try net.connectUnixSocket("/var/run/app.sock");
defer stream.close();

var buf: [4096]u8 = undefined;
var reader = stream.reader(&buf);
var writer = stream.writer(&buf);
// ... use like TCP

Unix Socket Server

const address = try net.Address.initUnix("/tmp/my.sock");
var server = try address.listen(.{ .reuse_address = true });
defer server.deinit();

// Remove socket file on cleanup
defer std.fs.deleteFileAbsolute("/tmp/my.sock") catch {};

while (true) {
    const conn = try server.accept();
    defer conn.stream.close();
    // handle connection...
}

Common Patterns

Echo Server

const std = @import("std");
const net = std.net;

pub fn main() !void {
    const address = net.Address.initIp4(.{ 0, 0, 0, 0 }, 7);  // echo port
    var server = try address.listen(.{ .reuse_address = true });
    defer server.deinit();

    while (true) {
        const conn = try server.accept();
        defer conn.stream.close();

        var buf: [4096]u8 = undefined;
        var reader = conn.stream.reader(&buf);
        var writer = conn.stream.writer(&buf);

        // Echo back everything received
        _ = reader.interface().streamRemaining(&writer.interface) catch {};
        writer.interface.flush() catch {};
    }
}

Simple HTTP GET

fn httpGet(allocator: Allocator, host: []const u8, path: []const u8) ![]u8 {
    const stream = try net.tcpConnectToHost(allocator, host, 80);
    defer stream.close();

    var write_buf: [1024]u8 = undefined;
    var writer = stream.writer(&write_buf);
    const w = &writer.interface;

    try w.print("GET {s} HTTP/1.1\r\n", .{path});
    try w.print("Host: {s}\r\n", .{host});
    try w.writeAll("Connection: close\r\n\r\n");
    try w.flush();

    var read_buf: [4096]u8 = undefined;
    var reader = stream.reader(&read_buf);

    var response: std.ArrayList(u8) = .empty;
    defer response.deinit(allocator);

    while (true) {
        const chunk = reader.interface().take(4096) catch |err| switch (err) {
            error.EndOfStream => break,
            error.ReadFailed => return reader.getError().?,
        };
        try response.appendSlice(allocator, chunk);
    }

    return response.toOwnedSlice(allocator);
}

Non-blocking Accept with Timeout

const std = @import("std");
const net = std.net;
const posix = std.posix;

fn acceptWithTimeout(server: *net.Server, timeout_ms: i32) !?net.Server.Connection {
    var pfd = [1]posix.pollfd{.{
        .fd = server.stream.handle,
        .events = posix.POLL.IN,
        .revents = undefined,
    }};

    const ready = try posix.poll(&pfd, timeout_ms);
    if (ready == 0) return null;  // timeout

    return try server.accept();
}

Address Validation

fn isValidIpAddress(str: []const u8) bool {
    _ = net.Address.parseIp(str, 0) catch return false;
    return true;
}

fn isValidHostname(hostname: []const u8) bool {
    return net.isValidHostName(hostname);
}

Dual-Stack Server (IPv4 + IPv6)

// Listen on IPv6 with dual-stack (accepts both IPv4 and IPv6)
const address = net.Address.initIp6(
    .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },  // ::
    8080,
    0,
    0,
);

var server = try address.listen(.{ .reuse_address = true });
defer server.deinit();

// IPv4 clients appear as IPv4-mapped IPv6 addresses (::ffff:x.x.x.x)

Connection Pool Pattern

const Connection = struct {
    stream: net.Stream,
    in_use: bool,
};

const Pool = struct {
    connections: std.ArrayList(Connection),
    allocator: Allocator,

    pub fn acquire(self: *Pool, address: net.Address) !net.Stream {
        // Find free connection
        for (self.connections.items) |*conn| {
            if (!conn.in_use) {
                conn.in_use = true;
                return conn.stream;
            }
        }
        // Create new connection
        const stream = try net.tcpConnectToAddress(address);
        try self.connections.append(self.allocator, .{
            .stream = stream,
            .in_use = true,
        });
        return stream;
    }

    pub fn release(self: *Pool, stream: net.Stream) void {
        for (self.connections.items) |*conn| {
            if (conn.stream.handle == stream.handle) {
                conn.in_use = false;
                return;
            }
        }
    }

    pub fn deinit(self: *Pool) void {
        for (self.connections.items) |conn| {
            conn.stream.close();
        }
        self.connections.deinit(self.allocator);
    }
};

Error Types

Connection Errors

pub const TcpConnectToHostError = GetAddressListError || TcpConnectToAddressError;

pub const TcpConnectToAddressError = posix.SocketError || posix.ConnectError;
// Includes: ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, etc.

DNS Errors

pub const GetAddressListError = error{
    TemporaryNameServerFailure,
    NameServerFailure,
    AddressFamilyNotSupported,
    UnknownHostName,
    HostLacksNetworkAddresses,
    // ... and others
};

Address Parse Errors

pub const IPv4ParseError = error{
    Overflow,
    InvalidEnd,
    InvalidCharacter,
    Incomplete,
    NonCanonical,  // e.g., leading zeros like "01.02.03.04"
};

pub const IPv6ParseError = error{
    Overflow,
    InvalidEnd,
    InvalidCharacter,
    Incomplete,
    InvalidIpv4Mapping,
};