# std.Io.net Reference (Zig 0.16.0) Cross-platform networking abstractions for IP connections, address handling, DNS resolution, Unix-domain sockets, and lower-level socket operations. Zig 0.16 exposes these APIs under `std.Io.net`; there is no root `std.net` module. Primary Zig 0.16 release-note source: https://ziglang.org/download/0.16.0/release-notes.html Networking operations take an explicit `std.Io`. Readers, writers, servers, sockets, and streams also use that `io` value for construction or cleanup. High-level HTTP clients store it directly: ```zig var client: std.http.Client = .{ .allocator = allocator, .io = io, }; defer client.deinit(); ``` ## Table of Contents - [API Map](#api-map) - [TCP Clients](#tcp-clients) - [TCP Servers](#tcp-servers) - [IP Address Types](#ip-address-types) - [Stream I/O](#stream-io) - [DNS and Host Names](#dns-and-host-names) - [Unix-Domain Sockets](#unix-domain-sockets) - [Sockets and Datagram APIs](#sockets-and-datagram-apis) - [Common Patterns](#common-patterns) - [Error Sets](#error-sets) ## API Map ```zig const net = std.Io.net; net.IpAddress // tagged union: .ip4 or .ip6 net.Ip4Address // value-oriented IPv4 bytes + port net.Ip6Address // IPv6 bytes + port + flow + interface net.HostName // validated DNS host name and lookup/connect APIs net.UnixAddress // Unix-domain socket path net.Socket // open socket plus resolved/bound address net.Stream // reliable connected byte stream net.Server // listening socket net.Protocol // tcp, udp, and other protocol identifiers ``` Use `IpAddress.connect` when an IP is already known. Use `HostName.connect` when DNS resolution and address fallback are required. ## TCP Clients ### Connect by Host Name ```zig const std = @import("std"); const net = std.Io.net; pub fn main(init: std.process.Init) !void { const io = init.io; const host: net.HostName = try .init("example.com"); const stream = try host.connect(io, 80, .{ .mode = .stream, .protocol = .tcp, }); defer stream.close(io); var read_buf: [4096]u8 = undefined; var write_buf: [1024]u8 = undefined; var reader = stream.reader(io, &read_buf); var writer = stream.writer(io, &write_buf); try writer.interface.writeAll( "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n", ); try writer.interface.flush(); while (reader.interface.take(4096)) |chunk| { std.debug.print("{s}", .{chunk}); } else |err| switch (err) { error.EndOfStream => {}, error.ReadFailed => return reader.err.?, } } ``` `HostName.connect` performs lookup and races/falls back across returned addresses. The host's bytes are externally owned, so keep their backing storage alive for the call. ### Connect to a Parsed Address ```zig const address = try net.IpAddress.parseIp4("192.168.1.1", 8080); const stream = try address.connect(io, .{ .mode = .stream, .protocol = .tcp, }); defer stream.close(io); ``` ### IPv6 and Scoped IPv6 ```zig // Pure parsing: no interface-name scope lookup. const loopback = try net.IpAddress.parseIp6("::1", 8080); const stream = try loopback.connect(io, .{ .mode = .stream, .protocol = .tcp }); defer stream.close(io); // Resolving `%eth0` / `%eno1` requires Io because the interface name must be // converted to an operating-system interface index. const link_local = try net.IpAddress.resolveIp6(io, "fe80::1%eth0", 8080); ``` ### Connection Timeout ```zig const host: net.HostName = try .init("example.com"); const stream = try host.connect(io, 443, .{ .mode = .stream, .protocol = .tcp, .timeout = .{ .duration = .fromSeconds(5) }, }); defer stream.close(io); ``` Timeouts can be `.none`, a relative `.duration`, or an absolute `.deadline`. ## TCP Servers ### Basic Server ```zig const std = @import("std"); const net = std.Io.net; fn serve(io: std.Io) !void { const address: net.IpAddress = .{ .ip4 = .unspecified(8080) }; var server = try address.listen(io, .{ .reuse_address = true, .kernel_backlog = 256, }); defer server.deinit(io); std.debug.print("Listening on port {d}\n", .{server.socket.address.getPort()}); while (true) { const client = try server.accept(io); defer client.close(io); try handleClient(io, client); } } fn handleClient(io: std.Io, stream: net.Stream) !void { var read_buf: [4096]u8 = undefined; var write_buf: [1024]u8 = undefined; var reader = stream.reader(io, &read_buf); var writer = stream.writer(io, &write_buf); const request_line = reader.interface.takeDelimiter('\n') catch |err| switch (err) { error.ReadFailed => return reader.err.?, error.StreamTooLong => return error.RequestLineTooLong, } orelse return; std.debug.print("Request from {f}: {s}\n", .{ stream.socket.address, request_line }); try writer.interface.writeAll("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello"); try writer.interface.flush(); } ``` `Server.accept(io)` returns a `Stream`, not a separate connection wrapper. The accepted stream contains its socket and address. ### Listen Options ```zig const server = try address.listen(io, .{ .kernel_backlog = 128, .reuse_address = true, .mode = .stream, .protocol = .tcp, }); ``` `IpAddress.ListenOptions` contains exactly `kernel_backlog`, `reuse_address`, `mode`, and `protocol`. It does not contain the older `force_nonblocking` field. ### Ephemeral Port ```zig const address: net.IpAddress = .{ .ip4 = .loopback(0) }; var server = try address.listen(io, .{}); defer server.deinit(io); const assigned_port = server.socket.address.getPort(); ``` The resolved port is stored on `server.socket.address`; there is no `listen_address` field. ## IP Address Types ### Tagged Union ```zig pub const IpAddress = union(enum) { ip4: Ip4Address, ip6: Ip6Address, }; ``` This is a value-oriented union, not an extern `sockaddr` overlay. OS-specific socket-address conversion is handled below this API. ### Construction ```zig const loopback4: net.IpAddress = .{ .ip4 = .loopback(8080) }; const any4: net.IpAddress = .{ .ip4 = .unspecified(8080) }; const loopback6: net.IpAddress = .{ .ip6 = .loopback(8080) }; const any6: net.IpAddress = .{ .ip6 = .unspecified(8080) }; const explicit4: net.IpAddress = .{ .ip4 = .{ .bytes = .{ 127, 0, 0, 1 }, .port = 8080, } }; ``` `Ip6Address` also has `flow: u32 = 0` and `interface: net.Interface = .none` fields. ### Parsing ```zig const addr4 = try net.IpAddress.parseIp4("192.168.1.1", 8080); const addr6 = try net.IpAddress.parseIp6("2001:db8::1", 8080); const either = try net.IpAddress.parse("::1", 8080); // Address plus optional port. IPv6 must be bracketed. const literal4 = try net.IpAddress.parseLiteral("192.168.1.1:8080"); const literal6 = try net.IpAddress.parseLiteral("[2001:db8::1]:8080"); // Handles an IPv6 interface-name scope and therefore requires Io. const scoped = try net.IpAddress.resolve(io, "fe80::1%eth0", 8080); ``` `parseLiteral` uses port zero when no port is present. `parse` accepts an explicit port and tries IPv4, then IPv6. `resolve` adds scoped-IPv6 interface lookup. ### Methods and Formatting ```zig var address = try net.IpAddress.parseIp4("127.0.0.1", 8080); const port = address.getPort(); address.setPort(9090); const same = address.eql(&other_address); var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try address.format(&writer); // omits an IPv6 interface-name scope try address.formatResolved(io, &writer); // includes a resolvable IPv6 scope ``` `Ip4Address.format` and `Ip6Address.format` include the native-endian port. `IpAddress.fromIp6` converts IPv4-mapped IPv6 addresses back to `.ip4` where possible. ### IPv4 Parse Errors ```zig pub const Ip4Address.ParseError = error{ Overflow, InvalidEnd, InvalidCharacter, Incomplete, NonCanonical, }; ``` For example, leading-zero forms such as `01.2.3.4` are non-canonical. ## Stream I/O ### Stream Shape and Lifecycle ```text pub const Stream = struct { socket: net.Socket, pub fn close(stream: *const Stream, io: std.Io) void; pub fn shutdown(stream: *const Stream, io: std.Io, how: net.ShutdownHow) !void; pub fn reader(stream: Stream, io: std.Io, buffer: []u8) Stream.Reader; pub fn writer(stream: Stream, io: std.Io, buffer: []u8) Stream.Writer; }; ``` Do not close a stream twice. Flush a buffered writer before shutdown/close when its bytes must reach the peer. ### Reading ```zig var read_buf: [4096]u8 = undefined; var reader = stream.reader(io, &read_buf); const r = &reader.interface; const data = r.take(100) catch |err| switch (err) { error.EndOfStream => return, error.ReadFailed => return reader.err.?, }; const line = r.takeDelimiter('\n') catch |err| switch (err) { error.ReadFailed => return reader.err.?, error.StreamTooLong => return error.LineTooLong, } orelse return; _ = data; _ = line; ``` The generic reader surface reports `error.ReadFailed`; the network-specific cause is stored in `reader.err`, with cases such as `ConnectionResetByPeer`, `Timeout`, `SocketUnconnected`, or `NetworkDown`. ### Writing ```zig var write_buf: [1024]u8 = undefined; var writer = stream.writer(io, &write_buf); const w = &writer.interface; try w.writeAll("Hello, World!"); try w.print("Count: {d}\n", .{42}); try w.flush(); ``` The generic writer reports `error.WriteFailed`; inspect `writer.err` for the network-specific cause. A successful `writeAll` may still be buffered until `flush`. ### Half-Close ```zig try stream.shutdown(io, .send); // no more application writes // Continue reading until EndOfStream if the protocol expects a response. ``` `ShutdownHow` is `.recv`, `.send`, or `.both`. ## DNS and Host Names ### Validation ```zig const host = try net.HostName.init("example.com"); try net.HostName.validate("api.example.com"); const same = host.eql(try .init("EXAMPLE.COM")); // DNS names compare case-insensitively const child = host.sameParentDomain(try .init("www.example.com")); ``` `HostName` retains a borrowed byte slice. Labels and total length are validated; the maximum is `net.HostName.max_len`. ### Queue-Based Lookup ```zig const host: net.HostName = try .init("example.com"); var result_storage: [16]net.HostName.LookupResult = undefined; var results: std.Io.Queue(net.HostName.LookupResult) = .init(&result_storage); try host.lookup(io, &results, .{ .port = 443 }); while (results.getOne(io)) |result| switch (result) { .address => |address| std.debug.print("address: {f}\n", .{address}), .canonical_name => |name| std.debug.print("canonical: {s}\n", .{name.bytes}), } else |err| switch (err) { error.Closed => {}, error.Canceled => return err, } ``` `lookup` adds zero or more `.address` results and exactly one `.canonical_name`, then closes the queue even on error. Capacity 16 guarantees the call itself need not block waiting for a consumer. ### Connect with Lookup and Fallback ```zig const host: net.HostName = try .init("example.com"); const stream = host.connect(io, 443, .{ .mode = .stream, .protocol = .tcp, }) catch |err| switch (err) { error.UnknownHostName, error.NoAddressReturned => return error.DnsFailure, error.ConnectionRefused => return error.ServerUnavailable, else => return err, }; defer stream.close(io); ``` For advanced callers, `HostName.connectMany` asynchronously attempts all resolved addresses and writes successes or per-address connection errors to a caller-provided queue. ## Unix-Domain Sockets ### Support and Client ```zig if (net.has_unix_sockets) { const address = try net.UnixAddress.init("/var/run/app.sock"); const stream = try address.connect(io); defer stream.close(io); var read_buf: [4096]u8 = undefined; var reader = stream.reader(io, &read_buf); _ = &reader; } ``` `UnixAddress` borrows its path and rejects paths longer than `UnixAddress.max_len`. `isAbstract()` detects an empty/leading-NUL abstract address representation. ### Server ```zig const socket_path = "/tmp/my.sock"; std.Io.Dir.deleteFileAbsolute(io, socket_path) catch |err| switch (err) { error.FileNotFound => {}, else => return err, }; defer std.Io.Dir.deleteFileAbsolute(io, socket_path) catch {}; const address = try net.UnixAddress.init(socket_path); var server = try address.listen(io, .{ .kernel_backlog = 128 }); defer server.deinit(io); while (true) { const client = try server.accept(io); defer client.close(io); // Handle one client in this loop body. } ``` Unix listen options contain only `kernel_backlog`; IP `reuse_address` options do not apply to this type. ## Sockets and Datagram APIs `IpAddress.bind` is the non-streaming counterpart to `listen`: ```zig const address: net.IpAddress = .{ .ip4 = .unspecified(5353) }; const socket = try address.bind(io, .{ .mode = .dgram, .protocol = .udp, .allow_broadcast = false, }); defer socket.close(io); ``` `BindOptions` contains `ip6_only`, `allow_broadcast`, required `mode`, and optional `protocol`. `Socket` also exposes message send/receive operations, shutdown, option accessors, and `closeMany`; use those when datagram boundaries or raw socket features matter. The listening API intentionally has a smaller option set than `bind`. In particular, `IpAddress.ListenOptions` has no `ip6_only` flag, so do not assume an IPv6 listener is unconditionally dual-stack on every target. Use explicitly managed IPv4/IPv6 listeners when that behavior must be controlled. ## Common Patterns ### Echo One Connection ```zig fn echoConnection(io: std.Io, stream: net.Stream) !void { var read_buf: [4096]u8 = undefined; var write_buf: [4096]u8 = undefined; var reader = stream.reader(io, &read_buf); var writer = stream.writer(io, &write_buf); _ = reader.interface.streamRemaining(&writer.interface) catch |err| switch (err) { error.ReadFailed => return reader.err.?, error.WriteFailed => return writer.err.?, }; try writer.interface.flush(); } ``` For a concurrent server, schedule each accepted stream through the active `std.Io` implementation and make one owner responsible for closing it. ### Address and Host Validation ```zig fn isValidIpAddress(text: []const u8) bool { _ = net.IpAddress.parse(text, 0) catch return false; return true; } fn isValidHostName(text: []const u8) bool { net.HostName.validate(text) catch return false; return true; } ``` Parsing an IP is a pure syntax/canonicality check. Host-name validation does not perform DNS lookup. ### Index-Based Connection Pool ```zig const Pool = struct { const Entry = struct { stream: net.Stream, in_use: bool }; entries: std.ArrayList(Entry) = .empty, allocator: std.mem.Allocator, io: std.Io, fn acquire(self: *Pool, address: *const net.IpAddress) !usize { for (self.entries.items, 0..) |*entry, index| { if (!entry.in_use) { entry.in_use = true; return index; } } const stream = try address.connect(self.io, .{ .mode = .stream, .protocol = .tcp }); errdefer stream.close(self.io); try self.entries.append(self.allocator, .{ .stream = stream, .in_use = true }); return self.entries.items.len - 1; } fn get(self: *Pool, index: usize) *net.Stream { return &self.entries.items[index].stream; } fn release(self: *Pool, index: usize) void { self.entries.items[index].in_use = false; } fn deinit(self: *Pool) void { for (self.entries.items) |entry| entry.stream.close(self.io); self.entries.deinit(self.allocator); self.* = undefined; } }; ``` Indices are used because growing an `ArrayList` can invalidate pointers into its storage. A production pool also needs protocol-aware liveness checks, concurrency control, capacity limits, idle expiry, and a policy for discarding failed streams. ### Prefer Protocol-Specific Clients Direct stream examples are useful for custom protocols and learning the I/O model. For HTTP, use `std.http.Client`: it handles framing, redirects/options, response-body lifecycle, proxies, and TLS concerns that a raw `GET` snippet does not. ## Error Sets ### IP Connection Errors `net.IpAddress.ConnectError` includes address/family and resource failures plus network outcomes such as: ```zig error.ConnectionRefused error.ConnectionResetByPeer error.HostUnreachable error.NetworkUnreachable error.NetworkDown error.Timeout error.WouldBlock error.AccessDenied ``` It also includes cancellation and implementation-specific unexpected I/O errors. Match only cases the caller can handle meaningfully and propagate the rest. ### Host Lookup and Connect Errors ```zig net.HostName.LookupError // UnknownHostName, NameServerFailure, // NoAddressReturned, configuration/DNS record errors, ... net.HostName.ConnectError // LookupError || net.IpAddress.ConnectError ``` The old `GetAddressListError` and `TcpConnectToHostError` aliases are not Zig 0.16 APIs. ### Stream Error Translation `Stream.Reader` and `Stream.Writer` deliberately adapt network errors to the generic `std.Io.Reader`/`std.Io.Writer` interfaces: - On `error.ReadFailed`, inspect `reader.err`. - On `error.WriteFailed`, inspect `writer.err`. - `EndOfStream` is the normal generic-reader signal for an orderly peer close. - Close, shutdown, accept, connect, lookup, bind, and listen all take the explicit `io` used to create or operate the resource.