common-lisp
lisp
サーバー
ソケット通信

最近はLisp愛が結構高まってきててCommon Lispで色々ごちゃごちゃやってるんですけど、今回Common LispのSBCLでのソケット通信について勉強したのでまとめておきます。


ストリームとは

ストリームとは、Common Lispでは外界のリソースをやり取りするときに使われるデータ型です。

Common Lispではこのストリームは


  1. コンソールストリーム

  2. ファイルストリーム

  3. ソケットストリーム

  4. 文字列ストリーム

の4つがあり、今回はサーバー立てなどをするにあたって他のコンピュータと通信するためのソケットストリームを利用します。


ソケットとは

ソケットとは、ネットワーク上にある別々のコンピュータで走っているプログラム同士がデータをやり取りするためのプロトコル(規則みたいな)のことです。

標準的なネットワーク(最近ではTCP/IPを使っている)にあるコンピュータと通信するにはこのソケットを作らなければいけません。

さて、ソケットを作るわけですが、ネットワーク上のソケットにはIPアドレスポート番号からなるソケットアドレスが割り振られています。

今回では手元のコンピュータでサーバーを立てるので、IPアドレスは127.0.0.1を、ポート番号は8080を使います。

ちなみにこの127.0.0.1というIPアドレスは特殊なアドレスで、常に現在のコンピュータ自身を指しています。

もし仮にサーバーとクライアントで別のコンピュータを使うのであればそれに応じたIPアドレスを使用してください。


ソケット通信をする

とりあえずソケット通信の実装をしようかと思うのですが、その前にまずソケット通信についてサーバー側とクライアント側がどのように行うかについて述べておきましょう。


サーバー側


  1. create ソケットの作成

  2. bind ソケットを特定のIPアドレスとポートに紐付け

  3. listen 接続を待ち受け

  4. accept 接続を受信

  5. close 接続を切断


クライアント側


  1. create ソケットの作成

  2. bind ソケットを特定のIPアドレスとポートに紐付け

  3. connect リモートソケットに接続

  4. close 接続を切断


実装

以下の環境はSBCLです。


server.lisp

(require 'sb-bsd-sockets)

(defparameter local '(127 0 0 1))

ここで、sb-bsd-socketsモジュールは、SBCLのためのBSDソケットAPIを提供しており、なので最初に(require 'sb-bsd-sockets)としています。

その後、ローカルホストのIPアドレスを設定。

ではソケットを作ります。


server.lisp

(defparameter server-socket (make-instance 'sb-bsd-sockets:inet-socket

:type :stream
:protocol :tcp)

次に、ソケットをローカルアドレスにバインドします。


server.lisp

(sb-bsd-sockets:socket-bind server-socket local 4321)


これでソケットをローカルアドレス127.0.0.1、ポート番号4321にバインドすることができました。

ではlistenにいきましょう。


server.lisp

(sb-bsd-sockets:socket-listen server-socket 500)


この2番目の引数の500はコネクションが繋がるまでログの数を表します。

それではサーバー側にacceptさせましょう。


server.lisp

(defparameter server-stream (sb-bsd-sockets:socket-accept my-socket))


こうするとサーバーは固まったように見えますが、サーバーはクライアントが接続してくるまでREPLに戻ってこない関数となっています。

それではサーバー側は準備ができたので、今度はクライアント側のコンソールに移ってこの手元のサーバーにconnectしましょう。

まずクライアント側でソケットを作ってバインドさせるところまでチャチャっとやります。


client.lisp

(defparameter local '(127 0 0 1))

(defparameter client-socket (make-instance 'sb-bsd-sockets:inet-socket
:type :stream
:protocol :tcp))

(sb-bsd-sockets:socket-bind client-socket local 1234)


ここでクライアント側のポート番号は1234としました。

とりあえずこれにて準備は完了しましたので、では早速connectしましょう。


client.lisp

(defparameter client-stream (sb-bsd-sockets:socket-connect my-socket local  4321))


これにて手元サーバーとの接続ができました。

ここでサーバー側のコンソールを見てみるとREPLに帰ってきてますよね?

実際にサーバー側のコンソールとクライアント側のコンソールでmy-streamを見てみるとpeerとしてお互いのポートに接続されていることが確認できると思います。

それではサーバーとクライアント間でメッセージのやり取りをしてみましょう。

まずはサーバーからクライアントにメッセージを送ります。

送るメッセージとしてはHello, Client!と送りましょう。


server.lisp

(defparmeter server-message (format nil "Hello, Client!"))

(sb-bsd-sockets:socket-send server-stream data (length data)
:external-format :utf-8)


これにてサーバーからクライアント側にメッセージを送ることができました。

それではこのメッセージをクライアント側で見てみましょう。


client.lisp

(defparameter buffer nil)

(defparameter server-message (sb-bsd-sockets:socket-receive client-stream buffer 14))


ここでソケットが受信するときは引数としてストリームとバッファ、データ長の3つをとります。

さて、これでサーバーから受け取ったメッセージを見てみるとちゃんと"Hello, Client"となっていますね。

ちなみに今回socket-receiveの3番目の引数として取っている14を10とすると、client-messageに格納される文字列の最大長が10文字までとなって、残りの4文字を獲得するにはもう一度socket-receiveを行わないといけません。

とりあえずこれにてサーバーからクライアントへメッセージを送るのはこれにて成功です。

次にクライアントからサーバーへメッセージを送るのですが、これはサーバーからクライアントへメッセージを送った時と全く同じ手順なので省略させていただきます。


CLISPでのソケット通信

SBCLではこのように結構厄介な手順を踏んで初めてソケット通信ができましたが、CLISPではめちゃくちゃ簡単なのでササっとまとめておきます。

まずはサーバー側のソケットの作成。


server.lisp

defparameter server-socket (socket-server 4321))


これはポート番号を4321としています。

次にlistenからbind、acceptまで一発でやっちゃいます。


server.lisp

(defparameter server-stream (socket-accept my-socket))


これにてサーバーはacceptの状態になるのでサーバー側の仕事はこれで終わりです。

次にクライアント側ですが、これもconnectまで一瞬でできます。


client.lisp

(defparameter client-stream (socket-connect 4321 "127.0.0.1"))


これでconnectの完了です。

ではメッセージのやり取りをいきましょう。

クライアント側からサーバーへメッセージを送ります。


client.lisp

(print "Hello, Server!" client-stream)


これにてメッセージの送信が完了していて、実際にサーバー側で見てみると


server.lisp

(read server-stream)


とすると"Hello, Server!"となっています。

(簡単すぎる.....)


まとめ

以上ではSBCLとCLISPそれぞれにおいてソケット通信のやり方を解説しました。

最近Land of Lispを読んでいて、CLISPでのソケット通信が書いてあったのでじゃあSBCLでもやってみるかぁってやってみたら想像以上に難航したので、その作業ログとして今回の記事をまとめるに至りました。

それでは皆さんもLispライフを満喫してください。

お疲れ様でした!