プログラミングを頑張る日記

プログラミングを勉強して、ハッカーになろう

Common Lispを頑張る(38)

「Land of Lisp」をやりますよもちろん。
今日は、SBCLでソケットを作るにはどうすりゃいいの、という話になります。

まずは参考書の例と、昨日たてた道筋を。

(defun serve (request-handler)
  (let ((socket (socket-server 8080)))
    (unwind-protect
        (loop (with-open-stream (stream (socket-accept socket))
                                (let* ((url (parse-url (read-line stream)))
                                       (path (car url))
                                       (header (get-header stream))
                                       (params (append (cdr url)
                                                       (get-content-params stream header)))
                                       (*standard-output* stream))
                                  (funcall request-handler path header params))))
      (socket-server-close socket))))

道筋はこちら。
■指定したポート番号にソケットを開く方法。
■ソケットを閉じる方法。
■ソケットとの接続を確立した相手とのストリームを作成する方法。

英語は苦手なのですが、そうも駄々をこねてもいられません。
SBCL 1.4.12 User Manual

Classであるsocketのインスタンスを作って、それにsocket-bindを使いアドレスに束縛する。
socket-acceptでソケットと繋がる。ストリームを作るのはsocket-make-streamっぽいですよね。
閉じるためにはsocket-closeかな。インスタンスを作る以外には大差なさそうですね。
ただ、ソケットと繋げるのとは別にストリームを作るのが面倒。

とにかくまずはライブラリのインポートをします。

