TCP Sockets
The rontolisp package provides four functions for plain TCP networking,
plus encrypted variants for both sides (tls-connect and tls-listen). They
are not part of Common Lisp; reference them with the rontolisp:
qualifier (see Packages). A connected socket is a
bidirectional stream handle in the same handle space as file streams, so
the standard stream functions work on it directly: read-line, write-line,
write-string, write-char, read-char, read-byte, write-byte and
close. Unlike buffered file
output, socket
writes are sent immediately (write-line flushes per line), and read-line
returns nil once the peer has closed the connection. A socket carries BYTES:
write-string puts the string's UTF-8 bytes on the wire and read-char reads
one character back out of them, so read-byte and read-char can be mixed on
the same handle. At end of stream the reads follow their own Common Lisp
defaults: read-char and read-byte signal end-of-file unless you pass the
eof arguments — (read-char sock nil :eof) yields :eof — while read-line
answers nil, as it does on a file. The printing functions (print, princ, format) do not
take a socket; render with (format nil ...) and send the result with
write-line or write-string.
| Function | Purpose |
|---|---|
rontolisp:tcp-connect | Open a client connection: (rontolisp:tcp-connect host port) |
rontolisp:tcp-listen | Bind a listening socket: (rontolisp:tcp-listen port &optional host) |
rontolisp:tcp-accept | Wait for a client connection: (rontolisp:tcp-accept listener) |
rontolisp:tcp-local-port | Read the bound port back (useful after listening on port 0) |
rontolisp:tls-connect | Open an encrypted client connection: (rontolisp:tls-connect host port) |
rontolisp:tls-upgrade | Wrap an already-connected stream handle in TLS as a client: (rontolisp:tls-upgrade stream host) |
rontolisp:tls-listen | Bind an encrypted listening socket from a PKCS12 keystore: (rontolisp:tls-listen keystore password port &optional host) |
rontolisp:tls-listen-pem | Bind an encrypted listening socket from PEM files: (rontolisp:tls-listen-pem cert-file key-file port &optional host) |
Backend support. The interpreter and JVM-compiled classes use the JDK socket classes and accept hostnames or IP literals. The WASM backend is component-only (
--component, overwasi:sockets@0.3.0): the tcp functions compile in Preview 1 (core-module) mode but raise call-time errors, hosts must be IPv4 literals, and the component must run with-S tcp=y -S inherit-network=y(a tcp component always compiles in exception-handling mode). Combining the tcp functions withrontolisp:http-handlercompiles into one component and runs underwasmtime serve— add-S cli=yto the flags above (without it the serve linker reports thewasi:sockets@0.3.0tcp-socketresource as missing at instantiation). wasmCloud'swash dev(2.5.2) hosts that component too and provideswasi:sockets0.3, with one difference: a loopback destination names a per-workload virtual network, not the machine's real 127.0.0.1 — a connect to a loopback address only reaches a listener inside the same wasmCloud workload (such as a service component bound there), while non-loopback addresses go out over the real network. In the browser playground every tcp function signals an error (the browser sandbox has no raw TCP), so the runnable example below only works outside the browser. See the tcp-connect reference page for the shared limitations (TCP only, no UDP). The TLS client functions (rontolisp:tls-connectandrontolisp:tls-upgrade) run on the interpreter, the JVM and the WASM--componentbackend (add-S tls=yto the flags above); the TLS server functions (rontolisp:tls-listenandrontolisp:tls-listen-pem) are interpreter/JVM only — thewasi:tlsproposal defines no server interface, so they are a permanent compile error on every WASM target.
The programs in this guide are complete and self-contained: copy each one into
a file and run it with any backend. They use only the rontolisp:tcp-*
primitives; the usocket-compatible shim at the
end shows how the same programs look through the portability API that existing
Common Lisp code expects.
A first round trip
The snippet below is self-contained: it listens on an ephemeral port, connects to itself over the loopback interface, and echoes one line back through the accepted handle:
An echo server
A real server binds a fixed port and serves connections in an accept loop.
Save the following as echo-server.lisp. Each accepted handle is read line by
line until read-line returns nil (the client closed), and every line is
written straight back:
(let ((listener (rontolisp:tcp-listen 7777)))
(if listener
(progn
(write-line "echo server listening on 127.0.0.1:7777")
(do ((n 1 (+ n 1))) (nil)
(let ((sock (rontolisp:tcp-accept listener)))
(write-line (format nil "client ~a connected" n))
(do ((line (read-line sock) (read-line sock)))
((null line) (close sock) (write-line "client disconnected"))
(write-line line sock)))))
(write-line "tcp-listen failed (is port 7777 already in use?)")))
The (if listener ...) check matters on the WASM component backend, where a
failed bind returns nil instead of signaling an error (the interpreter and
JVM signal). The server loops forever — stop it with Ctrl-C.
Running it
On the interpreter:
rontolisp echo-server.lisp
Compiled to a JVM class (the class is named after the output file):
rontolisp echo-server.lisp -o EchoServer.class
java EchoServer
Compiled to a WASM component (wasmtime 46+; note the two -S flags that grant
network access — without them the component still starts, but tcp-listen
returns nil):
rontolisp echo-server.lisp -o echo-server.wasm --component
wasmtime run -S tcp=y -S inherit-network=y echo-server.wasm
Whichever backend serves, talk to it with any TCP client, for example
nc (netcat):
$ nc 127.0.0.1 7777
hello
hello
world
world
An echo client
The matching client connects to the server, sends every line read from
standard input, and prints each reply until stdin ends. Save it as
echo-client.lisp:
(let ((sock (rontolisp:tcp-connect "127.0.0.1" 7777)))
(if sock
(do ((line (read-line) (read-line)))
((null line) (close sock))
(write-line line sock)
(write-line (read-line sock)))
(write-line "cannot connect to 127.0.0.1:7777 (is echo-server.lisp running?)")))
Start echo-server.lisp first (any backend), then pipe input to the client —
the server and the client can each run on a different backend:
echo hello | rontolisp echo-client.lisp
An HTTP server
Because a socket handle is a line stream and read-line strips one trailing
carriage return, HTTP's CRLF-terminated request line and headers read as plain
lines (the blank line ending the headers reads as ""); response header lines
get their carriage return back via code-char 13 before write-line appends
the newline. That is enough to answer curl and browsers. Save the following
as http-hello.lisp — it serves a small HTML page showing the request line and
a running request counter, one connection per request:
;; Appends the carriage return of an HTTP CRLF line ending (write-line then
;; appends the newline).
(defun crlf (s)
(concatenate 'string s (format nil "~a" (code-char 13))))
;; Consumes the request headers up to the blank line that ends them.
(defun drain-headers (sock)
(do ((line (read-line sock) (read-line sock)))
((or (null line) (string= line "")))))
(let ((listener (rontolisp:tcp-listen 8080)))
(if listener
(progn
(write-line "http server listening on http://127.0.0.1:8080/")
(do ((n 1 (+ n 1))) (nil)
(let* ((sock (rontolisp:tcp-accept listener))
(request (read-line sock)))
(if request
(let ((body (format nil "<h1>hello from rontolisp</h1><p>request ~a: ~a</p>" n request)))
(drain-headers sock)
(write-line (crlf "HTTP/1.1 200 OK") sock)
(write-line (crlf "Content-Type: text/html") sock)
;; + 1: write-line terminates the body with a newline
(write-line (crlf (format nil "Content-Length: ~a" (+ (length body) 1))) sock)
(write-line (crlf "Connection: close") sock)
(write-line (crlf "") sock)
(write-line body sock)
(write-line (format nil "served request ~a: ~a" n request))))
(close sock))))
(write-line "tcp-listen failed (is port 8080 already in use?)")))
Run it on any backend and open http://127.0.0.1:8080/ in a browser or with
curl http://127.0.0.1:8080/.
For real HTTP work there is no need to hand-roll the protocol over a socket: the client side is
rontolisp:fetch(see the HTTP Requests guide), and the server siderontolisp:http-handlerparses requests and adapts responses for you (see the Serving HTTP guide). The hand-rolled server above is here to show the socket primitives, not as the recommended way to serve HTTP.
A miniature Redis server
A larger example: an in-memory key-value server that speaks enough of RESP2
(the Redis serialization protocol) that the real redis-cli connects and
works, and — like real Redis — also accepts "inline commands" (a plain
space-separated line), so telnet 127.0.0.1 6379 or nc 127.0.0.1 6379 work
too. Both framings arrive as CRLF-terminated lines, which read-line reads as
plain lines. The store is a hash table with string keys that survives across
connections. It supports (case-insensitive) PING, SET, GET, DEL,
EXISTS, INCR, KEYS, DBSIZE and QUIT. Save it as kv-server.lisp:
;; --- small string helpers ---------------------------------------------------
;; Appends the carriage return of a RESP CRLF line ending (write-line then
;; appends the newline).
(defun crlf (s)
(concatenate 'string s (format nil "~a" (code-char 13))))
;; "SET key value" -> ("SET" "key" "value")
(defun split-words (s)
(cond ((string= s "") nil)
(t (let ((p (position #\space s)))
(if p
(cons (subseq s 0 p) (split-words (subseq s (+ p 1))))
(list s))))))
;; ("hello" "world") -> "hello world"
(defun join-words (ws)
(cond ((null ws) "")
((null (cdr ws)) (car ws))
(t (concatenate 'string (car ws) " " (join-words (cdr ws))))))
;; t when s is a non-empty run of decimal digits (with an optional leading -).
(defun integer-string-p (s)
(let* ((n (length s))
(start (if (and (> n 0) (char= (char s 0) #\-)) 1 0)))
(and (> n start)
(do ((i start (+ i 1)))
((or (>= i n) (not (digit-char-p (char s i))))
(>= i n))))))
;; --- RESP replies -----------------------------------------------------------
(defun reply-simple (s sock)
(write-line (crlf (concatenate 'string "+" s)) sock))
(defun reply-error (s sock)
(write-line (crlf (concatenate 'string "-ERR " s)) sock))
(defun reply-int (n sock)
(write-line (crlf (format nil ":~a" n)) sock))
(defun reply-bulk (s sock)
(if s
(progn
(write-line (crlf (format nil "$~a" (length s))) sock)
(write-line (crlf s) sock))
(write-line (crlf "$-1") sock)))
(defun reply-array-header (n sock)
(write-line (crlf (format nil "*~a" n)) sock))
;; --- request framing --------------------------------------------------------
;; Reads one RESP bulk-string element: the "$<len>" header line, then the
;; payload line (the payload must not contain a newline).
(defun read-bulk (sock)
(let ((header (read-line sock)))
(if header (read-line sock) nil)))
(defun read-resp-array (count sock acc)
(if (<= count 0)
(reverse acc)
(let ((arg (read-bulk sock)))
(if arg
(read-resp-array (- count 1) sock (cons arg acc))
nil))))
;; Reads one command as a list of argument strings: a "*<n>" line starts a
;; RESP2 array (what redis-cli sends); anything else is an inline command
;; (what telnet/nc users type). nil at connection close.
(defun read-command (sock)
(let ((line (read-line sock)))
(cond ((null line) nil)
((string= line "") (read-command sock))
((char= (char line 0) #\*)
(let ((count (subseq line 1)))
(if (integer-string-p count)
(read-resp-array (parse-integer count) sock nil)
(list "!bad-frame"))))
(t (split-words line)))))
;; --- commands ---------------------------------------------------------------
;; Handles one command; returns nil after QUIT (closing the session).
(defun handle-command (args store sock)
(let ((cmd (string-upcase (car args)))
(key (cadr args)))
(cond ((string= cmd "PING")
(if key (reply-bulk key sock) (reply-simple "PONG" sock))
t)
((string= cmd "SET")
(if (and key (cddr args))
(progn
(setf (gethash key store) (join-words (cddr args)))
(reply-simple "OK" sock))
(reply-error "wrong number of arguments for 'set' command" sock))
t)
((string= cmd "GET")
(if key
(reply-bulk (gethash key store) sock)
(reply-error "wrong number of arguments for 'get' command" sock))
t)
((string= cmd "DEL")
(let ((removed 0))
(dolist (k (cdr args))
(when (gethash k store)
(remhash k store)
(incf removed)))
(reply-int removed sock))
t)
((string= cmd "EXISTS")
(reply-int (if (and key (gethash key store)) 1 0) sock)
t)
((string= cmd "INCR")
(let ((current (if key (or (gethash key store) "0") "0")))
(cond ((null key)
(reply-error "wrong number of arguments for 'incr' command" sock))
((integer-string-p current)
(let ((n (+ (parse-integer current) 1)))
(setf (gethash key store) (format nil "~a" n))
(reply-int n sock)))
(t (reply-error "value is not an integer or out of range" sock))))
t)
((string= cmd "KEYS")
(let ((pattern (or key "*"))
(keys nil))
(maphash (lambda (k v)
(if (or (string= pattern "*") (string= pattern k))
(push k keys)))
store)
(reply-array-header (length keys) sock)
(dolist (k keys)
(reply-bulk k sock)))
t)
((string= cmd "DBSIZE")
(reply-int (hash-table-count store) sock)
t)
((string= cmd "COMMAND")
;; redis-cli asks COMMAND DOCS on connect; an empty array satisfies it.
(reply-array-header 0 sock)
t)
((string= cmd "QUIT")
(reply-simple "OK" sock)
nil)
(t (reply-error (format nil "unknown command '~a'" (car args)) sock)
t))))
;; --- server loop ------------------------------------------------------------
(let ((store (make-hash-table))
(listener (rontolisp:tcp-listen 6379)))
(if listener
(progn
(write-line "mini-redis listening on 127.0.0.1:6379 (try: redis-cli -p 6379 ping)")
(do ((n 1 (+ n 1))) (nil)
(let ((sock (rontolisp:tcp-accept listener)))
(do ((args (read-command sock) (read-command sock)))
((or (null args) (not (handle-command args store sock)))
(close sock))))))
(write-line "tcp-listen failed (is port 6379 already in use? a real redis, perhaps)")))
Run it on any backend, then talk to it with the real redis-cli:
redis-cli -p 6379 set greeting hello
redis-cli -p 6379 get greeting
redis-cli -p 6379 incr counter
TLS connections
rontolisp:tls-connect is
the encrypted counterpart of tcp-connect: it performs a TLS handshake after
connecting and returns the same kind of stream handle, so read-line,
write-line, read-byte, write-byte and close work unchanged. The server
certificate is validated against the JDK default trust store and the hostname
is verified; point the javax.net.ssl.trustStore system properties at your
own trust store to accept self-signed certificates, or pass :insecure t to
skip verification entirely (development only). See the reference page for
details and an HTTPS-by-hand example:
(let ((sock (rontolisp:tls-connect "example.com" 443)))
... ; speak any TLS-wrapped protocol over the handle
(close sock))
To start TLS over a connection you already opened — the shape an HTTP
client library needs, since it connects (and possibly issues a proxy CONNECT)
before starting TLS — use
rontolisp:tls-upgrade: it
takes an existing socket handle plus the server name to verify against and
returns a new handle over the same connection. The bundled
cl+ssl shim system rides it, which
is what gives usocket+cl+ssl client libraries their https:// path.
The server side is
rontolisp:tls-listen: it
takes a PKCS12 keystore file and returns a listener that the plain
rontolisp:tcp-accept / rontolisp:tcp-local-port / close work on; each
accepted connection completes its handshake on the first read. To serve
straight from PEM files (certbot / OpenSSL output) instead of a PKCS12
keystore, use
rontolisp:tls-listen-pem.
The TLS client functions (tls-connect / tls-upgrade) also run on the
WASM --component backend, over wasmtime's experimental
wasi:tls@0.3.0-draft interface — add -S tls=y to the socket run flags;
failures answer nil there, :insecure signals (the draft exposes no
verification knob), and tls-upgrade upgrades the handle in place (it
answers the same handle, and requires that nothing was written to it yet). The
TLS server functions are interpreter/JVM only — a permanent compile error on
every WASM target, because the wasi:tls proposal defines no server
interface.
Both server programs below need a PKCS12 keystore holding the server key and
certificate. Generate a self-signed one for localhost with the JDK keytool
(or export one from OpenSSL with openssl pkcs12 -export):
keytool -genkeypair -alias rontolisp-tls -keyalg EC -dname CN=localhost \
-validity 365 -ext SAN=ip:127.0.0.1,dns:localhost \
-storetype PKCS12 -keystore tls-server.p12 \
-storepass changeit -keypass changeit
An HTTPS server
This is the TLS twin of the HTTP server above: identical once the listener
exists, because a tls-listen listener hands tcp-accept the same kind of
stream handle. tls-listen never returns nil — a missing keystore, a wrong
password or a busy port signals an error instead — so there is no nil check.
Save it as https-hello.lisp:
;; Appends the carriage return of an HTTP CRLF line ending (write-line then
;; appends the newline).
(defun crlf (s)
(concatenate 'string s (format nil "~a" (code-char 13))))
;; Consumes the request headers up to the blank line that ends them.
(defun drain-headers (sock)
(do ((line (read-line sock) (read-line sock)))
((or (null line) (string= line "")))))
(let ((listener (rontolisp:tls-listen "tls-server.p12" "changeit" 8443)))
(write-line "https server listening on https://127.0.0.1:8443/")
(do ((n 1 (+ n 1))) (nil)
(let* ((sock (rontolisp:tcp-accept listener))
(request (read-line sock)))
(if request
(let ((body (format nil "<h1>hello from rontolisp over TLS</h1><p>request ~a: ~a</p>" n request)))
(drain-headers sock)
(write-line (crlf "HTTP/1.1 200 OK") sock)
(write-line (crlf "Content-Type: text/html") sock)
;; + 1: write-line terminates the body with a newline
(write-line (crlf (format nil "Content-Length: ~a" (+ (length body) 1))) sock)
(write-line (crlf "Connection: close") sock)
(write-line (crlf "") sock)
(write-line body sock)
(write-line (format nil "served request ~a: ~a" n request))))
(close sock))))
Run it on the interpreter or the JVM, then (using -k because the certificate
is self-signed):
curl -k https://127.0.0.1:8443/
A TLS Redis server
The same is true of the key-value server: swap tcp-listen for tls-listen
and everything else is unchanged. This serves the RESP2 protocol over TLS on
port 6380 (like a real Redis with --tls-port). Save it as
kv-server-tls.lisp:
;; --- small string helpers ---------------------------------------------------
;; Appends the carriage return of a RESP CRLF line ending (write-line then
;; appends the newline).
(defun crlf (s)
(concatenate 'string s (format nil "~a" (code-char 13))))
;; "SET key value" -> ("SET" "key" "value")
(defun split-words (s)
(cond ((string= s "") nil)
(t (let ((p (position #\space s)))
(if p
(cons (subseq s 0 p) (split-words (subseq s (+ p 1))))
(list s))))))
;; ("hello" "world") -> "hello world"
(defun join-words (ws)
(cond ((null ws) "")
((null (cdr ws)) (car ws))
(t (concatenate 'string (car ws) " " (join-words (cdr ws))))))
;; t when s is a non-empty run of decimal digits (with an optional leading -).
(defun integer-string-p (s)
(let* ((n (length s))
(start (if (and (> n 0) (char= (char s 0) #\-)) 1 0)))
(and (> n start)
(do ((i start (+ i 1)))
((or (>= i n) (not (digit-char-p (char s i))))
(>= i n))))))
;; --- RESP replies -----------------------------------------------------------
(defun reply-simple (s sock)
(write-line (crlf (concatenate 'string "+" s)) sock))
(defun reply-error (s sock)
(write-line (crlf (concatenate 'string "-ERR " s)) sock))
(defun reply-int (n sock)
(write-line (crlf (format nil ":~a" n)) sock))
(defun reply-bulk (s sock)
(if s
(progn
(write-line (crlf (format nil "$~a" (length s))) sock)
(write-line (crlf s) sock))
(write-line (crlf "$-1") sock)))
(defun reply-array-header (n sock)
(write-line (crlf (format nil "*~a" n)) sock))
;; --- request framing --------------------------------------------------------
;; Reads one RESP bulk-string element: the "$<len>" header line, then the
;; payload line (the payload must not contain a newline).
(defun read-bulk (sock)
(let ((header (read-line sock)))
(if header (read-line sock) nil)))
(defun read-resp-array (count sock acc)
(if (<= count 0)
(reverse acc)
(let ((arg (read-bulk sock)))
(if arg
(read-resp-array (- count 1) sock (cons arg acc))
nil))))
;; Reads one command as a list of argument strings: a "*<n>" line starts a
;; RESP2 array (what redis-cli sends); anything else is an inline command
;; (what telnet/nc users type). nil at connection close.
(defun read-command (sock)
(let ((line (read-line sock)))
(cond ((null line) nil)
((string= line "") (read-command sock))
((char= (char line 0) #\*)
(let ((count (subseq line 1)))
(if (integer-string-p count)
(read-resp-array (parse-integer count) sock nil)
(list "!bad-frame"))))
(t (split-words line)))))
;; --- commands ---------------------------------------------------------------
;; Handles one command; returns nil after QUIT (closing the session).
(defun handle-command (args store sock)
(let ((cmd (string-upcase (car args)))
(key (cadr args)))
(cond ((string= cmd "PING")
(if key (reply-bulk key sock) (reply-simple "PONG" sock))
t)
((string= cmd "SET")
(if (and key (cddr args))
(progn
(setf (gethash key store) (join-words (cddr args)))
(reply-simple "OK" sock))
(reply-error "wrong number of arguments for 'set' command" sock))
t)
((string= cmd "GET")
(if key
(reply-bulk (gethash key store) sock)
(reply-error "wrong number of arguments for 'get' command" sock))
t)
((string= cmd "DEL")
(let ((removed 0))
(dolist (k (cdr args))
(when (gethash k store)
(remhash k store)
(incf removed)))
(reply-int removed sock))
t)
((string= cmd "EXISTS")
(reply-int (if (and key (gethash key store)) 1 0) sock)
t)
((string= cmd "INCR")
(let ((current (if key (or (gethash key store) "0") "0")))
(cond ((null key)
(reply-error "wrong number of arguments for 'incr' command" sock))
((integer-string-p current)
(let ((n (+ (parse-integer current) 1)))
(setf (gethash key store) (format nil "~a" n))
(reply-int n sock)))
(t (reply-error "value is not an integer or out of range" sock))))
t)
((string= cmd "KEYS")
(let ((pattern (or key "*"))
(keys nil))
(maphash (lambda (k v)
(if (or (string= pattern "*") (string= pattern k))
(push k keys)))
store)
(reply-array-header (length keys) sock)
(dolist (k keys)
(reply-bulk k sock)))
t)
((string= cmd "DBSIZE")
(reply-int (hash-table-count store) sock)
t)
((string= cmd "COMMAND")
;; redis-cli asks COMMAND DOCS on connect; an empty array satisfies it.
(reply-array-header 0 sock)
t)
((string= cmd "QUIT")
(reply-simple "OK" sock)
nil)
(t (reply-error (format nil "unknown command '~a'" (car args)) sock)
t))))
;; --- server loop ------------------------------------------------------------
(let ((store (make-hash-table))
(listener (rontolisp:tls-listen "tls-server.p12" "changeit" 6380)))
(write-line "mini-redis (TLS) listening on 127.0.0.1:6380 (try: redis-cli --tls --insecure -p 6380 ping)")
(do ((n 1 (+ n 1))) (nil)
(let ((sock (rontolisp:tcp-accept listener)))
(do ((args (read-command sock) (read-command sock)))
((or (null args) (not (handle-command args store sock)))
(close sock))))))
Run it on the interpreter or the JVM, then talk to it over TLS (--insecure
because the certificate is self-signed):
redis-cli --tls --insecure -p 6380 set greeting hello
redis-cli --tls --insecure -p 6380 get greeting
The usocket-compatible shim
Existing Common Lisp networking code is usually written against the
usocket portability library rather than
an implementation's own socket API. rontolisp ships a built-in usocket
package reproducing its core API over the rontolisp:tcp-* built-ins, so such
code runs with fewer changes -- Postmodern's cl-postgres socket layer
(socket-connect with :element-type '(unsigned-byte 8) + socket-stream)
works verbatim:
The mapping is direct because a rontolisp socket IS its stream handle:
usocket:socket-stream is the identity function, usocket:socket-close is
close, and usocket:socket-listen flips the host-first argument order onto
rontolisp:tcp-listen. usocket:*wildcard-host* ("0.0.0.0") and
usocket:*auto-port* (0) work as in usocket, the get-local-* /
get-peer-* accessors read ports and addresses back, and the with-*
convenience macros
(with-client-socket / with-connected-socket / with-server-socket /
with-socket-listener) bind and
close sockets around a body. The package loads on first use, and it is also
the built-in ASDF system "usocket": (asdf:load-system "usocket"),
(ql:quickload :usocket) and a third-party .asd's
:depends-on ("usocket") all resolve to it without touching the network.
The servers earlier in this guide, rewritten against this shim, wrap the
accept loop in with-server-socket (which closes each connection on every
exit) and take the listen failure as a typed usocket:socket-error:
(handler-case
(let ((listener (usocket:socket-listen "127.0.0.1" 7777 :reuse-address t)))
(write-line "echo server listening on 127.0.0.1:7777")
(do ((n 1 (+ n 1))) (nil)
(usocket:with-server-socket (sock (usocket:socket-accept listener))
(let ((stream (usocket:socket-stream sock)))
(write-line (format nil "client ~a connected" n))
(do ((line (read-line stream) (read-line stream)))
((null line) (write-line "client disconnected"))
(write-line line stream))))))
(usocket:socket-error (e)
(declare (ignore e))
(write-line "socket-listen failed (is port 7777 already in use?)")))
Limitations of the shim (deliberate -- rontolisp's socket model is lite):
- TCP only.
:protocol :datagram(UDP) signals an error, andsocket-send/socket-receive/socket-shutdowndo not exist. - Typed conditions on the interpreter and the JVM. A failure in
socket-connect/socket-listen/socket-acceptsignals a typedusocket:socket-error(message preserved), so(handler-case (usocket:socket-connect ...) (usocket:socket-error (e) ...))works there. The subtypes (connection-refused-error&c) are defined but the re-signal always usessocket-error(catch that). On the WASM component backend a failed connect/accept yields anilhandle instead of signaling, so thehandler-casepattern has nothing to catch there (test the handle fornilinstead). socket-optionsupports:receive-timeoutonly.(setf (usocket:socket-option sock :receive-timeout) seconds)sets a real per-socket read deadline on the interpreter and the JVM (viarontolisp:tcp-set-timeout); a timed-out read signals an ordinary catchableerror, notusocket:timeout-error, and the getter reads the set seconds back. On the WASM backends the write SIGNALS instead of installing a timeout that never fires. Every other option signals rather than being silently ignored.wait-for-inputis alisten-based poll: on the interpreter and the JVM the wait is real (listenasks the kernel receive buffer, polled every 10 ms until data arrives or:timeoutelapses;:ready-onlyworks). On the WASM backends it returns immediately claiming readiness when nothing is buffered — reads block anyway, so the common wait-then-read loop behaves identically, but a:timeoutpoll cannot be honoured there. Stream sockets only (a listener in the list signals), and wait-list objects do not exist.socket-serverdoes not exist (write your own accept loop).- The
with-*macros close the socket on every exit on the interpreter and the JVM (they expand overunwind-protect); this includes the WASM component backend (every tcp component already compiles in exception-handling mode). The compatibility keyword arguments (:element-type,:timeout,:nodelay,:reuse-address, ...) are accepted and ignored. - Backends: interpreter and JVM are full; WASM is component-only like the
tcp built-ins, and the address/peer accessors return real addresses and
ports there (a failure returns
nilinstead of signaling).
See also
The examples/net/ directory
ships these programs as ready-to-run files (written against the usocket shim),
each with per-backend run instructions in its header comment. For HTTP there is
no need to hand-roll the protocol over a socket in either direction: the
client side is rontolisp:fetch (see the
HTTP Requests guide), and for the server side
rontolisp:http-handler parses requests and adapts responses for you (see the
Serving HTTP guide).