Skip to content

tcp — native blocking sockets

The bottom layer of the tcp → net → serve stack. Binds directly to the platform socket API (POSIX / Winsock) with no C shim and no framework runtime, hands you raw TcpHandles and caller-owned buffers, and reports failures as status codes plus a classified TcpErrorKind rather than exceptions. Depends only on the nimony toolchain and libc sockets — no third-party packages.

Status — Solid and complete for what it is: IPv4 + IPv6/dual-stack, blocking I/O with a non-blocking + timeout escape hatch, the full common sockopt set, and SIGPIPE-safe writes all ship and are covered by tests. The IPv6 addressing gap is now closed: formatIpv6/parseIpv6Text give RFC 5952 canonical text (with :: compression and the ::ffff:a.b.c.d v4-mapped tail), and TcpEndpoint carries an address family — localTcpEndpoint/peerTcpEndpoint/acceptTcpWithPeer read a sockaddr_storage and return the real v6 address of an IPv6 peer.

Quickstart

nim
import tcp

initTcp()                          # no-op on POSIX; WSAStartup on Windows

# Family-agnostic client: resolves A + AAAA, connects to the first that accepts.
let fd = connectHostTcp("example.com", 80)
if not isValidTcp(fd):
  echo "connect failed: ", $lastTcpErrorKind()
  quit 1

let req = "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n"
discard writeAllTcp(fd, req.toCString(), req.len)

var buf = newString(4096)
let n = readTcp(fd, buf[0].addr, buf.len)   # >0 bytes, 0 EOF, <0 error
if n > 0:
  buf.setLen(n)
  echo buf

closeTcp(fd)
shutdownTcp()

API

The whole surface lives in tcp/native.nim and is re-exported by the tcp umbrella module. Handles are raw platform descriptors (cint on POSIX, uint/SOCKET on Windows); the library never owns or frees them for you beyond closeTcp.

Types & constants

