Networking
The io module has TCP, UDP and Unix domain sockets, IP and socket
addresses, and host name lookup; the tls module encrypts TCP connections,
and the http module speaks HTTP over them.
In a plain program every call blocks the calling thread until it completes,
like File. To serve several connections at once, give each
one its own thread, or its own task: in a
task, a call that has to wait parks only that task.
On WebAssembly (--target wasm32-wasip1) there are no sockets, host name
lookups or TLS: each of those calls returns an IoError saying it is not
supported on WASI (see WebAssembly).
Addresses
An IpAddress is an IPv4 or IPv6 address. A SocketAddress is an IP
address and a port:
import io;
main(): int {
var home = io.IpAddress.v4(127, 0, 0, 1);
assert(home.is_loopback(), "127.0.0.1 is loopback");
# Text is read strictly and printed in one canonical form.
var six = io.IpAddress.parse("2001:DB8:0:0:0:0:0:1").value();
assert(six.to_string() == "2001:db8::1", "IPv6 prints compressed and lowercase");
assert(io.IpAddress.parse("256.1.1.1").error().is_invalid_input(), "256 is out of range");
var service = io.SocketAddress.parse("[::1]:8080").value();
assert(service.is_v6() && service.port() == 8080, "IPv6 with a port");
assert(service.to_string() == "[::1]:8080", "brackets around IPv6");
# A link-local IPv6 address names its interface after `%`: a number, or
# a name such as `eth0` that is looked up on this machine.
var link = io.SocketAddress.parse("[fe80::1%4000000000]:8080").value();
assert(link.address().scope_id() == 4000000000, "the scope id");
# A host name goes to the system resolver; an IP address is used as is.
var local = io.resolve("localhost", 80).value();
assert(local.count >= 1 && local[0].address().is_loopback(), "localhost is loopback");
return 0;
}
IpAddress.v4(a, b, c, d)andIpAddress.v6(a, ..., h)build an address from its bytes or its eight 16-bit groups.loopback_v4(),loopback_v6(),unspecified_v4()(0.0.0.0) andunspecified_v6()(::) are the well-known ones.IpAddress()is0.0.0.0.IpAddress.parsereads dotted decimal IPv4 and the RFC 4291 text form of IPv6, andto_string()prints the RFC 5952 form: lowercase, zeros compressed,::ffff:1.2.3.4for an IPv4-mapped address. Parsing and printing are the same on every platform.- An IPv6 address may carry a scope id, the network interface a link-local
address belongs to:
fe80::1%eth0orfe80::1%2(RFC 4007).scope_id()reads it (0 for none) andwith_scope_id(id)sets it. A name is looked up on this machine, andto_string()prints the interface’s name when it has one, else the number. Sockets pass it to the system assin6_scope_id. In a URI the%is escaped,http://[fe80::1%25eth0]/(RFC 6874); the HTTP client decodes it. SocketAddress(address, port)orSocketAddress.parse("1.2.3.4:80")make a socket address. IPv6 takes brackets:[::1]:80.resolve(host, port)looks a host name up with the system resolver (the hosts file and DNS) and returns every address it has, withportattached. An IP address is returned as is. A name with no address fails withis_not_found().
TCP
A TcpListener waits for connections. Each TcpStream is one connection:
import io;
# A server and a client on this machine, taking turns on one thread: the
# system completes a connection before the server accepts it.
main(): int {
# Port 0 asks the system for any free port.
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var address = listener.local_address().value();
var client = io.TcpStream.connect(address).value();
var (server, peer) = listener.accept().value();
assert(peer == client.local_address().value(), "accept reports who connected");
# The client sends a request and closes its sending side.
assert(client.write_all("ping").is_ok(), "send the request");
client.shutdown_write();
# The server reads until the client is done: read returns 0 at the end.
var request = Array<u8>();
while (server.read(request, 4096).value() > 0) {
}
assert(request.count == 4, "the whole request arrived");
# It answers, and the client reads exactly the bytes it expects.
server.write_all("pong");
var reply = Array<u8>();
assert(client.read_exact(reply, 4).is_ok(), "read the reply");
assert(reply[0] == 'p' && reply[1] == 'o', "the reply is pong");
return 0;
}
TcpListener.bind(address)listens on a local address. Port 0 lets the system pick a free port, andlocal_address()says which one it picked.accept()waits for the next connection and returns the stream and the peer’s address as a tuple.for (var connection in listener.incoming())accepts one connection per step, yielding anIoResult<TcpStream>(see Iteration).TcpStream.connect(address)connects to an address.TcpStream.connect_host(host, port)resolves a name and tries each of its addresses in turn.read(buffer, max)waits until some data arrives, then appends up tomaxbytes of it tobuffer, the same wayFile.readdoes. It returns 0 once the peer has closed its side.read_exact(buffer, count)loops untilcountbytes have arrived.write(data)sends what the system will take in one call.write_all(data)loops until everything is sent;datais au8[]or aString.shutdown_write()tells the peer that nothing more is coming, while this side can still read.set_no_delay(true)sends small writes immediately instead of combining them.local_address()andpeer_address()return the two ends of the connection.
Reading and writing any stream
A File, a TcpStream, a UnixStream and a tls.TlsStream have the
io.Reader and io.Writer traits, so a function that takes an
io.Reader (or <R: io.Reader>) reads from any of them:
import io;
# Any stream that is an io.Reader: a File, a socket, or a TLS stream.
count_lines(source: io.Reader): int {
var count = 0;
for (var line in source.lines()) {
if (line.is_err()) {
break;
}
count += 1;
}
return count;
}
# `print` and `print_line` write a value's text to any io.Writer, the way the
# global `print` writes to standard output.
main(): int {
var path = io.Directory.temp().join("networking-print.txt");
var file = io.File.create(path).value();
file.print("total: ");
file.print_line(42);
file.print_line((1, "two"));
file.close();
assert(io.File.read_to_string(path).value() == "total: 42\n(1, two)\n", "the file holds both lines");
assert(count_lines(io.File.open(path).value()) == 2, "a File is a Reader");
io.File.remove(path);
# The same calls work on a TcpStream (and a UnixStream).
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var client = io.TcpStream.connect(listener.local_address().value()).value();
var (server, peer) = listener.accept().value();
client.print_line($"hello from {client.local_address().value()}");
client.print_line("bye");
client.shutdown_write();
assert(count_lines(server) == 2, "a TcpStream is a Reader");
return 0;
}
io.Readerneedsread(buffer, max)and giveslines(), the stream’s lines asIoResult<String>s, andchunks(size), its bytes in arrays of at mostsize, both read as aforloop asks for them (see Iteration).io.Writerneedswrite(data)andwrite_all(data: u8[])and givesprint(value)andprint_line(value). They write the text thatto_stringmakes of any value, as the globalprintdoes on standard output;print_lineadds a\n. Both return anIoResult<void>, likewrite_all.io.ByteReader(bytes)reads an array of bytes as a stream, andio.ByteWriter()collects what is written in itsbytesarray, for code that takes a stream when the data is in memory.compress.DecompressReaderandcompress.CompressWriterwrap another stream (see Compression and archives).- Another type gets them by providing those methods and declaring
extend MyStream with io.Reader;orextend MyStream with io.Writer;. io.Print, which gave onlyprintandprint_line, is deprecated: useio.Writer.
UDP
A UdpSocket sends and receives datagrams. Each one arrives whole or not
at all, and in no guaranteed order:
import io;
main(): int {
var loopback = io.SocketAddress(io.IpAddress.loopback_v4(), 0);
var sender = io.UdpSocket.bind(loopback).value();
var receiver = io.UdpSocket.bind(loopback).value();
var datagram = Array<u8>();
datagram.append(1);
datagram.append(2);
datagram.append(3);
sender.send_to(datagram, receiver.local_address().value());
# One receive takes one whole datagram and says who sent it.
var buffer = Array<u8>();
var (count, from) = receiver.receive_from(buffer, 1500).value();
assert(count == 3 && buffer[2] == 3, "the datagram arrived whole");
assert(from == sender.local_address().value(), "from the sender");
return 0;
}
UdpSocket.bind(address)opens a socket on a local address.send_to(data, address)sends one datagram.receive_from(buffer, max)waits for one datagram, appends it tobuffer, and returns the byte count and the sender. The part of a datagram beyondmaxbytes is lost.- After
connect(address),sendandreceiveuse that peer, and datagrams from anyone else are dropped. set_broadcast(true)allows sending to a broadcast address.
Unix domain sockets
A Unix domain socket connects processes on the same machine through a path
in the file system instead of an IP address and port. UnixListener and
UnixStream work like TcpListener and TcpStream:
import io;
# A server and a client over a Unix domain socket, taking turns on one
# thread, as in the TCP example.
main(): int {
var path = io.Directory.temp().join("pepper-networking-unix.sock");
# Binding fails if the socket file is still there from an earlier run.
io.File.remove(path);
var listener = io.UnixListener.bind(path).value();
var client = io.UnixStream.connect(path).value();
var server = listener.accept().value();
assert(client.write_all("ping").is_ok(), "send the request");
var request = Array<u8>();
assert(server.read_exact(request, 4).is_ok(), "read the request");
server.write_all("pong");
var reply = Array<u8>();
assert(client.read_exact(reply, 4).is_ok(), "read the reply");
assert(reply[0] == 'p' && reply[1] == 'o', "the reply is pong");
# Closing the listener leaves the socket file; remove it when done.
listener.close();
assert(io.File.remove(path).is_ok(), "remove the socket file");
return 0;
}
UnixListener.bind(path)creates a socket file atpathand listens on it. Binding fails withis_address_in_use()if a file is already there, and closing the listener does not remove the file, so a server removes it withFile.removebefore binding and when it is done.UnixStream.connect(path)connects to it. It fails withis_not_found()when there is no socket file, and withis_connection_refused()when the file is there but nothing listens on it.accept()returns just the stream: the client’s end has no path.- Reading, writing,
shutdown_write(),into_transfer()andadoptare the same as on aTcpStream, andSharedUnixListeneris the counterpart ofSharedTcpListener. - A path holds at most 107 bytes (103 on macOS); a longer or empty one
fails with
is_invalid_input().
Unix datagram sockets
UnixDatagram is the Unix domain counterpart of UdpSocket, on Linux and
macOS only (Windows has no Unix datagram sockets, so it is declared with
[Target(OS(Linux) || OS(Mac))]):
# Unix datagram sockets: a server bound to a path answers each datagram.
import io;
main(): int {
# Linux and macOS only: Windows has no Unix datagram sockets.
[Target(OS(Linux) || OS(Mac))]
{
var folder = io.Directory.temp().join("pepper-udgram-example");
io.Directory.remove_all(folder);
io.Directory.create_all(folder).value();
var server_path = folder.join("server.sock");
var client_path = folder.join("client.sock");
var server = io.UnixDatagram.bind(server_path).value();
var client = io.UnixDatagram.bind(client_path).value();
client.send_to("ping".bytes(), server_path).value();
# Each receive takes one whole datagram and names its sender.
var request = Array<u8>();
var (n, sender) = server.receive_from(request, 1024).value();
assert(n == 4 && sender == client_path, "one datagram from the client");
server.send_to("pong".bytes(), sender).value();
var reply = Array<u8>();
client.receive(reply, 1024).value();
assert(reply.count == 4, "the reply");
# A pair is two sockets already connected to each other.
var (a, b) = io.UnixDatagram.pair().value();
a.send("hi".bytes()).value();
var got = Array<u8>();
assert(b.receive(got, 16).value() == 2, "through the pair");
# Closing leaves the socket files; remove them.
io.Directory.remove_all(folder).value();
}
return 0;
}
UnixDatagram.bind(path)creates a socket file to receive on, with the same path rules and errors asUnixListener.bind;unbound()makes one that can only send, andpair()two that are connected to each other.send_to(data, path)sends one datagram; it fails withis_not_found()when there is no socket file andis_connection_refused()when nothing is bound to it any more. A datagram is never dropped silently. While the receiver’s queue is full, a send waits on Linux; on macOS it fails at once with “No buffer space available” (ENOBUFS) and sends nothing, so try again later.receive_from(buffer, max)returns the byte count and the sender’s path, which is empty for an unbound sender. As with UDP, a datagram longer thanmaxis cut.connect(path),sendandreceivework as on aUdpSocket.
TLS
The tls module encrypts a TCP connection with TLS 1.2 or 1.3. A
TlsConnector makes client connections and checks the server’s
certificate. A TlsAcceptor serves them with a certificate and key. Both
produce a TlsStream, which reads and writes like a TcpStream:
import async;
import io;
import tls;
import "test_certificate";
# A TLS server and client on this machine, in two tasks.
main(): int {
var connector = tls.TlsConnector.create();
if (connector.is_err()) {
# On Windows, or where OpenSSL 3 is not installed.
print(connector.error().message);
return 0;
}
# A real server reads its certificate and key from files it was given.
var certificate = io.Directory.temp().join("pepper-example-cert.pem");
var key = io.Directory.temp().join("pepper-example-key.pem");
io.File.write_text(certificate, test_certificate());
io.File.write_text(key, test_private_key());
async.run(
() => {
var acceptor = tls.TlsAcceptor.create(certificate, key).value();
# Mutual TLS: only clients with a certificate the test
# certificate issued (here, the test certificate itself) get in.
assert(acceptor.require_client_certificate(certificate).is_ok(), "ask clients for certificates");
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var port = listener.local_address().value().port();
var server = async.Task<void>.spawn_local(() => {
var (tcp, peer) = listener.accept().value();
var stream = acceptor.accept(tcp).value();
var request = Array<u8>();
stream.read_exact(request, 4);
stream.write_all("pong");
stream.shutdown();
});
# The client trusts the test certificate as well as the system's.
var client = connector.value();
assert(client.add_root_certificate(certificate).is_ok(), "trust the test certificate");
assert(client.set_identity(certificate, key).is_ok(), "present a client certificate");
var stream = client.connect_host("localhost", port).value();
stream.write_all("ping");
var reply = Array<u8>();
while (stream.read(reply, 4096).value() > 0) {}
assert(reply.count == 4 && reply[0] == 'p' && reply[1] == 'o', "the reply is pong");
server.join();
}
);
io.File.remove(certificate);
io.File.remove(key);
return 0;
}
TlsConnector.create()trusts the system’s root certificates, andadd_root_certificate(path)adds more from a PEM file, such as a private certificate authority. Make one connector and share it: it is[Shared].connector.connect(tcp, host)runs the handshake over a connectedTcpStream. It checks that the certificate is valid and was issued forhost, which is also sent to the server (SNI).connect_host(host, port)connects the TCP stream first. An IP address inhostis checked against the certificate’s addresses.TlsAcceptor.create(certificate_chain, private_key)reads PEM files, andacceptor.accept(tcp)runs the handshake over a connection aTcpListeneraccepted.- Mutual TLS:
acceptor.require_client_certificate(roots)asks every client for a certificate and accepts only one issued by a certificate in the PEM fileroots;connector.set_identity(certificate_chain, private_key)gives the client a certificate to present. A client with no certificate, or an untrusted one, fails the server’saccept. With TLS 1.3 the client’sconnectcan succeed first, and its next read then fails or returns 0. - ALPN:
connector.set_protocols(["h2", "http/1.1"])offers protocols to servers, most preferred first, andacceptor.set_protocols(...)lists the ones a server accepts, also most preferred first. The handshake picks the first of the server’s that the client offers, andstream.protocol()names it on both sides. A client that offers no protocols connects with none chosen; one whose protocols the server accepts none of fails the handshake (theno_application_protocolalert of RFC 7301). read,read_exact,writeandwrite_allwork on the decrypted bytes.shutdown()sends the TLS close_notify, andreadreturns 0 once the peer has sent one or closed the connection.- A failed handshake returns an error whose message says why, such as
certificate verify failed: self-signed certificate.
On Linux and macOS the module uses OpenSSL 3, loaded when the first
connector or acceptor is made: install it with the system’s package
manager, or brew install openssl@3 on macOS. If it is missing,
create() returns an error. On Windows it uses the system’s SChannel,
which needs nothing installed; a private key (a server’s or a client’s)
must be PKCS #8 (BEGIN PRIVATE KEY), and TLS 1.3 needs Windows 11 or
Server 2022.
Timeouts
A socket call waits as long as it takes unless a timeout limits it:
import io;
# A read that gives up when the peer stays silent, and a connect with a
# limit of its own.
main(): int {
var listener = io.TcpListener.bind(io.SocketAddress.parse("127.0.0.1:0").value()).value();
var address = listener.local_address().value();
# connect_timeout gives up after 2 seconds instead of the system's limit.
var client = io.TcpStream.connect_timeout(address, 2000).value();
var (server, peer) = listener.accept().value();
# The server says nothing, so the read gives up after 100 milliseconds.
client.set_read_timeout(100).value();
var reply = Array<u8>();
var silent = client.read(reply, 4096);
assert(silent.is_err() && silent.error().is_timed_out(), "the read timed out");
# The stream is still open: once the server answers, the read succeeds.
server.write_all("pong");
assert(client.read_exact(reply, 4).is_ok(), "read the late reply");
return 0;
}
set_read_timeout(milliseconds)limits how long a read (or a UDP datagram receive) waits for data, andset_write_timeout(milliseconds)how long a write waits for room to send. A call that waits longer fails withis_timed_out(), and the socket stays open for another try. Each wait counts on its own, soread_exactorwrite_allcan take longer in all. 0, the default, means no limit;read_timeout()andwrite_timeout()return the current values.TcpStream,UdpSocket,UnixStreamandUnixDatagramhave them.TcpStream.connect_timeout(address, milliseconds)gives up on a connection that is not established in time, instead of after the system’s own limit, which is often minutes.- They work the same outside a task, where the thread blocks, and inside
one, where only the task parks. In a task,
async.timeoutalso limits every wait in its block, and whichever ends first wins. - A
TlsStreamwaits through itsTcpStream, so the TCP stream’s timeouts limit its reads, writes and handshake too. Set them beforeconnectoraccept.
Errors
Every operation returns an IoResult. Besides the file-system predicates,
IoError has these for networking:
| Predicate | When |
|---|---|
is_connection_refused() | nothing listens at the address |
is_connection_reset() | the peer reset or closed the connection (writing to a closed connection included) |
is_address_in_use() | another socket already has the address and port |
is_address_not_available() | the IP address is not one of this machine’s |
is_timed_out() | the peer never answered, or a read, write or connect timeout expired |
is_not_found() | resolve found no address for the name, or UnixStream.connect found no socket file |
Writing to a closed connection never raises SIGPIPE, so it cannot kill
the process. The write returns an error instead.
Closing
A socket closes when the last reference to it goes, like a File, and
close() closes it earlier and reports any error. A socket cannot be
copied, so it cannot go through a channel to another
thread. Use it on the thread that made it, or move it: into_transfer()
turns a TcpStream (or a UnixStream) into a thread-safe SocketTransfer
that TcpStream.adopt (or UnixStream.adopt) takes on another thread, and a SharedTcpListener is a
listener any thread can accept on (see
Tasks).
On POSIX a listener sets SO_REUSEADDR, so a server can restart right
away. On Windows it does not, because there that option would let another
program take over the port.
The reference pages for addresses, sockets and Unix domain sockets and TLS document every method.