こける Wired-Winsockを使ってみようぜ-千客万来(サーバ編)'98/01/26

10.千客万来(サーバ編)

さて、前回は「ベル」でした。
今度は「受話器」です。
そうしたら「サーバコンポーネント」ができあがるので、これを使ったサンプルを一つ作りましょう。

まずは「クライアントコンポーネント」の解体から。
「クライアントコンポーネント」は、いわば「電子電話帳」「ダイヤル」付き「受話器」でした。
「電子電話帳」部分が「サーバ名」を「IPアドレス」に変える部分です。
「サーバ用の受話器」には「電子電話帳」も「ダイヤル」も必要ありません。
ただの「受話器」に「ベルへの接続」が欲しいだけです。
クライアントコンポーネントをコピーして使ってもいいのですが、せっかくですから「継承」を使ってみましょう。
もともとこうして継承を意識して作っておけば楽だったのですが、クライアントを作成している段階では何故そうするのかを読めなかったでしょうから、ここでやるわけです。

まず、「クライアントコンポーネント」からKHostInfを扱っている部分と、Connect回りの部分を取り外します。
これだけだと後でConnect回りを付け足しにくいので、改造を入れます。

どこが付け足しにくいかというと、「WSAAsyncSelect」に渡す「FD_CONNECT」の部分です。
WSAAsyncSelectに渡す「何が発生した時イベントが欲しいか」という引数を取得するメソッドを追加しましょう。
これはProtected部にvirtualなメソッドを作成します。
Connectを付け足す時にFD_CONNECTだけ付け足せば良くなります。

また、クライアントソケットはソケットを自分で作りましたが、サーバ用の受話器はacceptの返り値を使います。
この違いを吸収するために、Socketプロパティを作ります。
Socketプロパティの読み出しではソケットが設定されてなければソケットを作り出します。
書き込み部分ではソケットがあればそれを閉じて新しいソケットを記憶します。
このプロパティは実行時のみ有効なプロパティなのでpublic部に作ります。

これで一旦「受話器」コンポーネントとして完成です。
しかし、こいつはこのままではクライアントでもサーバ用受話器でもありません。
このままでは使いみちが無いのです。

この「受話器」コンポーネントを継承して再度クライアントコンポーネントを作ります。
先程はずした「ダイヤル」と「電子電話帳」を作り込むのです。
基本的には先程はずしたものを付けなおすだけです。
Socketプロパティに何も設定せずに読み出すようにすれば、親の受話器がソケットを作ってくれます。
また、「何が発生した時イベントが欲しいか」取得メソッドをオーバライドして、FD_CONNECTもイベントをもらうようにします。
そうしておいて、WndProcメソッドでFD_CONNECTイベントだけ処理し、処理しなかったものはすべて親にやってもらえば良いのです。

クライアントコンポーネントから「受話器」を取り出せました。
クライアントコンポーネントは元どおりに使えるはずです。

お次はサーバ用「受話器」です。
まずは使い勝手を考えましょう。
いくつ受話器が必要になるか判らないので、必要な時に作り必要なくなれば消えて欲しいのです。
「ベル」コンポーネントに管理してもらいましょう。
「ベル」コンポーネントがacceptした時に作って、不要になれば消しましょう。
ん?
不要になるのは「受話器」コンポーネントをCloseした時です。
それを知っているのは「受話器」。
なら「受話器」から「ベル」へ「不要になったから消して」と頼まなければなりません。
自分で消えることはできません。
「ベル」へイベントをとばす?
駄目です。「受話器」のCloseメソッドから「ベル」へイベントをとばしても、そのイベントが終了した時「受話器」のCloseメソッドに帰ってきてしまいます。
このイベントで「受話器」を消してしまうとCloseメソッドに帰ってきた時、存在しないコンポーネントのメソッドの中にいることになります。
Closeメソッドが終了してから消して欲しいのです。
こういう場合はPostMessageを使います。
PostMessageは「メッセージキュー」の中にメッセージを入れるWindowsAPIです。
このメッセージが取り出されるのは、次にメッセージキューを見に行った時。
それはCloseメソッドが終了した後になります。

これで「受話器」を作って消す準備はできました。
どうやって管理しましょう?
ここでいう管理とは「現在存在する受話器一覧」を作るってことです。
これが無ければ、チャットサーバを作った時に「接続しているみんなに送信する」って事ができませんし、「ベル」が消える時「受話器」を消すって事もできません。
TListを使う?
これでも良いのですが、もっといいものがあります。
「ベル」のComponentsプロパティ、ComponentCountプロパティです。
「ベル」はコンポーネントですから、TComponentを継承しています。
TComponentには他のコンポーネントを管理するためのComponentsプロパティとComponentCountプロパティがあります。
自分が消える時にComponentsプロパティに入っているコンポーネントは消えます。
また、Componentsプロパティに入っているコンポーネントが自分より先に消えた場合、Componentsプロパティから削除されます。
ComponentCountプロパティはComponentsプロパティにいくつコンポーネントが入っているかを示します。

