socket
nimony/lib/std/socket.nim
const ReadChunk: int64How much is asked for per
read, and the buffer's initial size and growth step.const MaxBuffered: int64Ceiling on one socket's read buffer. Past this the peer is made to wait rather than us to allocate: a protocol that needs a whole message resident enforces its own, smaller limit long before this one.
type PeerAddr = object raw: Sockaddr_storage
proc family(p: PeerAddr): int64AF_INET,AF_INET6, or0for an address nothing filled in. Read throughSockAddr, whose declaration already knows whether this platform puts a length byte in front of the family.proc isV4(p: PeerAddr): boolproc isV6(p: PeerAddr): boolproc peerLen(p: PeerAddr): uint32The length of the sockaddr
p.rawactually holds — the size of its family, not the capacity of its storage. The kernel reports the same values for accept/recvfrom, and asendTo/connectcopies the address with this length so the callee reads what the kernel wrote: passingsizeof(Sockaddr_storage)makes sendto fail with EINVAL where the BSDs compare it againstsin_len.proc port(p: PeerAddr): int64The peer's port, or
0if there is no address. Network byte order read as two bytes, so no host-endianness assumption is needed.proc addIp(s: var string; p: PeerAddr)Append the address without the port. Appends nothing for an unfilled one.
proc ip(p: PeerAddr): stringThe address without the port:
"127.0.0.1","::1",""for none.proc $(p: PeerAddr): stringAddress and port in the form everything else writes them:
1.2.3.4:80for v4,[::1]:80for v6 — the brackets are not decoration, they are what keeps the port's colon apart from the address's.proc readAsync(fd: int32; buf: pointer not nil; len: int64; dl: Deadline): int64One
read, parked on the ring. The bytes read,0at end of stream, or negative on error —IoTimedOutwhen the deadline arrived first.resultis initialised before its address is taken because the ring writes through that pointer when it completes, and the compiler will not hand out the address of something it cannot prove is initialised.proc writeAsync(fd: int32; buf: pointer not nil; len: int64; dl: Deadline): int64One
write, parked on the ring.proc acceptAsync(listenFd: int32; peer: var PeerAddr; dl: Deadline): int64One
accept, parked on the ring. The accepted fd, or negative on error —IoTimedOutwhen the deadline arrived first.peeris filled with who connected, at no extra syscall: the kernel wrote the address as part of the accept and this is only the hand-off. It is left untouched when the accept fails.The returned fd is not non-blocking yet —
setNonBlockingit before handing it to aSocket, or every subsequent op on it blocks a lane.proc recvFromAsync(fd: int32; buf: pointer not nil; len: int64; dl: Deadline; peer: var PeerAddr): int64One
recvfrom, parked on the ring. The bytes of one datagram, or a negative result —IoTimedOutwhen the deadline arrived first.peeris filled with the sender, at no extra syscall: the kernel wrote the address as part of the recvfrom and this is only the hand-off. It is left untouched when the receive fails.proc sendToAsync(fd: int32; buf: pointer not nil; len: int64; sa: Sockaddr_storage; saLen: uint32; dl: Deadline): int64One
sendto, parked on the ring. The bytes of the datagram sent, or a negative result —IoTimedOutwhen the deadline arrived first.type Listener = object fd: int32 deadline: Deadline
proc =destroy(l: Listener)proc =wasMoved(l: var Listener)proc =copy(dest: var Listener; src: Listener)Two listeners over one descriptor, whichever dies first closes it. Like
Socket, this is move-only.proc listen(port: uint16; backlog: int64; deadline: Deadline): ListenerA bound, listening socket on
port;0asks the kernel to pick one, andboundPortreports what it chose. Binding and listening are synchronous — nothing for the ring to wait on — so onlyacceptparks.proc boundPort(l: Listener): uint16proc close(l: var Listener)Close now rather than at the end of the scope, idempotently, like
Socket.close.proc accept(l: Listener; dl: Deadline): SocketTake the next pending connection, parked on the ring until a peer appears or the deadline arrives. The returned
Socketis already non-blocking, is owned by its descriptor, and knows who connected:peerrecords the address the kernel's accept already wrote, at no extra syscall — the ask an access log, a rate limiter and everyX-Forwarded-Fortrust decision cannot reconstruct afterwards.0is not a valid result here — a peer is a descriptor, not an end of stream — so its negative results raise, exactly asread/writereport their errors.proc connect(host: string; port: uint16; dl: Deadline): SocketA non-blocking
Socketconnected tohost:port, parked on the ring until the handshake completes or the deadline arrives. RaisesTimeoutErrorif the deadline arrived first,IOErrorif the address is not a dotted-quad literal or the peer refused.dlbounds the connection attempt only: a socket that is left with a deadline from its connect would expire every later read or write once that moment passed — the connect budget is not a lifetime. The returned Socket has no standing deadline; a protocol sets one per request viarenew. Itspeeris who we connected to, taken from the same address that went to the ring.type Socket = object fd: int32 deadline: Deadline peer: PeerAddr rbuf: seq[char] rlen: int64 rpos: int64 wbuf: seq[char]
proc =destroy(s: Socket)Closing is not something a caller has to remember. A
Socketowns its fd, so the fd goes when the socket does — the leak that mattered was never the buffers, it was the descriptor on the error path nobody wrote.Like the explicit
close, this must run on the thread that submitted the socket's in-flight ops, because the ring's slot arenas are per-lane. That is what a socket belonging to its lane for life buys: it also dies there.A hand-written
=destroyreplaces the field-wise one, so the buffers are freed here too.proc =wasMoved(s: var Socket)The fd went with the destination, so this one must not close it.
-1rather than the default zeroing:0is a perfectly good descriptor — stdin — and a moved-from socket that closes it is the kind of bug that surfaces somewhere else entirely.proc =dup(x: Socket): Socketproc =copy(dest: var Socket; src: Socket)Move-only: two sockets over one fd means two owners of one descriptor, and whichever is destroyed first closes it under the other.
proc initSocket(fd: int32; deadline: Deadline): Socketfdmust already be non-blocking. The deadline has no default: a socket with no budget is one a quiet peer can hold forever.The read and write buffers start empty and are allocated by the first
fill/scratchthat needs one. Allocating them here meant two 8 KB buffers per socket for a peer that may never say anything — and a server holding idle connections, or a client holding a hundred thousand of them, pays that per connection whether it is used or not.proc budget(s: Socket; dl: Deadline): DeadlineThe deadline an operation actually runs under: the socket's, or a tighter one the caller supplied.
earliermeans a caller can never widen the budget by passing a later instant.proc renew(s: var Socket; deadline: Deadline)Start a fresh budget. A kept-alive connection serves many exchanges and each gets its own, or the first would spend the whole socket's allowance on behalf of all of them.
proc close(s: var Socket)Close now rather than at the end of the scope. Idempotent, and the destructor then has nothing left to do — which is what a protocol above wants when "the peer said goodbye" happens well before the socket's owner goes out of scope.
proc buffered(s: Socket): int64Bytes read but not yet consumed.
proc peek(s: Socket): openArray[char]The bytes read but not yet consumed, as a view into the buffer. This is what a parser is handed: it neither slices nor owns anything, and what it does not consume stays put for the next read to extend.
proc consume(s: var Socket; n: int64)Drop the first
nbytes ofpeek— what the parser just used.proc fill(s: var Socket; dl: Deadline): int64One read into the buffer. The bytes added, or
0at end of stream — which is a peer that has finished, not a failure, so it is a value and not a raise. Compacts first, then grows up toMaxBuffered.Raises
FullErrorwhen the buffer is at its ceiling and still full: the peer has sent more of something than we agreed to hold, and growing further would let it pick our memory use.TimeoutErrorwhen the deadline arrived first,IOErrorotherwise.proc take(s: var Socket; dest: var openArray[char]; limit: int64): int64Copy up to
limitbytes of what is already buffered intodest, and consume them. No IO, so no deadline: this is the half of a read that a protocol doing its own framing wants afterfillhas put bytes in.proc read(s: var Socket; dest: var openArray[char]; dl: Deadline): int64Fill
dest, starting with whatever is already buffered. The bytes copied — fewer than asked for only at end of stream. Raises whatfillraises.proc readLine(s: var Socket; dl: Deadline): stringOne line, up to and including its
\n, minus the delimiter and any trailing\r. An empty result is end of stream — which, likereadreturning0, is an end and not a failure. A line can arrive split acrossfills, so this loops on the buffer rather than assuming one read held the whole line.proc toErr(n: int64): ErrorCodeWhat a negative ring result means. Exported because a protocol above may have to decide for itself whether a
0— the peer closing — is an end or a truncation, and wants the same mapping for the rest.proc writeAll(s: var Socket; buf: pointer not nil; len: int64; dl: Deadline)Write every byte or raise. A short write is normal — the peer's window is not our business — so it loops rather than reporting partial success that a caller would have to unpick. There is no partial success to report: either every byte went or this raised.
proc write(s: var Socket; data: openArray[char]; dl: Deadline)Send bytes already in memory. Nothing is copied.
proc writeLine(s: var Socket; line: string; dl: Deadline)write(line)followed by a\n. Two sends rather than one: there is no persistent frame here the way there is forscratch/flush, and a line echoed back is not a protocol that needs its bytes coalesced — but both the payload and its terminator are guaranteed to go out, each whole.proc scratch(s: var Socket; n: int64): var openArray[char]A writable staging area of at least
nbytes, kept between calls so serializing a message does not allocate per message. Valid until the nextscratch;flushis what sends it.proc flush(s: var Socket; n: int64; dl: Deadline)Send the first
nbytes of the staging area in one write. One write and not several because a frame split across writes is a frame a peer can be left half-way through.type UdpSocket = object fd: int32 deadline: Deadline peer: PeerAddr
proc =destroy(s: UdpSocket)proc =wasMoved(s: var UdpSocket)-1rather than the default zeroing:0is a perfectly good descriptor — stdin — and a moved-from socket that closes it is a bug that surfaces somewhere else entirely.proc =dup(s: UdpSocket): UdpSocketproc =copy(dest: var UdpSocket; src: UdpSocket)Move-only: two sockets over one descriptor means two owners, and whichever is destroyed first closes it under the other.
proc budget(s: UdpSocket; dl: Deadline): Deadlineproc createSocket(domain: Domain; typ: SockType; proto: Protocol; dl: Deadline): int32A non-blocking socket of
domain/typ/proto, created entirely through the ring: socket(2) and the non-blocking flag are ops the polling thread performs, so this proc blocks on nothing and makes no syscall of its own. In that it is the passive sibling ofioring.listenTcp— which still binds synchronously, because a pre-assembled listener has nothing between the syscalls to expose. Delivers the fd, or raisestoErr'sErrorCodewhen the socket or the flag cannot be made.proc createUdp(port: uint16; dl: Deadline): int32A non-blocking UDP socket bound to the wildcard address on
port;0asks the kernel to pick one (boundPortreports it), the passive sibling ofioring.listenTcp— and, likecreateSocket, built only from ring ops: nothing here blocks and nothing is this proc's own syscall.proc openUdp(port: uint16; deadline: Deadline): UdpSocketA bound, non-blocking UDP socket on
port;0asks the kernel to pick one, reported byboundPort. The whole creation is ring ops — the socket, its flag, its bind — so call this from a.passiveproc (a task undersubmit), never from a plain thread's top level: the suspends that await the polling thread's instant completions need a continuation to resume into. Only the datagram exchange parks beyond that.proc boundPort(s: UdpSocket): uint16proc close(s: var UdpSocket)Close now rather than at the end of the scope, idempotently, like
Socket.close.proc udpConnect(s: var UdpSocket; host: string; port: uint16; dl: Deadline)Connect an existing, bound UDP socket to
host:port. Connecting a datagram socket does no traffic: it just records the peer, which is the destination every latersendDatagramgoes to and the only source a laterrecvDatagramaccepts — the UDP analogue ofaccept, which is why the ring's stream-shaped read/write submit paths work here unmodified. The peer the kernel recorded is also the ones.peerreports.proc recvFrom(s: var UdpSocket; dest: var openArray[char]; peer: var PeerAddr; dl: Deadline): int64Receive one datagram into
destand report who sent it inpeer. The bytes it held, which is not a stream count: a datagram is a whole message, so this is a full message (never a0-means-closed condition). A datagram larger thandestis truncated to it — that is UDP's contract, not something this proc can repair. This is the server form — it works on an unconnected socket and answers "where do I send the reply" for free. RaisesTimeoutErrorif the deadline arrived first.proc recvDatagram(s: var UdpSocket; dest: var openArray[char]; dl: Deadline): int64Receive one datagram into
destfrom the peerudpConnectrecorded. The bytes it held, with the same whole-message semantics asrecvFrom. This is the connected form — the datagram equivalent ofread, and about the only case where a datagram count can be mistaken for a stream count, which is why a caller that has connected uses this and a client rarely does. RaisesTimeoutErrorif the deadline arrived first.proc sendDatagram(s: var UdpSocket; data: openArray[char]; dl: Deadline)Send
dataas one datagram to the peerudpConnectrecorded. A datagram is atomic: the whole buffer is one message, unlike the stream side where a short write is the peer's window. RaisesTimeoutErrorif the deadline arrived first.proc sendTo(s: var UdpSocket; data: openArray[char]; to: PeerAddr; dl: Deadline)Send
dataas one datagram toto, an address this socket did necessarily not connect to — the reply half ofrecvFrom, which is how a server answers a client it never met. The address is copied into the ring op, sotodoes not need to outlive the call.proc sendTo(s: var UdpSocket; data: openArray[char]; host: string; port: uint16; dl: Deadline)sendTo(data, to)for a dotted-quadhost:portliteral: builds the peer address the wayudpConnectdoes and sends to it. Returns early whenhostis not a dotted-quad literal — raiseIOErrorlikeconnectdoes.