tcp
Raw TCP listeners and streams. Returns and consumes Bytes values
for binary I/O. Blocking operations cooperate with silt’s task scheduler — a
silt task that calls tcp.accept or tcp.read yields its slot, letting other
tasks run, until the I/O completes.
The tcp feature is enabled by default. To build silt without it, disable
default features in your Cargo.toml.
Summary
| Function | Signature | Description |
|---|---|---|
accept | (TcpListener) -> Result(TcpStream, TcpError) | Wait for an incoming connection (cooperative I/O) |
close | (TcpStream) -> () | Shut the connection down: operations in flight on it return, later ones give Err(Closed) |
connect | (String) -> Result(TcpStream, TcpError) | Open a TCP connection to host:port (cooperative I/O) |
listen | (String) -> Result(TcpListener, TcpError) | Bind a TCP listener to host:port |
local_port | (TcpListener) -> Int | The port the listener is bound to: the one the system chose for port 0 |
peer_addr | (TcpStream) -> Result(String, TcpError) | The address of the other end, as ip:port |
read | (TcpStream, Int) -> Result(Bytes, TcpError) | Read up to max bytes (cooperative) |
read_exact | (TcpStream, Int) -> Result(Bytes, TcpError) | Read exactly n bytes (cooperative; loops) |
set_nodelay | (TcpStream, Bool) -> Result((), TcpError) | Send small writes at once (true) instead of gathering them (Nagle’s algorithm, the default) |
write | (TcpStream, Bytes) -> Result((), TcpError) | Write the entire buffer and flush (cooperative) |
Errors
Every fallible tcp.* call returns Result(T, TcpError). Variants are
narrow by design — the socket failure space is small once you strip
out the OS-specific noise. A variant says what failed:
| Variant | Fields | Meaning |
|---|---|---|
Connect(msg) | String | tcp.connect (or the connection of tcp.connect_tls) made no connection: refused, unreachable, no such host |
Bind(msg) | String | tcp.listen bound no listener: the address is in use, is not this machine’s, is no address |
Tls(msg) | String | TLS handshake failure |
Closed | — | the connection is closed: by tcp.close, or by the other end, however the system reports that (a reset, an abort, a broken pipe, an end of data in the middle of a read_exact) |
Timeout | — | op exceeded its deadline |
Other(msg) | String | unclassified socket failure |
TcpError implements the built-in Error trait, so e.message()
renders any variant as a string when you don’t want to branch on it.
Echo server example
import bytes
import task
import tcp
import time
fn main() {
match tcp.listen("127.0.0.1:8080") {
Ok(listener) -> {
println("listening on 127.0.0.1:8080")
loop {
match tcp.accept(listener) {
Ok(conn) -> {
let _ = task.spawn { ->
match tcp.read(conn, 4096) {
Ok(buf) -> {
let _ = tcp.write(conn, buf)
tcp.close(conn)
}
Err(_) -> tcp.close(conn)
}
}
}
Err(e) -> println("accept error: {e.message()}")
}
}
}
Err(e) -> println("listen error: {e.message()}")
}
}open in playgroundAddresses and ports
tcp.listen("host:port") binds where it is told:
"127.0.0.1:8080"listens on loopback: only programs on this machine can connect. This is the address for development servers."0.0.0.0:8080"listens on every network interface: other machines can connect.- Port
0asks the system for a free port;tcp.local_port(listener)gives the one it chose:
import tcp
fn main() {
when let Ok(listener) = tcp.listen("127.0.0.1:0") else {
panic("cannot listen")
}
println(tcp.local_port(listener) > 0)
}open in playgroundConnections that arrive before the program accepts them wait in the listener: up to 4,096 of them on Unix (or the system’s own limit, if that is lower), 128 elsewhere. A client beyond that is not refused; its system tries again, a second or more later.
A listener is also what http.serve serves on.
While it does, the listener is the server’s alone: tcp.accept,
tcp.accept_tls and tcp.accept_tls_mtls on it return
Err(Other("the listener is served by http.serve")) at once, and
so does an accept that was waiting on it when the server started. When
the task that serves has been cancelled, the listener is the program’s
again: the next tcp.accept gets the next client.
Cooperative I/O
accept, connect, read, read_exact, and write wait without
holding up the scheduler: the operation runs on a thread of the I/O pool
and the task (or main) waits for its value while other tasks run. From
silt’s perspective the call looks synchronous.
Reading and writing at the same time. A plain TCP connection can be
read by one task and written by another at once: neither waits for the
other. (Two tasks that read the same connection take turns, as do two
that write.) A TLS connection is different: its reads and writes take
turns, so a task that waits in tcp.read holds up a tcp.write on the
same TLS connection until the read returns.
How much a read takes. tcp.read(conn, max) gives what one read
finds, at most max bytes and at most 64 KiB (65536 bytes) at once: a
larger max asks for no more. tcp.read_exact(conn, n) reads until it
has n bytes; a connection that ends before that is Err(Closed).
Neither sets memory aside for the number asked for, only for the bytes
that arrive. The same holds for a chunk of stream.tcp_chunks.
Stream lifetime
tcp.close(conn) shuts the connection down: a read or write that
another task has in flight on it returns, and later calls fail with
Closed. A closed connection answers nothing: tcp.peer_addr and
tcp.set_nodelay give Err(Closed) too. Without tcp.close the socket is closed when the last
reference to the connection is gone.
A read or write that nobody waits for ends its connection. When a
task stops waiting for tcp.read, tcp.read_exact or tcp.write
(SILT_IO_TIMEOUT passed, the task was cancelled or was the function of
a task.deadline whose time is up, or the program ended), the
connection is shut down exactly as by tcp.close. That is what lets the
blocked operation, and its thread, end. So a read that timed out cannot
be tried again: the next call on that connection gives Err(Closed).
So is the connection of an operation whose result arrives in the very
moment in which its task stops waiting: what the read had taken is gone
with the connection. The same holds for the
stream.tcp_* sources and sink when their pipeline is cut short. A
tcp.accept that nobody waits
for is woken and gives up; the listener stays usable. A tcp.accept_tls that
is given up while its client has connected but not finished the handshake
closes that connection.
Notes
- silt does not use async/await. Blocking calls run on the same I/O pool as
io.read_file,fs.list_dir, etc. (see the I/O pool).
TLS (opt-in feature)
The tcp-tls Cargo feature adds TLS support via rustls. Build silt with
--features tcp-tls to enable.
| Function | Signature | Description |
|---|---|---|
accept_tls | (TcpListener, Bytes, Bytes) -> Result(TcpStream, TcpError) | Accept a connection and complete the TLS server handshake using the supplied PEM cert chain + key |
accept_tls_mtls | (TcpListener, Bytes, Bytes, Bytes) -> Result(TcpStream, TcpError) | Like accept_tls, but also requires the client to present a cert chaining to the supplied CA PEM bundle (mutual TLS) |
connect_tls | (String, String) -> Result(TcpStream, TcpError) | Open a TCP connection then complete the TLS client handshake against hostname |
Returned TcpStream handles are interchangeable with plain TCP streams —
tcp.read, tcp.write, and tcp.close work identically. Trust anchors
for connect_tls come from the webpki-roots crate (Mozilla CA bundle).
Authentication is delegated to your system: silt does not add a separate
credential layer.
import bytes
import tcp
fn main() {
-- Open a TLS-protected connection and echo a small payload.
-- (Build silt with `--features tcp-tls` for these functions.)
match tcp.connect_tls("example.com:443", "example.com") {
Ok(conn) -> {
let _ = tcp.write(conn, bytes.from_string("hello"))
tcp.close(conn)
}
Err(e) -> println("connect_tls err: {e.message()}")
}
}
Mutual TLS (mTLS)
accept_tls_mtls adds client-certificate verification on top of
accept_tls. The fourth argument is a PEM-encoded bundle of CA
certificates — every connecting client must present a certificate that
chains to one of those CAs, or the TLS handshake fails and the call
returns Err(Tls(msg)). This is appropriate for service-to-service
APIs, internal mesh traffic, and any flow where you want cryptographic
client identity rather than bearer tokens.
Under the hood the server uses rustls’
WebPkiClientVerifier::builder(roots).build(), which requires
authentication by default (anonymous clients are rejected).
import bytes
import io
import tcp
fn main() {
-- Load the server identity and the CA bundle that signs your
-- clients. (Build silt with `--features tcp-tls` for this function.)
match io.read_file("server.crt") {
Ok(cert) -> match io.read_file("server.key") {
Ok(key) -> match io.read_file("clients-ca.crt") {
Ok(client_ca) -> match tcp.listen("0.0.0.0:8443") {
Ok(listener) -> match tcp.accept_tls_mtls(listener, cert, key, client_ca) {
Ok(conn) -> {
-- Peer is authenticated by cert at this point.
let _ = tcp.write(conn, bytes.from_string("hello, authenticated client"))
tcp.close(conn)
}
Err(e) -> println("mTLS handshake failed: {e.message()}")
}
Err(e) -> println("listen err: {e.message()}")
}
Err(e) -> println("ca load err: {e.message()}")
}
Err(e) -> println("key load err: {e.message()}")
}
Err(e) -> println("cert load err: {e.message()}")
}
}