Common Lisp でサーバプログラム - その 1

CL でのサーバサイドプログラミングを手探りで色々試す。今回は超単純エコーサーバを書く。仕様は以下。

  • 一度に扱えるクライアントは 1 つだけ。
  • クライアントから "stop-server" と入力するとサーバを終了させる。
  • クライアントで EOF を入力するとクライアントを切断し、次のクライアントの接続を待つ。

ソケットライブラリには usocket を使う。何はともあれ usocket をインストールする。

CL-USER> (asdf-install:install :usocket)

次は asd ファイルを作る。

(defsystem echo-server
  :components ((:file "packages")
               (:file "echo-server" :depends-on ("packages")))
  :depends-on (:usocket))

asd ファイルに定義した通り、ファイル構成は packages.lisp と echo-server.lisp の 2 つ。とりあえず package の定義を含める packages.lisp を作る。

(defpackage :echo-server
  (:use :cl)
  (:export :start))

最後にサーバ本体の echo-server.lisp を作る。

(in-package :echo-server)

(define-condition stop-server (condition) ())

(defun make-server-socket (port)
  (usocket:socket-listen "localhost" port :reuseaddress t))

(defun dispose (server)
  (format t "close...~%")
  (usocket:socket-close server))

(defun accept-client (server)
  (prog1 (usocket:socket-accept server)
    (format t "accept a client!~%")))

(defun trim-input (input)
  (string-trim " ^M" input))

(defun echo-input (client-stream input)
  (format t "echo to client!~%")
  (format client-stream "~A~%" (trim-input input))
  (force-output client-stream))

(defun input= (input expected)
  (string= (trim-input input) expected))

(defun handle-client (client)
  (with-open-stream (stream (usocket:socket-stream client))
      (loop for input = (read-line stream nil nil)
         while input
         if (input= input "stop-server") do
           (error 'stop-server)
         else do
           (echo-input stream input))
      t))

(defun start (&key (port 8080))
  (let ((server-sock (make-server-socket port)))
    (handler-case
        (loop while (handle-client (accept-client server-sock)))
      (stop-server () (format t "exit...~%"))
      (condition () (format t "unexpected exit!~%")))
    (dispose server-sock)))

使ってみる。まずはサーバを起動する。

CL-USER> (echo-server:start)

次にクライアント。クライアントプログラムは作ってないので、telnet で代用。

$ telnet
telnet> open localhost 8080
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hoge
hoge
fuga
fuga

クライアントから "stop-server" と入力すればサーバが終了する機能はちゃんと動いてるのを確認できた。が、EOF (Ctrl-D 入力でいいんだよなぁ) でのクライアント切断はうまくいかず。Ctrl-D には無反応だった。

以下、今後の課題。

  • EOF の問題を解決する (ネットワークプログラミング関係ないけど...)。
  • libevent 的なものを使って非同期 IO で多重化する。
  • その他なにか思いつけば。