CL-USER> (require 'sb-bsd-sockets)
NIL
CL-USER>

nilが返ると不安になるなあ。
試してみます。

CL-USER> (make-instance 'sb-bsd-sockets:socket
				     :type :stream
				     :protocol :tcp)
; Evaluation aborted on #<SIMPLE-ERROR "No socket family" {100385F223}>.
CL-USER> 

むむむ。怒られかた的にインポートできていないというわけでもないような。
少なくとも'sb-bsd-socketsのことは知っていそうですね。

とエラーをググっていたらsb-bsd-socketsのGithubに行き着きました。
sbcl/sockets.lisp at master · sbcl/sbcl · GitHub
このエラーはここで定義されていそう。
そして最初のSBCLのマニュアルをもう少しちゃんと読んだら、
おもいっきり「socketを直接インスタンスにするなよ」みたいなことが書いてありそうです。

じゃあこれかな。

CL-USER>  (make-instance 'sb-bsd-sockets:inet-socket :type :stream :protocol :tcp)
#<SB-BSD-SOCKETS:INET-SOCKET 0.0.0.0:0, fd: 17 {1003BA2D13}>
CL-USER>

よしよし。

TCPUDPとやらについてちょいと調べたら、TCPがストリームを開くという文字列が目に飛びこんできたので
TCPでいきます。雑な知識でも手を動かしていくうちにきちんと身につくのです。多分。

しかし今作ったインスタンスIPアドレスが0.0.0.0です。参考書がどこでIPアドレスを設定しているのか
先を読んでもわかりません。デフォルトがそうなのでしょうか。
少なくともこちらの環境では設定しておく方が安心です。
...これもマニュアル読んだらsocket-bindでやるんですね。英語ちゃんと読めないのに流し読みしすぎですね。

さて、参考書、つまりCLispでいうsocket-serverも書けるのではないでしょうか。
やってみます。

CL-USER> (defun socket-server (ip port)
           (let* ((socket (make-instance 'SB-BSD-SOCKETS:INET-SOCKET
                                :type :stream :protocol :tcp)))
             (sb-bsd-sockets:socket-bind socket ip port)))
WARNING: redefining COMMON-LISP-USER::SOCKET-SERVER in DEFUN
SOCKET-SERVER
CL-USER> (socket-server '(127 0 0 1) 8080)
NIL
CL-USER>

む、let*の戻り値は本体の評価でしたか。忘れてました。nilじゃ困ります。

CL-USER> (defun socket-server (ip port)
           (let* ((socket (make-instance 'SB-BSD-SOCKETS:INET-SOCKET
                                :type :stream :protocol :tcp)))
             (sb-bsd-sockets:socket-bind socket ip port)
           socket))
WARNING: redefining COMMON-LISP-USER::SOCKET-SERVER in DEFUN
SOCKET-SERVER
CL-USER>

さ、これで作成したソケットが返ってくるはずです。
試してみます。

CL-USER> (socket-server '(127 0 0 1) 8080)
; Evaluation aborted on #<SB-BSD-SOCKETS:ADDRESS-IN-USE-ERROR {10043ABB03}>.
CL-USER>

...げ。ポートが開きっぱなしになりました。socketはローカルで定義しちゃったからもはや未定義。
ポートを直接指定して閉じなくては。

う~ん。

MB-Air:.emacs.d user$ lsof -i -P | grep 8080
sbcl      71057 user   18u  IPv4 0xb76170867f398947      0t0  TCP localhost:8080 (CLOSED)
MB-Air:.emacs.d user$

開いてはいないんですね。作っただけだからか。どうやって手放せばいいのかな。
...結局ターミナルから"kill -9 71057"で殺しました。
SBCL側からどうにかする方法をご存知で教えてもいいという方がいらっしゃったら是非ご教授ください...。

次はCLispのsocket-acceptにあたる関数を作ります。

CL-USER> (defun my-socket-accept (socket)
           (sb-bsd-sockets:socket-accept socket)
           (sb-bsd-sockets:socket-make-stream socket))
MY-SOCKET-ACCEPT
CL-USER>

これだけでいい気がするのですがなんか怖いですね。

まあ、いちかばちか。serve関数を書いてみます。

CL-USER> (defun serve (request-handler)
           (let ((socket (socket-server '(127 0 0 0) 8080)))
             (unwind-protect
                  (loop (with-open-stream (stream (my-socket-accept socket))
                          (let* ((url (parse-url (read-line stream)))
                                 (path (car url))
                                 (header (get-header stream))
                                 (params (append (cdr url)
                                                 (get-content-params stream header)))
                                 (*standard-output* stream))
                            (funcall request-handler path header params))))
               (sb-bsd-sockets:socket-close socket))))
SERVE
CL-USER>

これでいいのかな。

HTMLを生成する

渡す関数がないとserve関数を試すこともできないので先へ進みます。
HTMLを生成する関数を書きます。

CL-USER> (defun hello-request-handler (path header params)
  (if (equal path "greeting")
      (let ((name (assoc 'name params)))
        (if (not name)
            (princ "<html><form>What is your name?<input name='name' /></form></html>")
            (format t "<html>Nice to meet you, ~a!</html>" (cdr name))))
      (princ "Sorry... I don't know that page.")))
; in: DEFUN HELLO-REQUEST-HANDLER
;     (SB-INT:NAMED-LAMBDA HELLO-REQUEST-HANDLER
;         (PATH HEADER PARAMS)
;       (BLOCK HELLO-REQUEST-HANDLER
;         (IF (EQUAL PATH "greeting")
;             (LET (#)
;               (IF #
;                   #
;                   #))
;             (PRINC "Sorry... I don't know that page."))))
; 
; caught STYLE-WARNING:
;   The variable HEADER is defined but never used.
; 
; compilation unit finished
;   caught 1 STYLE-WARNING condition
HELLO-REQUEST-HANDLER
CL-USER> 

テストします。

CL-USER> (hello-request-handler "lolcat" '() '())
Sorry... I don't know that page.
"Sorry... I don't know that page."
CL-USER> (hello-request-handler "greeting" '() '())
<html><form>What is your name?<input name='name' /></form></html>
"<html><form>What is your name?<input name='name' /></form></html>"
CL-USER>

テストも問題なし。リクエストハンドラのインターフェースが完全に独立しているので
デバッグがたやすい。いいことですね。

いよいよです。Webサイトを立ち上げます。

CL-USER> (serve #'hello-request-handler)
; Evaluation aborted on #<SB-BSD-SOCKETS:SOCKET-ERROR {1002B78FC3}>.
CL-USER>

ぐわああ!どうにもsocket-serverでだめっぽいです。

CL-USER> (defparameter *localhost-address* '(127 0 0 1))
*LOCALHOST-ADDRESS*
CL-USER> (defparameter *socket* (socket-server '(127 0 0 0) 8080))
; Evaluation aborted on #<SB-BSD-SOCKETS:SOCKET-ERROR {1003C55E53}>.
CL-USER> (defparameter *socket* (socket-server *localhost-address* 8080))
*SOCKET*
CL-USER>

examples/sbcl-sockets.lisp at master · drichardson/examples · GitHub
上記のサイトを例をみて、真似してみたらいけました。
引数の渡しかたが不味かったようですが、よくわかっていません...。
とりあえず関数をなおします。

CL-USER> (defun serve (request-handler)
           (let ((socket (socket-server *localhost-address* 8080)))
             (unwind-protect
                  (loop (with-open-stream (stream (my-socket-accept socket))
                          (let* ((url (parse-url (read-line stream)))
                                 (path (car url))
                                 (header (get-header stream))
                                 (params (append (cdr url)
                                                 (get-content-params stream header)))
                                 (*standard-output* stream))
                            (funcall request-handler path header params))))
               (sb-bsd-sockets:socket-close socket))))
WARNING: redefining COMMON-LISP-USER::SERVE in DEFUN
SERVE
CL-USER>

さて、リベンジ。

CL-USER> (serve #'hello-request-handler)
; Evaluation aborted on #<SB-BSD-SOCKETS:INVALID-ARGUMENT-ERROR {1004847653}>.
CL-USER>

acceptの引数があかんと怒られです。さきほどのサイトをみてもよくわからず、とりあえず真似します。

CL-USER> (defun my-socket-accept (socket)
           (sb-bsd-sockets:socket-make-stream (sb-bsd-sockets:socket-accept socket)))
WARNING: redefining COMMON-LISP-USER::MY-SOCKET-ACCEPT in DEFUN
MY-SOCKET-ACCEPT
CL-USER>

また同じように怒られました。ちゃんとさきほどのサイトを見ます。

CL-USER> (defun socket-server (ip port)
           (let* ((socket (make-instance 'SB-BSD-SOCKETS:INET-SOCKET
                                :type :stream :protocol :tcp)))
             (setf (sb-bsd-sockets:sockopt-reuse-address socket) t)
             (sb-bsd-sockets:socket-bind socket ip port)
             (sb-bsd-sockets:socket-listen socket 1)
           socket))
WARNING: redefining COMMON-LISP-USER::SOCKET-SERVER in DEFUN
SOCKET-SERVER
CL-USER>

socket-listenが欠けていたのが致命的だったようです。
マニュアルにもacceptを待つようにするみたいなことが書いてあります。

CL-USER> (serve #'hello-request-handler)

返ってきません。これは...!

File descriptor must be opened either for input or output.
   [Condition of type SIMPLE-ERROR]

Restarts:
 0: [RETRY] Retry SLIME REPL evaluation request.
 1: [*ABORT] Return to SLIME's top level.
 2: [ABORT] abort thread (#<THREAD "new-repl-thread" RUNNING {100450A263}>)

Backtrace:
  0: (SB-SYS:MAKE-FD-STREAM 18 :CLASS SB-SYS:FD-STREAM :INPUT NIL :OUTPUT NIL :ELEMENT-TYPE CHARACTER :BUFFERING :FULL :EXTERNAL-FORMAT :DEFAULT :SERVE-EVENTS NIL :TIMEOUT NIL :FILE NIL :ORIGINAL NIL :DELE..
  1: ((:METHOD SB-BSD-SOCKETS:SOCKET-MAKE-STREAM (SB-BSD-SOCKETS:SOCKET)) #<SB-BSD-SOCKETS:INET-SOCKET 127.0.0.1:8080, peer: 127.0.0.1:65042, fd: 18 {1002D66F03}> :INPUT NIL :OUTPUT NIL :ELEMENT-TYPE CHARA..
  2: (SERVE #<FUNCTION HELLO-REQUEST-HANDLER>)
  3: (SB-INT:SIMPLE-EVAL-IN-LEXENV (SERVE (FUNCTION HELLO-REQUEST-HANDLER)) #<NULL-LEXENV>)
  4: (EVAL (SERVE (FUNCTION HELLO-REQUEST-HANDLER)))
 --more--

繋いだタイミングででしょうか。エラーが起きてました。
make-fd-streamでエラーが起きているよう。
Gitを読むに、inputもoutputもこれに渡していないとこのエラーが起きると。
そして上のログを見るに、socket-make-streamにソケットしか渡していなかったのがだめでしたね。

双方向でもいいのでしょうか。

CL-USER> (defun my-socket-accept (socket)
           (sb-bsd-sockets:socket-accept socket)
           (sb-bsd-sockets:socket-make-stream socket :output t))
WARNING: redefining COMMON-LISP-USER::MY-SOCKET-ACCEPT in DEFUN
MY-SOCKET-ACCEPT
CL-USER>

いや、入力はストリームで受け取らないかな。とりあえずこれで。

#<SB-SYS:FD-STREAM for "socket 127.0.0.1:8080" {1004048BD3}> is not a character input stream.
   [Condition of type SIMPLE-TYPE-ERROR]

Restarts:
 0: [RETRY] Retry SLIME REPL evaluation request.
 1: [*ABORT] Return to SLIME's top level.
 2: [ABORT] abort thread (#<THREAD "new-repl-thread" RUNNING {100316A683}>)

Backtrace:
  0: (SB-KERNEL:ILL-IN #<SB-SYS:FD-STREAM for "socket 127.0.0.1:8080" {1004048BD3}>)
  1: (READ-LINE #<SB-SYS:FD-STREAM for "socket 127.0.0.1:8080" {1004048BD3}> T NIL #<unused argument>)
  2: (SERVE #<FUNCTION HELLO-REQUEST-HANDLER>)
      Locals:
        REQUEST-HANDLER = #<FUNCTION HELLO-REQUEST-HANDLER>
        SOCKET = #<SB-BSD-SOCKETS:INET-SOCKET 127.0.0.1:8080, fd: 13 {1004047533}>
        STREAM = #<SB-SYS:FD-STREAM for "socket 127.0.0.1:8080" {1004048BD3}>
  3: (SB-INT:SIMPLE-EVAL-IN-LEXENV (SERVE (FUNCTION HELLO-REQUEST-HANDLER)) #<NULL-LEXENV>)
  4: (EVAL (SERVE (FUNCTION HELLO-REQUEST-HANDLER)))
 --more--

次はこのエラーです。
一個ずつ片づけられているのはいいのですが、体力の限界です...。一週間の疲労が...。
続きは明日の自分に任せます。
それでは...。