こける Wired-Winsockを使ってみようぜ-待ち人来たる(サーバ編)'98/01/12

9.待ち人来たる(サーバ編)


TCPを使ってネットワーク対応ゲームなどを作るにはクライアントだけでは話になりません。
サーバもないとね。
で、今回からサーバ編です。
今回の説明だけでサーバプログラムを作ることもできるのですが、複数のクライアントを管理するというところまでいきませんので、同時に2つのクライアントを扱えないというちょっと情けないサーバになってしまいます。なので、今回はサンプルプログラムがありません。

クライアントとサーバのプログラムの組みかたで最大の違いは、以下の2点でしょうか。
(1)クライアントは仕事があるときだけソケットを作る
サーバはクライアントがつなぎに来るのを待つためにもソケットを作る
(2)クライアントは基本的にサーバ1つを相手にしていればいい
サーバは複数のクライアントの相手をしなければならない

送信や受信、ソケットのクローズ、相手のソケットがクローズされたことを検出する等の点はサーバでもクライアントでも変わりません。
「サーバは複数のクライアントの相手をする」という点からすると、非同期を使うか同期を使ってクライアントが接続されるごとにスレッドを作るかという感じになりますけれど。
すでに非同期の使い方も記述しましたし、特にスレッドを使う必要も無いでしょう。
クライアントのときとは違い、サーバでは非同期しか書きません。

今回は主に「サーバはクライアントがつなぎに来るのを待つためにもソケットを作る」という方を書きます。

「TCPでの通信は電話に似ている」という話を覚えているでしょうか?
ちょっとここで電話機の機能を分解してみましょう。(ここからしばらく電話機のたとえで突っ走ります。)
TCPとのたとえ上、以下の3つに分解します。
(1)受話器送信と受信を行う部分ですね。電話を受ける方でもかける方でも同じように使います。
(2)ダイヤル電話をかける為の部分です。電話をかける方にしか必要ありません。
(3)ベル電話がかかってきたことを知らせる部分です。電話を受ける方にしか必要ありません。

サーバは「電話を受ける方」ですね。
普通の電話なら「1対1」(キャッチホンはちょっと忘れてください)ですから電話を受けた方も「受話器が一つ」で構わないのですが、サーバは一度に複数のクライアントからの要求を受けなければなりません。
そうすると「受話器が一つ」では足りません。しかし予めその数を予測することもできません。
なので、「受話器となるソケットはクライアントがつなぎにくるごとに1つ」作り出され、「クライアントとの接続がきれるたびに破棄」されます。
しかし、ベルは1つで良いですね。電話をつなぐと止まりますから。

受話器はその度作り出され、ベルは一つ。
サーバでは「ベル」の為のソケットと、「受話器」の為のソケットの2種類を扱います。つまり、「クライアントが接続してくる」目印であり、「クライアントが接続してきた」ことを知らせるソケットと、実際にクライアントと1対1で通信するソケットです。

サーバ側で最初に作っておくのは「ベル」に相当するソケットです。
「ベル」が鳴ったら「受話器」に相当するソケットを作ります。
「受話器」はそのままクライアントとの通信に入り、「ベル」はまた鳴り出すのを待ちます。

なんとなくサーバプログラムのイメージが掴めましたか?
では今回は「ベル」の説明です。「受話器」は基本的にクライアントと同じですものね。
「ベル」に相当するソケットを作るのですが、作るときはクライアントと全く同じ作り方です。
ソケットを作ったときはクライアントもサーバも無いのですね。
function socket(af, struct, protocol: Integer): TSocket;
afはAF_INET、structはSOCK_STREAM、protocolには0を指定します。
返ってくるのは「ソケット」です。失敗したらINVALID_SOCKETです。
改めて説明するまでもないと思うんだけど、前のページを繰るのは面倒でしょうからちょっとだけ再掲しました。

次にこの「ベル」にクライアントの制限とポートを設定します。
function bind(s: TSocket; var addr: TSockAddr; namelen: Integer): Integer;
sがソケットです。
addrに「クライアントの制限」と待ち受けるポートを入れときます。
クライアントの制限とは「IPアドレスXXXXからの要求しか認めない」という指定なんですが、普通そんな制限しませんよね。
「誰でもいいから」INADDR_ANYというのはここで使います。「制限しない」ということです。
ポートには0以外の決った数値を入れておいてください。
とはいえ、あなたがメールサーバとかWebサーバ等の今までに存在する世間でもよく使われているサービスを作る気でなければ1024番以降を使ってください。
できれば10000番以降をお勧めします。
1024番以前は有名どころのサービスの為に予約されています。Webサーバ用の80番とかです。
でHTTPプロキシあたりがよく8080番とかを使います。HTTPプロキシは有名どころのサービスでは無いのですね。
しかしこれらとあたるのも気持ちが悪い。
で、10000番以降をお勧めするわけです。
0を設定すると「自動的に割り振れ」です。
サーバに自動的に割り振られるとクライアントが困ります。
受信専用の電話で電話番号がそのたびごと変わるような物ですから。
namelenにはaddrのサイズ、固定でsizeof(TSockAddr)としておきます。

addrがvarであってconstでは無いことに注意してください。
bindを呼び出すと変更される可能性があるのです。

bindの返り値は成功したら0、失敗したらSOCKET_ERRORです。
bindは気を付けていてもエラーを返してくるかもしれません。よくあるエラーの時に説明した「そのアドレス(ポート)は使われている」というやつです。自分自身が2重起動されていたり、よそのサーバプログラムとポートがバッティングしていたり、「裏でこっそり動いていた」ソケットとぶつかったりですね。
なもので、bindのエラーはちょっと避け難い物だと思っといてください。
「よそのサーバプログラムとポートがバッティング」している場合にユーザ側で回避できるようにポートはユーザ側で設定できるようにしておくのが良いと思います。
たとえあなたのプログラムがメールサーバやWebサーバなどであり、よく知られているポートを使いたかったとしても。

