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

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

Common Lispを頑張る(34)

昨日の続きからやっていきます。

Lispでファイルを出力するとき、作ろうとしたファイルが既に存在していた場合どうするかは、
:if-existsキーワードで指定することができます。

CL-USER> (with-open-file (mystream "data.txt" :direction :output :if-exists :error) ;もし存在するならエラー
           (print "my data" mystream))
; Evaluation aborted on #<SB-INT:SIMPLE-FILE-ERROR "~@<~?~@[: ~2I~_~A~]~:>" {1005198FF3}>.
CL-USER> (with-open-file (mystream "data.txt" :direction :output :if-exists :supersede) ;問答無用で上書き
           (print "my data" mystream))
"my data"
CL-USER> 

wiht-open-fileを使うことで、ファイルをオープンしたりクローズしたりするコマンドを
いちいち使う必要がなくなります。便利ですね。

ソケットを使う

ここまででREPLとファイルに対してストリームを使ってみたので、
次は他のコンピュータとやりとりするためストリームを使ってみます。
標準的なネットワークにある他のコンピュータと通信するプログラムを書きたければ、
まずソケットとやらを作らなければならないそうです。
ソケットとは、ネットワークの別々のコンピュータで走っているプログラム同士がやりとりするためのメカニズムだそう。
ソケットの標準化はCommon Lispの仕様化に間に合わなかったそうで、
ここからはCLispのソケットコマンドについて解説いただけるようです。それは不味い。
当然SBCLにもあるはずなので、解説を参考にしてGoogle先生に訊きながらやっていこうと思います。

ソケットアドレス

ネットワーク上のソケットにはソケットアドレスが割り当てられているそうです。
ソケットアドレスは次の2つの要素からなるそう。
IPアドレス:ネットワーク上でコンピュータを一意に指定する番号。
■ポート番号:ネットワークを使うプログラムは、同じコンピュータで走る他のプログラムが既に使っていない
ポート番号を選んで使わなければならない。

上記の要素の条件から、ソケットアドレスは各PCのプログラムごとに一意になるはずです。
ネットワーク上を走るメッセージはTCPパケットと呼ばれ、行き先を示すソケットアドレスが付加されているそうです。
宛先のIPアドレスを持つコンピュータがソケットを受け取ると、OSがソケットアドレスのポート番号を確認して、
どのプログラムがメッセージを受け取るかを判断するようになっていると。
そのために、プログラムはポートを使うためにそのポートに結びつけられたソケットを作るそうです。
つまりソケットとは、プログラムがOSに「ポートxxxにメッセージが届いたら自分に頂戴」とするためのものなのですね。

コネクション

2つのプログラム間でソケットを使ってメッセージをやり取りするためには、
いくつかのステップを経て、コネクションとやらを初期化する必要があるそうです。
最初のステップは、一方のプログラムがソケットを作ってそれをListenすることで、
もう一方のプログラムが通信を始めるのを待つ状態に入ることだそう。
この、ソケットをListenする側のプログラムをサーバと呼ぶそうです。
もう一方はクライエントで、そちらがソケットを作った後、それを使ってサーバとコネクションを確立するそうです。
ここまでの手順で問題が起きていなければ、2つのプログラムはコネクションを通じてやりとりできるようになります。
さて、そろそろ実践してみます。

ソケット上でメッセージを送る

まずはOSからポート番号を貰わなければなりません。そしたらソケットをそのポート番号に束縛します。
どうしたものか。とりあえずSBCLのマニュアルを読んできます。
socketというクラスがあるみたいですね。これを使えばいいのかな。とりあえず試してみます。

CL-USER> (defstruct (my-socket (:include socket)))
; Evaluation aborted on #<SIMPLE-ERROR "Class is not yet defined or was undefined: ~S" {10059462E3}>.
CL-USER>

む、出来ない。...ふむふむ。読み込まねばならないものがあるんですね。

CL-USER> (require :sb-bsd-sockets)
NIL
CL-USER> (defstruct (my-socket (:include socket)))
; Evaluation aborted on #<SIMPLE-ERROR "Class is not yet defined or was undefined: ~S" {1005BD2E73}>.
CL-USER> s

読込めた気がしない、と思いつつ試してみたらやっぱり駄目でした。
う~ん、なんだろうな。パッケージを実は持っていないのでしょうか。
でもパッケージのリストを見てみると入っているんですよね。そういうことじゃないのかな。
roswellで改めてsbclインストールしてみたりしてみましたが解決できず。
悔しいですが、諦めてCLispでやっていきます。
色々やっていたら間抜けにもinit.elの設定を壊してしまったようで
それどころじゃなくなったというのもあります。悲しみ。

CL-USER> (defparameter my-socket (socket-server 4321))
MY-SOCKET
CL-USER>