symbolsignaturewhat it does
TcpHandlecint (POSIX) / uint (Windows)Raw platform socket descriptor.
InvalidTcpHandleconst TcpHandleSentinel for a failed/absent handle (-1 POSIX, not 0'u Windows).
TcpErrorKindenumPortable error class: tcpErrorNone, tcpErrorRetry, tcpErrorTimeout, tcpErrorInterrupted, tcpErrorDisconnected, tcpErrorRefused, tcpErrorUnreachable, tcpErrorUnknown.
TcpConnectStatusenumtcpConnectFailed, tcpConnectInProgress, tcpConnectConnected.
TcpAddressFamilyenumtcpFamilyV4 / tcpFamilyV6 — which address a TcpEndpoint carries.
TcpEndpointobjectfamily: TcpAddressFamily, address: uint32 (host-order IPv4, v4 only), v6: array[16, byte] (network-order, v6 only), scopeId: uint32, port: int.
TcpConnectResultobjecthandle: TcpHandle, status: TcpConnectStatus, errorCode: int.
TcpPollRequestobjectread: bool, write: bool — readiness interest passed to pollTcp.
TcpPollResultobjectread, write, error, hangup, invalid bools — decoded poll revents.

Lifecycle

symbolsignaturewhat it does
initTcpproc initTcp()Initialise sockets. No-op on POSIX; WSAStartup (idempotent) on Windows.
shutdownTcpproc shutdownTcp()Tear down. No-op on POSIX; WSACleanup on Windows.

Errors

symbolsignaturewhat it does
lastTcpErrorCodeproc lastTcpErrorCode(): intLast platform socket error for the current thread (errno / WSAGetLastError).
lastTcpErrorKindproc lastTcpErrorKind(): TcpErrorKindclassifyTcpErrorCode(lastTcpErrorCode()).
classifyTcpErrorCodeproc classifyTcpErrorCode(code: int): TcpErrorKindMap a raw code onto a portable TcpErrorKind.
tcpErrorWouldRetryproc tcpErrorWouldRetry(code: int): boolTrue for EAGAIN/EWOULDBLOCK/EINPROGRESS/EALREADY-class codes.
tcpErrorTimedOutproc tcpErrorTimedOut(code: int): boolTrue for a timeout code.
tcpErrorInterruptedproc tcpErrorInterrupted(code: int): boolTrue for EINTR.
tcpErrorDisconnectedproc tcpErrorDisconnected(code: int): boolTrue for disconnect/refused/unreachable classes.

Addressing (IPv4 & IPv6)

symbolsignaturewhat it does
formatIpv4proc formatIpv4(address: uint32): stringHost-order IPv4 → dotted-decimal "a.b.c.d" (high byte first).
parseIpv4Textproc parseIpv4Text(s: string; dest: var uint32): boolChar-walked, range-checked inverse of formatIpv4; rejects bad/empty octets and wrong dot counts.
formatIpv6proc formatIpv6(a: array[16, byte]): string16 network-order bytes → RFC 5952 canonical text: lowercase, no leading zeros, single longest :: zero-run (leftmost on ties), ::ffff:a.b.c.d for v4-mapped.
parseIpv6Textproc parseIpv6Text(s: string): tuple[ok: bool, bytes: array[16, byte]]Char-walked inverse of formatIpv6: full form, one :: compression, and a trailing IPv4 dotted quad. ok == false (bytes zeroed) on bad input.
resolveTcp4proc resolveTcp4(host: string; dest: var uint32): boolResolve the first IPv4 (getaddrinfo) address for host into host order.

Connecting

symbolsignaturewhat it does
connectTcp4proc connectTcp4(hostOrderAddr: uint32; port: int): TcpHandleBlocking IPv4 connect to a host-order address.
connectTcp4NonBlockingproc connectTcp4NonBlocking(hostOrderAddr: uint32; port: int): TcpConnectResultStart a non-blocking connect; poll writable then finishTcpConnect.
connectTcp4Timeoutproc connectTcp4Timeout(hostOrderAddr: uint32; port: int; timeoutMillis: int): TcpConnectResultBlocking connect with a timeout (non-blocking connect + pollTcp); handle restored to blocking on success.
connectLocalhostTcpproc connectLocalhostTcp(port: int): TcpHandleBlocking connect to 127.0.0.1:port.
connectLocalhostTcpNonBlockingproc connectLocalhostTcpNonBlocking(port: int): TcpConnectResultNon-blocking 127.0.0.1 connect.
connectHostTcpproc connectHostTcp(host: string; port: int): TcpHandleResolve host AF_UNSPEC (IPv4 and IPv6) and connect to the first address that accepts — family-agnostic client entry point.
finishTcpConnectproc finishTcpConnect(fd: TcpHandle): bool / proc finishTcpConnect(fd: TcpHandle; errorCode: var int): boolCheck whether a non-blocking connect completed (reads SO_ERROR).

Listening & accepting

symbolsignaturewhat it does
listenTcp4proc listenTcp4(hostOrderAddr: uint32; port: int; backlog = 128): TcpHandleBind + listen on a host-order IPv4 address (sets SO_REUSEADDR).
listenTcpproc listenTcp(port: int; backlog = 128): TcpHandlelistenTcp4(INADDR_ANY, …) — listen on all IPv4 interfaces.
listenTcp6proc listenTcp6(port: int; backlog = 128; dualStack = true): TcpHandleIPv6 wildcard listener; with dualStack clears IPV6_V6ONLY so one socket also accepts IPv4-mapped connections.
acceptTcpproc acceptTcp(listenFd: TcpHandle): TcpHandleAccept the next connection.
acceptTcpWithPeerproc acceptTcpWithPeer(listenFd: TcpHandle; peer: var TcpEndpoint): TcpHandleAccept and fill the peer's endpoint (family-aware — v4 or v6).

Reading & writing

symbolsignaturewhat it does
readTcpproc readTcp(fd: TcpHandle; buf: pointer; len: int): intrecv into a caller-owned buffer. Returns bytes read, 0 on EOF, <0 on error.
writeTcpproc writeTcp(fd: TcpHandle; buf: pointer; len: int): intsend from a caller-owned buffer. Uses MSG_NOSIGNAL on Linux/BSD (broken pipe → EPIPE, not a signal).
writeAllTcpproc writeAllTcp(fd: TcpHandle; buf: pointer; len: int): intRetry short writes until len bytes sent or error; returns bytes written.
closeTcpproc closeTcp(fd: TcpHandle)Close the handle (no-op on InvalidTcpHandle).
isValidTcpproc isValidTcp(fd: TcpHandle): boolfd != InvalidTcpHandle.
shutdownTcpReadproc shutdownTcpRead(fd: TcpHandle): boolHalf-close the receive side (SHUT_RD).
shutdownTcpWriteproc shutdownTcpWrite(fd: TcpHandle): boolSend EOF to the peer, keep receiving (SHUT_WR).
shutdownTcpBothproc shutdownTcpBoth(fd: TcpHandle): boolHalf-close both directions (SHUT_RDWR), handle stays open.

Non-blocking & readiness

symbolsignaturewhat it does
setTcpBlockingproc setTcpBlocking(fd: TcpHandle; blocking: bool): boolSwitch blocking/non-blocking (fcntl O_NONBLOCK / ioctlsocket FIONBIO).
setTcpNonBlockingproc setTcpNonBlocking(fd: TcpHandle): boolConvenience for setTcpBlocking(fd, false).
pollTcpproc pollTcp(fd: TcpHandle; request: TcpPollRequest; timeoutMillis: int; ready: var TcpPollResult): intWait for readiness via poll / WSAPOLL. Returns >0 ready, 0 timeout, <0 error; decodes revents into ready.
waitTcpReadableproc waitTcpReadable(fd: TcpHandle; timeoutMillis: int): boolTrue if readable within the timeout.
waitTcpWritableproc waitTcpWritable(fd: TcpHandle; timeoutMillis: int): boolTrue if writable within the timeout.
tcpSocketErrorCodeproc tcpSocketErrorCode(fd: TcpHandle; errorCode: var int): bool / proc tcpSocketErrorCode(fd: TcpHandle): intRead the pending SO_ERROR value (-1 if unreadable in the single-return form).

Socket options

symbolsignaturewhat it does
setTcpNoDelayproc setTcpNoDelay(fd: TcpHandle; enabled = true): boolToggle TCP_NODELAY (disable Nagle for latency-sensitive small writes).
setTcpKeepAliveproc setTcpKeepAlive(fd: TcpHandle; enabled = true): boolToggle platform-default SO_KEEPALIVE.
setTcpReuseAddrproc setTcpReuseAddr(fd: TcpHandle; enabled = true): boolToggle SO_REUSEADDR (rebind through TIME_WAIT).
setTcpReusePortproc setTcpReusePort(fd: TcpHandle; enabled = true): boolToggle SO_REUSEPORT; returns false on Windows (unsupported).
setTcpBroadcastproc setTcpBroadcast(fd: TcpHandle; enabled = true): boolToggle SO_BROADCAST.
setTcpLingerproc setTcpLinger(fd: TcpHandle; onoff: bool; seconds: int): boolConfigure SO_LINGERclose() blocks up to seconds to flush, or disable.
setTcpRecvBufferSizeproc setTcpRecvBufferSize(fd: TcpHandle; bytes: int): boolRequest SO_RCVBUF.
setTcpSendBufferSizeproc setTcpSendBufferSize(fd: TcpHandle; bytes: int): boolRequest SO_SNDBUF.
setTcpReadTimeoutMillisproc setTcpReadTimeoutMillis(fd: TcpHandle; millis: int): boolBound blocking reads (SO_RCVTIMEO); 0 restores default.
setTcpWriteTimeoutMillisproc setTcpWriteTimeoutMillis(fd: TcpHandle; millis: int): boolBound blocking writes (SO_SNDTIMEO); 0 restores default.
setTcpTimeoutMillisproc setTcpTimeoutMillis(fd: TcpHandle; millis: int): boolApply one timeout to both read and write.
setTcpOptionproc setTcpOption(fd: TcpHandle; level, optname: cint; intval: int): boolGeneric passthrough to set any integer-valued sockopt.
getTcpOptionproc getTcpOption(fd: TcpHandle; level, optname: cint; dest: var cint): boolGeneric passthrough to read any integer-valued sockopt.

Endpoints

symbolsignaturewhat it does
localTcpEndpointproc localTcpEndpoint(fd: TcpHandle): TcpEndpointThe socket's bound address + port (getsockname into sockaddr_storage, family-aware — v4 or v6), or an invalid endpoint.
peerTcpEndpointproc peerTcpEndpoint(fd: TcpHandle): TcpEndpointThe connected peer's address + port (getpeername, family-aware — v4 or v6), or invalid.
invalidTcpEndpointproc invalidTcpEndpoint(): TcpEndpointSentinel endpoint (address: 0, port: -1).

Design notes

  • Status codes, not exceptions. Every fallible call returns a bool, a signed count, or a TcpConnectResult; nothing raises. Classify raw codes with classifyTcpErrorCode / the tcpError* predicates, or read lastTcpErrorKind.
  • Caller-owned buffers. readTcp / writeTcp take a pointer + len and never allocate — you own the memory and the loop. writeAllTcp is the only retry helper.
  • One handle, two modes. A socket starts blocking; setTcpNonBlocking, pollTcp, and the *Timeout connect flip it as needed on the same descriptor.
  • Family-agnostic core. connectHostTcp (getaddrinfo AF_UNSPEC → try each) and listenTcp6(dualStack = true) (one IPv6 socket, IPV6_V6ONLY cleared) are the IPv4+IPv6 primitives the net/serve layers build on; the connect path passes the resolver's opaque sockaddr straight to connect, so no per-family struct is needed.
  • SIGPIPE-safe writes. writeTcp sends with MSG_NOSIGNAL on Linux/BSD, so writing to a broken pipe returns EPIPE (→ tcpErrorDisconnected) instead of killing the process. macOS/Windows lack the flag and fall back to 0.
  • Family-aware endpoints. TcpEndpoint carries a family tag: v4 keeps the fast uint32 address, v6 fills 16 v6 bytes plus scopeId. The endpoint readers pull getsockname/getpeername into a sockaddr_storage and branch on AF_INET6, so an accepted IPv6 peer surfaces its real address — rendered as RFC 5952 text via formatIpv6 (or the bracketed [::1]:port form up in net).

Requirements

  • Nimony toolchain (aoughwl fork). Pure nimony; no framework runtime.
  • libc sockets only — POSIX <sys/socket.h> / <netdb.h> / <poll.h> on Unix, ws2_32.dll (Winsock2, WSAPOLL) on Windows. No third-party package dependencies.

aoughwl — self-hosted platform for things n stuff. Contact / Support on Discord for access to the private backends.