bindがすんだらそろそろソケットを非同期モードに変えておきましょう。
WSAAsyncSelectです。
function WSAAsyncSelect(s:TSocket; HWindow:HWnd; wMsg: Integer; lEvent:LongInt):Integer;
これもクライアントの時に出てきましたね。
sにソケット、HWindowにメッセージの送り先、wMsgにメッセージID、lEventに何が起こったときメッセージを送ってきて欲しいかです。
このソケットは「ベル」ですから「クライアントが接続してきたら」メッセージを送って欲しいわけです。クライアントのときはFD_READ,FD_WRITE,FD_CLOSE,FD_CONNECTで受信、送信可能時、相手の切断、相手との接続でした。今度はFD_ACCEPTを指定します。

ソケットのモードを非同期に変えたらいよいよ「クライアントの受け付け」を開始します。
function listen(s: TSocket; backlog: Integer): Integer;
backlogには3〜5の数値を選びます。この数値の最大値である5にはSOMAXCONNという定義がありますのでこれを指定すればいいでしょう。
backlogに設定する値は何かということは、acceptの説明のときに行います。

listenの返り値が0なら成功、失敗ならSOCKET_ERRORです。
エラーはバグで無い限りちょっと考えられません。

これでクライアントが接続にくるとWSAAsyncSelectで指定したウィンドゥに指定したメッセージが飛んできます。
このメッセージの捕まえかたは「固まらない通信」を参照してください。lParamにFD_ACCEPTが入っていること以外同じです。

このメッセージが飛んできたところで、この接続してきたクライアント用に「受話器」を作り出します。これを行うのがacceptなんですが、Delphi3とDelphi2で違うんですねぇ。
Delphi3の場合
function accept(s: TSocket; addr: PSockAddr; addrlen: PInteger): TSocket;

Delphi2の場合
function accept(s: TSocket; var addr: TSockAddr; var addrlen: Integer): TSocket;
addrとaddrlenでDelphi2ではvar引数、Delphi3ではポインタとなっています。
仕方ないのでどちらでも通るように条件コンパイルを行いましょう。
var
  sock:   TSocket;
  addr:   TSockAddr;
  addrlen:Integer;
begin
  addrlen:=SizeOf(addr);
  FillChar(addr,SizeOf(addr),0);
{$IFDEF VER100}
{ Delphi3 }
  sock:=accept(BellSock,@addr,@addrlen);
{$ELSE}
{ Delphi2 }
  sock:=accept(BellSock,addr,addrlen);
{$ENDIF}
addrlenにはSizeOf(addr)としておけばOKです。
引数のaddrには何も設定しなくてもかまいません。acceptが設定してかえします。
acceptはaddrに相手(クライアント)の情報を設定してくれるんですが、ほぼ使い道がありません。
相手の情報が欲しくなったら、getpeernameというWinsockAPIを使えば分かるのでとっておく必要もないのです。
getpeernameはあまり使わないので説明を省きますが。

acceptの返り値が「受話器」に相当するソケットです。
エラーしたらINVALID_SOCKETですが、ちょっとエラーする状況が思い付きません。
受話器に相当するソケットは即座にWSAAsyncSelectでFD_READ,FD_WRITE.FD_CLOSEを設定してしまいましょう。
FD_CONNECTは必要ありません。すでにこのソケットはクライアントと接続されています。
このソケットは各クライアントと通信するために使いますので、ちゃんと管理しておいてくださいね。
「受話器」ソケット用のプログラムはほとんどクライアントと同じになります。
送信バッファも必要でしょうし、送信/受信のインタフェースもほぼ同じはず。
クライアント用のコンポーネントをちょっといじってまったく同じにしてしまうってのがISPのTTCPのとっている方策です。
(あれはサーバもクライアントも1つのコンポーネントでやってますが。)

「ベル」に相当するソケットは、以後「クライアントが接続してきた」というメッセージが飛んでくる毎にacceptを呼び出すだけです。
アプリケーションが終了するときにはこのソケットもclosesocketで閉じておいてください。
「受話器」に相当するソケットはsocketで作り出したわけではありませんが、これも各クライアントとの通信が終わったらclosesocketで閉じます。

ここでlistenの引数backlogの数値について説明します。
listenでクライアントを待ちはじめると、クライアントが接続にくるたびにWSAAsyncSelctで指定したメッセージが飛んできます。
しかし、このメッセージの処理を行いacceptするまでが遅かったらどうでしょう?
この間に次のクライアントが接続しにきたら?
winsockは、実際にクライアントが接続にくるととりあえずソケットを作成しそのまま接続します。そしてそのソケットをキューにいれて「クライアントが接続にきた」というメッセージを飛ばすわけです。
acceptはそのキューからソケットを取り出しているだけです。
このキューの大きさを指定するのがlistenのbacklog引数です。
この大きさはサーバの処理速度に依存するのですが、そもそも3〜5しか指定できませんのでそんなに指定の自由度がある訳ではありません。
SOMAXCONN固定でいいんじゃないでしょうか。

さてと、今回はサンプルプログラムもありませんがこの辺で終わります。
ここまででサーバプログラムのwinsockの使い方の説明は終わってるんですがね。
逆に次回はほとんどWinsock固有の部分がありません。

次の話
[表紙] [Program Files] [オブジェクト指向異聞] [プログラム未整理知識] [Winsockを使ってみようぜ] [だべり] [What's New] [書いた奴] [リンク]