v1.2.2 — 2026-06-07; non-blocking mode + readiness check added later.
TCP and UDP socket primitives, plus non-blocking mode and readiness polling. Uses POSIX sockets on Linux/macOS and Winsock2 on Windows. v1.2.2 — 2026-06-07; TLS client support added later.
TCP, TLS, and UDP socket primitives. Uses POSIX sockets on Linux/macOS and Winsock2 on Windows; TLS via OpenSSL.
No extra packages required. Enabled by default (-DBUILD_MODULE_NETWORK=ON).
(import (curry network))(tcp-connect host port) ; connect to host:port, return (in-port . out-port)tcp-connect returns a pair of ports — (in-port . out-port) — not a
single socket handle. A Curry port is one-directional, and a TCP connection
needs both read and write, so you get two independent ports over the same
underlying connection. Close each with close-port when done.
(tcp-listen port) ; listen on port (all interfaces), return a raw socket handle
(tcp-listen port backlog) ; listen with explicit backlog
(tcp-accept listen-sock) ; block until a client connects, return (in-port . out-port)
(tcp-close listen-sock) ; close the *listening* socket (not a port pair)tcp-listen's own return value is a raw listening-socket handle (not a
stream, so not a port) — close it with tcp-close. Each accepted
connection from tcp-accept is a port pair like tcp-connect's, closed
with close-port on each end.
(tcp-connect-tls host port) ; connect + TLS handshake, return (in-port . out-port)Same port-pair contract as tcp-connect, but the connection is TLS-wrapped
via OpenSSL. Certificate verification is on by default (system trust
store, SSL_VERIFY_PEER) — connecting to a host with an invalid or
self-signed certificate raises rather than silently connecting insecurely.
SNI (SSL_set_tlsext_host_name) is sent automatically using the host
argument, needed by most modern HTTPS servers that multiplex by hostname.
No TLS server side (tls-listen/tls-accept) yet — client-side was the
actually-blocking gap (talking to HTTPS-only APIs over raw sockets); see
issue #14 if you need
server-side TLS.
(udp-socket) ; create a UDP socket
(udp-bind sock port) ; bind to a local port
(udp-send sock data host port) ; send bytevector data to host:port
(udp-recv sock max-bytes) ; receive up to max-bytes; returns (data host port)UDP is datagram-oriented, not stream-oriented, so udp-socket stays a raw
handle — there's no port wrapping for it.
(socket-set-nonblocking! sock) ; fcntl O_NONBLOCK
(socket-ready? sock) ; immediate poll, #t if data/connection pending
(socket-ready? sock timeout-ms) ; block up to timeout-ms waiting for readinessBoth accept either a raw socket handle (tcp-listen's/udp-socket's
return value) or a port (tcp-connect's/tcp-accept's in-port or
out-port) — whichever you have on hand. socket-ready? on a listening
socket means "tcp-accept would not block"; on a connected port it means
"there's data to read without blocking."
This is deliberately not a full epoll/kqueue event-loop reactor. curry's actors are real OS threads (not green threads/coroutines — those are a separate, unimplemented roadmap item), so a full reactor integration wouldn't buy much over the current thread-per-actor model yet. What's here covers a real, immediately useful case: multiplexing a handful of sockets on one thread without needing an actor per connection. See issue #15 for the fuller reactor, if/when green threads make it worthwhile.
(import (curry network))
(define conn (tcp-connect "example.com" 80))
(define in (car conn))
(define out (cdr conn))
(write-string "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n" out)
(flush-output-port out)
(let loop ((line (read-line in)))
(unless (eof-object? line)
(display line) (newline)
(loop (read-line in))))
(close-port in)
(close-port out)(import (curry network))
(define conn (tcp-connect-tls "example.com" 443))
(define in (car conn))
(define out (cdr conn))
(write-string "GET / HTTP/1.0\r\nHost: example.com\r\nConnection: close\r\n\r\n" out)
(flush-output-port out)
(let loop ((line (read-line in)))
(unless (eof-object? line)
(display line) (newline)
(loop (read-line in))))
(close-port in)
(close-port out)(import (curry network))
(import (scheme base))
(define listener (tcp-listen 7777))
(display "Listening on port 7777...\n")
(let loop ()
(define conn (tcp-accept listener))
(define in (car conn))
(define out (cdr conn))
(spawn (lambda ()
(let echo ()
(define line (read-line in))
(unless (eof-object? line)
(write-string line out)
(write-string "\n" out)
(flush-output-port out)
(echo)))
(close-port in)
(close-port out)))
(loop))(import (curry network))
(import (scheme base))
(define listener (tcp-listen 7778))
(define conns '()) ; list of (in . out) pairs currently connected
(let loop ()
;; Accept a new connection if one is pending, without blocking.
(when (socket-ready? listener)
(set! conns (cons (tcp-accept listener) conns)))
;; Service whichever existing connections have data ready.
(set! conns
(filter
(lambda (conn)
(if (socket-ready? (car conn))
(let ((line (read-line (car conn))))
(if (eof-object? line)
(begin (close-port (car conn)) (close-port (cdr conn)) #f)
(begin (write-string line (cdr conn))
(write-string "\n" (cdr conn))
(flush-output-port (cdr conn))
#t)))
#t))
conns))
(loop))Currently broken —
udp-recvusesrecv(), notrecvfrom(), so it cannot actually report the sender's address; it returns only the raw bytevector today, not(data host port)as shown below. This example documents the intended contract. See issue #16.
(import (curry network))
(define sock (udp-socket))
(udp-bind sock 9999)
(let loop ()
(define-values (data host port) (apply values (udp-recv sock 1024)))
(udp-send sock data host port)
(loop))tcp-connect/tcp-acceptreturn(in-port . out-port)pairs — real Curry ports usable withread-line,write-string,read-char, etc. Remember toflush-output-portafter writing before expecting a reply (output ports are buffered by default, same as any other Curry port).tcp-listenbinds to0.0.0.0/::(all interfaces, dual-stack). To bind to a specific interface, use the raw C extension API.tcp-connect-tlsrequiresBUILD_MODULE_NETWORK=ON(default) and links OpenSSL (already a dependency via(curry crypto)) — no extra CMake flag.