567 lines
17 KiB
Markdown
567 lines
17 KiB
Markdown
# 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.
|