Socket Primitives¶
The raw BSD socket calls, as Result-returning primitives: sock_tcp_connect, sock_tcp_listen, sock_tcp_accept, sock_send, sock_recv, sock_close, sock_dup, the timeouts, the UDP trio, and sock_dns_resolve.
Import¶
use <net/socket>
Overview¶
net/socket is the low-level half of the network stack. It exists because the byte-level work cannot be written in Sushi: an array or a struct may not cross the C ABI (CE5003), a u8[] has no way to yield a pointer, and a ptr is opaque, so sockaddr could neither be built nor read.
Most programs want <net/tcp>, <net/udp>, <net/dns> or <net/url> instead. Those wrap these primitives in types and are what the examples below build on. Reach for <net/socket> when you want a descriptor without a wrapper.
Every function answers Result@(T, NetError). A descriptor is a bare i32, and closing it is the caller's business: there is no RAII for a socket, exactly as there is none for a file.
Types¶
NetError is a predefined enum whose HOME is <net/error>: that import
brings the bare name -- and so does every net module above this one, which re-exports
it -- and every function here answers it:
public enum NetError:
ConnectionRefused ConnectionReset TimedOut
Closed AddressInUse AddressNotAvailable
NetworkUnreachable HostUnreachable ResolveFailed
PermissionDenied TooManyOpen InvalidAddress
Interrupted MessageTooLarge Other
Two of the mappings are worth knowing:
TimedOutcoversEAGAIN. Every socket this module creates is blocking, soEAGAINfrom a read or a write can only mean the timeout you asked for expired — that is how POSIX reports a socket timeout,connectreportingETIMEDOUTinstead.ClosedcoversEBADF. An operation on a descriptor that was already closed names the condition rather than falling through toOther.
ResolveFailed is the one variant no errno reaches: getaddrinfo answers with an EAI_* code, which is not errno and whose sign is not even the same on macOS and Linux.
Functions¶
sock_tcp_listen(string host, i32 port, i32 backlog) -> Result@(i32, NetError)¶
Bind a listening socket. An empty host means the wildcard address. Port 0 asks the kernel to choose one, and sock_local_port reads back what it chose — which is what lets a test bind without naming a port. SO_REUSEADDR is always set, so a port whose last connection is still in TIME_WAIT binds again at once.
sock_local_port(i32 fd) -> Result@(i32, NetError)¶
The port a descriptor actually bound, through getsockname.
use <net/socket>
fn main() i32:
match sock_tcp_listen("127.0.0.1", 0, 8):
Result.Ok(fd) ->
let i32 port = sock_local_port(fd).realise(-1)
println("listening on {port}")
sock_close(fd).realise(-1)
Result.Err(_) -> println("could not listen")
return Result.Ok(0)
sock_tcp_connect(string host, i32 port) -> Result@(i32, NetError)¶
Connect to a host and port. The host may be a name or a numeric address; a name is resolved and every answer is tried in turn. There is no connect timeout — that needs a non-blocking socket and select — so an address that answers nothing waits for the kernel to give up.
sock_tcp_accept(i32 fd) -> Result@(i32, NetError)¶
Take the next connection waiting on a listener. Give the listener a timeout first and this answers TimedOut instead of waiting forever.
sock_send(i32 fd, u8[] data) -> Result@(i32, NetError)¶
Write bytes, and answer how many went. One write may take fewer bytes than it was offered; send_all() in <net/tcp> is the loop. The buffer stays the caller's: this borrows it and never frees it.
sock_recv(i32 fd, i32 max) -> Result@(u8[], NetError)¶
Read what one read gives, up to max bytes.
An empty answer means the peer closed cleanly. recv sets no errno at the end of a stream, so reporting an error there would report a stale one. A timeout is the other case and answers Err(TimedOut), which leaves the two unambiguous — so while data.len() > 0 is a correct read loop.
sock_close(i32 fd) -> Result@(i32, NetError)¶
Close a descriptor.
sock_dup(i32 fd) -> Result@(i32, NetError)¶
A second descriptor over the same open socket: dup(2). It is the socket twin of <io/files>'s fd_dup, and TcpListener.share() is written on it. Closing one descriptor leaves the other open, and a connection waiting on the port goes to whichever one accepts first.
sock_peer_ip(i32 fd) -> Result@(string, NetError) and sock_peer_port(i32 fd) -> Result@(i32, NetError)¶
Who is at the other end. The address is rendered numerically and asks no resolver, so neither call makes a network request. They are separate so that a test can assert the address — which is fixed — without asserting an ephemeral port.
sock_set_recv_timeout(i32 fd, i32 ms) and sock_set_send_timeout(i32 fd, i32 ms)¶
Bound how long a read or a write may wait; both answer Result@(i32, NetError). A bound that expires answers NetError.TimedOut. A listening socket honours the receive bound, which is what gives sock_tcp_accept a bound too.
sock_udp_bind(string host, i32 port) -> Result@(i32, NetError)¶
Bind a datagram socket. SO_REUSEADDR is deliberately not set here: on a datagram socket it means several sockets sharing a port, which is a different thing to ask for.
sock_udp_send_to(i32 fd, u8[] data, string host, i32 port) -> Result@(i32, NetError)¶
Send one datagram. The destination is resolved on every call.
sock_udp_recv_from(i32 fd, i32 max) -> Result@(Datagram, NetError)¶
Wait for one datagram and answer it with its sender. Datagram is a predefined struct:
public struct Datagram:
u8[] data
string peer_ip
i32 peer_port
The sender rides along with the bytes because an unconnected datagram socket has no getpeername: the sender exists only at the instant its datagram arrives.
sock_dns_resolve(string host) -> Result@(string[], NetError)¶
Resolve a name to numeric address texts. <net/dns> reads these into IpAddr, which is what most callers want.
See also¶
- TCP —
TcpStreamandTcpListenerover these primitives - UDP —
UdpSocket - DNS — a name resolved into typed addresses
- IP addresses —
IpAddr, parse and format - URLs — lexical URL splitting