Componentsプロパティに管理してもらうには、管理して欲しいコンポーネントのOwnerプロパティに自分を設定するだけです。
これはコンポーネントのCreateコンストラクタの引数に自分を渡せばいいのです。
通常のコンポーネントはこの引数にFormを渡しています。
そうすることでFormが消える時、FormのComponentsプロパティの中に入っているすべてのコンポーネントが消えるわけです。

Componentsプロパティを使うにしても、ComponentsプロパティはTComponent型の配列プロパティですからちょっと使いにくい。
「サーバ用受話器コンポーネント」型の配列プロパティを作りましょう。
このプロパティはComponentsプロパティをキャストするだけですね。

サーバ用受話器コンポーネントのイベントはどうしましょう?
オブジェクトインスペクタで設定したいのですが、設計時にはサーバ用受話器コンポーネントは存在しません。
これは「ベル」コンポーネントに「サーバ用受話器コンポーネント」に設定する為のイベントを追加しましょう。
そうしておいてサーバ用受話器コンポーネントを作り出した時、自分のイベントをコピーするのです。
あれ?そうするとSender引数がおかしいですね。
「ベルコンポーネント」に設定したイベントですからSender引数が「サーバ用受話器コンポーネント」では少し変。
サーバ用受話器コンポーネントに設定するのは「ベルコンポーネント」のメソッドにして、そのメソッドのなかでSender引数を差し替えましょう。
お?そうすると「どのサーバ用受話器コンポーネント」で発生したイベントか判らなくなりますね。
それなら「どのサーバ用受話器コンポーネントで発生したイベントか」というのを引数に付けましょう。
これでオブジェクトインスペクタからサーバ用受話器コンポーネントのイベントが設定できます。

あとは「ベルコンポーネント」の名前を「サーバコンポーネント」に変えると出来上がり。

せっかくですからこのサーバコンポーネントを使ってサンプルを作りましょうか。
クライアントの時にチャットだったので、このチャットサーバにしようかとも思ったのですが、それは皆さんへの課題ということで。
今回は「Webサーバごっこ」とでもして、「httpd」にしましょう。
エラー処理も必要だとされている機能のサポートも無しにして超簡易版httpdです。
CGIも何も使えないばかりか、いろいろ不都合もあります。

基本的には"GET ファイル名"という文字列を受信するとそれに相当するファイルを読み出してきて送り返します。
ファイル名のパスは"/"で区切られていますから、送られてきた文字列を一文字づつ読み出してきて"/"を"¥"に変えます。
見つからなければ予め用意してある「notfound.htm」を送り返します。
受信の終わりは2つ連続した改行で見つけます。

また、送信が完了するまでソケットを閉じて欲しくないのでSetSockOptを使いました。
function setsockopt(s: TSocket; level, optname: Integer; optval: PChar; optlen: Integer): Integer;
sにソケット、levelにSOL_SOCKET、optnameにSO_LINGERを設定します。
optvalには、TLinger型の変数のアドレスを渡します。
optlenはsizeof(TLinger)でいいです。

TLingerはこんなレコード型です。
type TLinger = packed record
    l_onoff: u_short;
    l_linger: u_short;
end;
l_onoffに0、l_lingerに0を設定すると「クローズするなら送信が終わるまで待て」という意味になります。
l_onoffに1、l_lingerを0に設定すると「クローズする時は送信が終わらなくても即座にクローズしろ」という意味です。

あとは特に説明するところは無いでしょう。

たったこれだけでもインターネットエクスプローラ2.0とネットスケープナビゲータ3.0では使えました。
ま、URLにディレクトリを指定すると変でしたが。
エラーをちゃんと返してないのでしかたないですね。(^^;;

またこれを作っていて気が付いたのですが、Windows95はFD_WRITEイベントをちゃんと返して来てくれないようです。
これを作るまで気が付きませんでした。
おかげでOnSendEmptyイベントがうまく発生していませんでした。
しかたないので送信バッファが空になった時、FD_WRITEが発生しなくてもOnSendEmptyイベントを発生させるようにしました。
[ソースダウンロード]

実行中の画面は出してもしかたないので止めます。
サーバは見栄えがしませんね。
[表紙] [Program Files] [オブジェクト指向異聞] [プログラム未整理知識] [Winsockを使ってみようぜ] [だべり] [What's New] [書いた奴] [リンク]