t-cool

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系マクロを使うことで、どのようなコーディングエラーを防ぎ、またコードを簡素化できるのかを見ていきます。

t-cool.hateblo.jp