TCPソケット
rontolisp パッケージは素のTCPネットワーキングのための4つの関数と、両側の
暗号化版 (tls-connect と tls-listen) を提供します。
これらは Common Lispの一部ではない ため、rontolisp: 修飾子で参照します
(パッケージを参照)。接続されたソケットはファイル
ストリームと同じハンドル空間の 双方向ストリームハンドル なので、標準の
ストリーム関数がそのまま使えます: read-line、write-line、read-byte、
write-byte、close。バッファリングされるファイル出力と異なり、ソケットへの
書き込みは即時に送信され (write-line は行ごとにフラッシュ)、相手が接続を
閉じると read-line は nil を返します。
| 関数 | 用途 |
|---|---|
rontolisp:tcp-connect | クライアント接続を開く: (rontolisp:tcp-connect host port) |
rontolisp:tcp-listen | リスニングソケットをバインドする: (rontolisp:tcp-listen port &optional host) |
rontolisp:tcp-accept | クライアント接続を待つ: (rontolisp:tcp-accept listener) |
rontolisp:tcp-local-port | バインドされたポートを読み取る (ポート 0 でlistenした後に便利) |
rontolisp:tls-connect | 暗号化されたクライアント接続を開く: (rontolisp:tls-connect host port) |
rontolisp:tls-listen | PKCS12 キーストアから暗号化されたリスニングソケットをバインドする: (rontolisp:tls-listen keystore password port &optional host) |
rontolisp:tls-listen-pem | PEM ファイルから暗号化されたリスニングソケットをバインドする: (rontolisp:tls-listen-pem cert-file key-file port &optional host) |
バックエンドのサポート。 インタプリタとJVMコンパイル済みクラスはJDKの ソケットクラスを使い、ホスト名とIPリテラルの両方を受け付けます。WASM バックエンドは componentモード専用 です (
--component、wasi:sockets@0.3.0経由): tcp関数はPreview 1 (コアモジュール) モードでは コンパイルエラーになり、ホストはIPv4リテラルでなければならず、component は非同期フラグに加えて-S tcp=y -S inherit-network=yを付けて実行する 必要があります。ブラウザプレイグラウンド ではすべてのtcp関数がエラーを シグナルします (ブラウザのサンドボックスには素のTCPがありません) — 下の 実行可能な例はブラウザの外でのみ動作します。共通の制限 (TCPのみ、 UDPなし) については tcp-connect のリファレンスページを参照してください。 TLS関数 (rontolisp:tls-connect、rontolisp:tls-listen、rontolisp:tls-listen-pem) はインタプリタ/JVM専用です (WASMバックエンドではコンパイルエラー)。
最初の往復
以下のスニペットは自己完結しています: エフェメラルポートでlistenし、 ループバックインターフェイス経由で自分自身に接続し、acceptしたハンドルを 通して1行をエコーバックします:
echoサーバー
実際のサーバーは固定ポートをバインドし、acceptループで接続を処理します。
以下を echo-server.lisp として保存してください
(examples/net/echo-server.lisp
としても同梱されています)。acceptしたハンドルは read-line が nil を返す
まで (クライアントが切断するまで) 1行ずつ読まれ、各行はそのまま書き戻され
ます:
(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?)")))
(if listener ...) のチェックはWASM componentバックエンドで重要です。
そこではバインドの失敗はエラーをシグナルする代わりに nil を返します
(インタプリタとJVMはシグナルします)。サーバーは永久にループします —
Ctrl-C で停止してください。
実行方法
インタプリタで:
rontolisp echo-server.lisp
JVMクラスにコンパイルして (クラス名は出力ファイル名から付きます):
rontolisp echo-server.lisp -o EchoServer.class
java EchoServer
WASM componentにコンパイルして (wasmtime 46+。ネットワークアクセスを許可する
2つの -S フラグに注意 — これらがなくてもcomponentは起動しますが、
tcp-listen が nil を返します):
rontolisp echo-server.lisp -o echo-server.wasm --component
wasmtime run -W gc=y -W component-model-more-async-builtins=y -S tcp=y -S inherit-network=y echo-server.wasm
どのバックエンドでサーブしていても、任意のTCPクライアント、たとえば
nc (netcat) で会話できます:
$ nc 127.0.0.1 7777
hello
hello
world
world
TLS接続
rontolisp:tls-connect は
tcp-connect の暗号化版です: 接続後に TLS ハンドシェイクを行い、同じ種類の
ストリームハンドルを返すため、read-line、write-line、read-byte、
write-byte、close がそのまま使えます。サーバー証明書は JDK デフォルトの
トラストストアで検証され、ホスト名も検証されます。自己署名証明書を受け入れる
には javax.net.ssl.trustStore システムプロパティで独自のトラストストアを
指定するか、:insecure t を渡して検証を完全にスキップします(開発用途のみ)。
詳細と手書き HTTPS の例はリファレンスページを参照してください:
(let ((sock (rontolisp:tls-connect "example.com" 443)))
... ; speak any TLS-wrapped protocol over the handle
(close sock))
サーバー側は
rontolisp:tls-listen です:
PKCS12 キーストアファイルを受け取り(自己署名キーストアを生成する 1 行の
keytool コマンドはリファレンスページに記載)、プレーンな
rontolisp:tcp-accept / rontolisp:tcp-local-port / close がそのまま使える
リスナーを返します。accept された各接続は最初の読み取りでハンドシェイクを
完了します。PKCS12 キーストアの代わりに PEM ファイル(certbot / OpenSSL の
出力)から直接提供するには、
rontolisp:tls-listen-pem
を使ってください。以下のサーバーの TLS 版は examples/ ディレクトリにあります —
https-hello.lisp
と
kv-server-tls.lisp:
(let* ((listener (rontolisp:tls-listen "tls-server.p12" "changeit" 8443))
(sock (rontolisp:tcp-accept listener)))
... ; serve the connection with the standard stream functions
(close sock)
(close listener))
usocket 互換シム
既存の Common Lisp のネットワークコードは、処理系固有のソケット API では
なく usocket ポータビリティ
ライブラリに対して書かれていることがほとんどです。rontolisp は
rontolisp:tcp-* 組み込みの上でそのコア API を再現する組み込みの
usocket パッケージを備えているため、そうしたコードがより少ない変更で
動きます -- Postmodern の cl-postgres ソケット層
(:element-type '(unsigned-byte 8) 付きの socket-connect +
socket-stream)はそのまま動きます:
rontolisp のソケットはストリームハンドルそのものなので、対応は直接的です:
usocket:socket-stream は恒等関数、usocket:socket-close は close、
usocket:socket-listen はホストが先の引数順を rontolisp:tcp-listen に
変換します。usocket:*wildcard-host*("0.0.0.0")と
usocket:*auto-port*(0)は usocket と同じように動き、get-local-* /
get-peer-* アクセサでポートとアドレスを読み戻せます。with-* 便利マクロ
(with-client-socket / with-connected-socket / with-server-socket /
with-socket-listener)は
本体の前後でソケットを束縛して閉じます。パッケージは最初の使用時に
ロードされ、組み込み ASDF システム "usocket" でもあります:
(asdf:load-system "usocket")、(ql:quickload :usocket)、サードパーティ
.asd の :depends-on ("usocket") はいずれもネットワークに触れずに
解決されます。
シムの制限(意図的なものです -- rontolisp のソケットモデルは lite です):
- TCP のみ。
:protocol :datagram(UDP)はエラーを通知し、socket-send/socket-receive/socket-shutdownは存在しません。 - インタープリタと JVM では型付きコンディション。
socket-connect/socket-listen/socket-acceptの失敗は型付きのusocket:socket-errorを通知します(メッセージは保持)。そのため(handler-case (usocket:socket-connect ...) (usocket:socket-error (e) ...))が動作します。サブタイプ(connection-refused-errorなど)も定義されますが、 再通知は常にsocket-errorを使います(そちらを捕捉してください)。WASM コンポーネントバックエンドではエラーは引き続き捕捉不能なトラップ(またはnilハンドル)で、wait-for-input的なコンディション処理は存在しません。 wait-for-inputとsocket-serverは存在しません(読み込みは ブロックします。accept ループは自分で書いてください)。with-*マクロはインタープリタと JVM ではあらゆる脱出時にソケットを 閉じます(unwind-protectに展開されます)。WASM コンポーネントバックエンドでは正常終了時のみ 閉じます(そこではunwind-protectがコンパイルできないため)。互換性の ためのキーワード引数(:element-type、:timeout、:nodelay、:reuse-addressなど)は受理して無視します。- バックエンド: インタープリタと JVM はフル対応。WASM は tcp 組み込みと
同じくコンポーネント専用で、アドレス系・peer 系アクセサは
nilを 返します。
その他のサンプル
examples/ ディレクトリ
にはさらに多くのソケットプログラムがあり、それぞれのヘッダーコメントに
バックエンドごとの実行手順が書かれています:
echo-client.lisp— echoサーバーに対応するクライアント: 標準入力の行をサーバーに送り、 応答をそれぞれ表示します。サーバーとクライアントは 別々の バックエンドで 実行できます。http-hello.lisp—curlやブラウザが理解する最小のHTTP/1.1サーバー。ソケットハンドル上のread-line/write-lineで構築されています (CRLFのリクエスト行はどの バックエンドでも普通の行として読めます)。TLS版のhttps-hello.lispは同じページをHTTPSで提供します (curl -k https://127.0.0.1:8443/)。kv-server.lisp— ミニチュアの Redis互換 インメモリkey-valueサーバー: 本物のredis-cliが動く程度のRESP2を話し (PING/SET/GET/DEL/INCR/...)、 ハッシュテーブルの状態は接続をまたいで保持されます。TLS版のkv-server-tls.lispは同じプロトコルをTLSで提供します (redis-cli --tls --insecure -p 6380)。
HTTPについては、どちらの向きでもソケット上でプロトコルを手書きする必要は
ありません。クライアント 側は rontolisp:fetch
(HTTPリクエストガイド参照)、サーバ 側は
rontolisp:http-handler がリクエストのパースとレスポンスの変換を引き受けます
(HTTP サーバガイド参照)。