定義した瞬間に「インターネット接続を許可しますか」と訊かれたので上手くいってそうです。
これはOSに、「これからポート番号の4321をmy-socketとして使うよ」と言っているんですかね。

そしてこのコマンドは少し危険だそう。使い終わったらちゃんとOSに返してあげなければ
誰もポート番号4321を使えなくなってしまうからです。

次は、このソケットへ接続したクライアントとの通信を扱うストリームを作ります。

CL-USER> (defparameter my-stream (socket-accept my-socket))

返ってこないですね...。
ああ、これでいいようです。クライアントが接続してくるまで返ってこないのが正解のよう。

さて、クライアント側の設定をしていきます。本当は別のパソコンがあればいいのですが、
残念ながらないので新しくCLispを立ち上げてやっていきます。

; SLIME 2.22
CL-USER> (defparameter my-stream (socket-connect 4321 "127.0.0.1"))
MY-STREAM
CL-USER>

"123.0.0.1"は常に現在のコンピュータ自身を指す特殊なアドレスだそうです。
気がついたらサーバ側で変化が。

CL-USER> (defparameter my-stream (socket-accept my-socket))
MY-STREAM
CL-USER>

よし、それではクライアントからメッセージを送ってみます。

CL-USER> (print "こちらクライアント、どうぞ" my-stream)
"こちらクライアント、どうぞ"
CL-USER>

サーバ側でmy-streamを確認してみます。ドキドキ。

CL-USER> (read my-stream)
"こちらクライアント、どうぞ"
CL-USER>

ああ、返ってきてくれました。本当によかった。それでは今度はサーバから送ってみます。

CL-USER> (print "こちらサーバ、どうぞ" my-stream)
"こちらサーバ、どうぞ"
CL-USER>

クライアントで受け取ります。

CL-USER> (read my-stream)
"こちらサーバ、どうぞ"
CL-USER>

...ふう、大仕事でした。

最後はお片づけをします。

CL-USER> (close my-stream)
T
CL-USER>

上記のコマンドをクライアント、サーバの両方で実行し、両端のストリームを閉じます。
後は、OSにポートを返却してソケットを開放します。

CL-USER> (socket-server-close my-socket)
NIL
CL-USER>

なんか疲れました。ネットワークというのは難しいですね。

文字列ストリーム

これまでのストリームとは違い、文字列ストリームは、
単に文字列をストリームのように見せるだけのものだそう。
他のストリームのように、文字列ストリームは文字列を読んだり、書きこんだりできるようです。

文字列ストリームは、make-string-output-streamとmake-string-input-streamで作れるとのこと。

CL-USER> (defparameter foo (make-string-output-stream))
FOO
CL-USER> (princ "This will go into foo." foo)
"This will go into foo."
CL-USER> (princ "This will also go into foo." foo)
"This will also go into foo."
CL-USER> (get-output-stream-string foo)
"This will go into foo.This will also go into foo."
CL-USER>

わざわざ文字列ストリームを使ったりするのには勿論理由があって、
デバッグに使ったり、長い文字列を作成するのに便利であるということがあるようです。

デバッグに文字列ストリーム

ストリームを引数にしている関数に、文字列ストリームを渡すことができるそうで、
それは確かに便利ですね。わざわざファイルを作ったり、ソケットを作らずとも
それらを引数にとる機能がちゃんと動いているか確認することができます。

また、そのようなデバッグを可能にするためにも、出力先はハードコーディングするのではなく
ストリームを使うようにしておくのがいいそうです。

長い文字列を作る

たくさんの文字列を一つずつ繋いでいくのは、そのままやると効率の悪い操作になるそうです。
多くの言語では文字列ビルダと呼ばれる機能を用意してそのオーバヘッドを避けており、
Lispでは文字列ストリームを使い効率を上げられるということです。

読みやすくデバッグしやすいコード

文字列ストリームを、with-output-to-stringと一緒に使うと、
読みやすくデバッグしやすいコードが書けるということで、やってみます。

CL-USER> (with-output-to-string (*standard-output*)
	   (princ "the sum of ")
	   (princ 5)
	   (princ " and ")
	   (princ "2")
	   (princ " is ")
	   (princ (+ 5 2)))
"the sum of 5 and 2 is 7"
CL-USER>

with-output-stream-to-stringマクロは、他のストリームへ向かうはずの出力を
横取りして、文字列に格納して返す様子。
上では*standard-output*に向かうprincの出力が文字列ストリームに向けられて、
処理が終わったタイミングで文字列ストリームに蓄積されたデータが文字列として返ってきています。
しかもこれは効率の悪いことをしていないので、concatenateを使うよりも効率がいいそうです。
読みやすいですしね。

これでストリームについての章は終わりです。
あと今日はinit.elの設定を見直さなければ...せっかくなのでブログにしようかな。
とりあえず締めます。