Common Lispでsocketプログラミング(前編)
Common Lispでsocket通信をするには、usocketというライブラリがよく使われます。
今回は、前編・後編に分けて、usocketの使い方を紹介します。
まずは、Sid Heroorさんの記事 Short guide to TCP/IP Client/Server programming in Common Lisp using usocketsです。
この記事は、Common Lispで、TCP/IPクライアントサーバー型のプログラミングを書くための入門ガイドです。この入門ガイドでは、Common Lispライブラリusocketを用いて進めます。
Common Lispでsocketプログラミングをしてみようと思ったときに、先例がなかったので、この入門ガイドを書きました。丸一日を費やした後、自分でsocketプログラミングのコードを書いてみるという案を思いつきました。この記事は自分自身のために書いたものですが、あなたがsocketプログラミングを始めるのに役立つはずです。
では、quicklispでusocketライブラリを読み込んでください。
(ql:quickload "usocket")
まず、サーバーを作る必要があります。主に2つの関数を呼び出す必要があります。
usocket:socket-listen関数とusocket:socket-accept関数です。
usocket:socket-listen関数は、あるポートに結合(bind)して、そのポートの上で接続を待ちます。usocket:socket-listen関数は、socket objectを返します。接続が受け入れられるまで、usocket:socket-listen関数はsocket objectと一緒に待機します。
usocket:socket-accept関数は、socketオブジェクトを受け取ります。usocket:socket-accept関数はブロッキングコール(blocking call)で、接続が確立されたときにだけ値を返し、その接続に固有の新しいsocket objectを返します。私たちは、その接続を、クライアントと情報伝達をするために使います。
(defun create-server (port) (let* ((socket (usocket:socket-listen "127.0.0.1" port)) ; 引数のIPとportを結合してsocket objectを返す (connection (usocket:socket-accept socket :element-type 'character))) ; 接続が確立されたときにだけ値を返す (unwind-protect (progn (format (usocket:socket-stream connection) "Hello World~%") (force-output (usocket:socket-stream connection))) (progn (format t "Closing sockets~%") (usocket:socket-close connection) (usocket:socket-close socket)))))
ここで、私がおかした失敗についてお話しましょう。
1つ目の勘違いは、usocket:socket-accept関数は、stream objectを返すというものでした。正しくは、usocket:socket-accept関数は、socket objectを返します。振り返ると、その勘違いのせいで多くの時間をさいてしまいました。もしsocketを書くのであれば、新しいsocketからそれに対応するstreamをえる必要があります。socket objectは、stream slotを持っていて、ここではそれを明示的に利用します。どうすれば、それが分かるのでしょうか。次のようにすれば分かります。
(describe connection)
2つ目の間違いは、新しいsocketとサーバーのsocketを両方閉じる必要があるというものでした。これは明らかなことですが、最初に書いていたコードでは、その接続(connection)だけを閉じていたので、私はsocketを使用したままにし続けていました。もちろん、もう一つの選択肢としては、listenのときに、socketを拒絶することです。
これらの間違いをクリアできれば、あとは簡単です。コネクションとサーバーのsocketを閉じて終わりです。
次に、クライアントを作りましょう。このパートは簡単です。サーバーのポートに接続してください。そうすれば、サーバーからreadできるはずです。
ここで私がした間違えは、read-line関数ではなく、read関数を使ったことでした。そうしていたので、サーバーから"Hello"とだけ返っていました。すこし散歩に行ってから、間違えに気づき、コードを修正しました。
(defun create-client (port) (let ((socket (usocket:socket-connect "127.0.0.1" port :element-type 'character))) (unwind-protect (progn (usocket:wait-for-input socket) (format t "~A~%" (read-line (usocket:socket-stream socket)))) (usocket:socket-close socket))))
では、ここまでのコードをどう動かせばいいのでしょうか?2つのREPLを起動させてください。1つがサーバー用で、もう一つがクライアント用です。このファイルを両方のREPLで読み込んでください。
1つ目のREPLでサーバーを作りましょう。
(create-server 12321)
では、2つ目のREPLでクライアントを走らせましょう。
(create-client 12321)
2つ目のREPLで、"Hello World"と表示されれば、成功です。
こちらも参考にしてください。 1. UDP/IPの入門ガイド
この記事に、次のようなコメントがされていました。
「誰かにこの記事を読んでもらうつもりなら、usocket:with-client-socketマクロとusocket:with-server-socketマクロを使うことをお勧めします。usocket:with-client-socketマクロは、stream variableを結合する利点があるので、stream accessorsに気をつかう必要がありません。」
実際、usocketを使う場合は、with系マクロを使うことが多いようです。
次回は、Smith Dhumbumroongさんの記事Socket programming in Common Lisp: how to use usocket’s with-* macrosを紹介し、with系マクロを使うことで、どのようなコーディングエラーを防ぎ、またコードを簡素化できるのかを見ていきます。