SSブログ

PHP でサーバソケットプログラミング (2) [PHP]

前回のプログラムの問題点

前回 [1] の sock_try2.php は同じ期間に1つの接続しか受け入れない設計でした。
クライアントから以下のように実行してみるとそれが分かります。

% set f [socket localhost 9999]
sock1544
% set g [socket localhost 9999]
sock1532
% puts $f f; flush $f
% puts $g g; flush $g
% puts $f f; flush $f
% puts $g g; flush $g
% puts $f f; flush $f
% puts $g g; flush $g
% close $f
% close $g

これを実行するとサーバ側は以下のようになります。

php sock_try2.php
f
f
f
g
g
g

クライアントからは2つの接続から交互に入力したつもりなのにサーバでは最初の接続が閉じてから2つ目の接続を処理しています。これは socket_try2.php が最初の接続で socket_accept して以降はその接続から読み切るまで次の socket_accept を実行しないためです。

複数接続を可能にする2つの方法

複数の接続に対応する方法の一つは fork を使うというものです。
これは socket_accept で新しい接続を受け入れたらその接続についての処理は子プロセスに引継ぎ、親プロセスはすぐに待ち受けに戻るというやりかたです。

しかし Windows 版の PHP では fork を使うことができないようなのでこれはあきらめます。

もう一つの方法は単一のプロセスで複数のソケットを管理し、かわりばんこに処理を行う方法です。かわりばんこに処理をすると言ってもデータが来ないソケットに対して socket_read を実行するとそこでブロックされてしまうのでプログラムは「管理しているソケットのうちどれがデータ読みこみ可能か」を知る必要があります。これを知るための関数が socket_select です。

socket_select はソケットの配列を受け取り、そのうちのどれかに変化があるまでブロックします。どれかに変化があったら配列を変化のあったソケットのみの配列に書き換えます。戻り値は変化のあったソケットの数です。

$num_changed_sockets = socket_select($read, $write = NULL, $except = NULL, NULL);

第1引数に読み込みを監視するソケットの配列を渡します。
第2引数が書き込みの監視、第3引数が例外の監視ですが、これらは今回は注目しないので NULL としています。
第4引数はタイムアウト時間ですが、基本的に NULL を指定していつまでも待つようにします。

これを使って複数接続可能版のサーバを書いて見ます。

複数接続可能版のサーバ
<?php // sock_try3.php

// 配列から要素を取り除く関数
function remove_elem(&$arr, $elem) {
  foreach ($arr as $k => $v) {
    if ($v == $elem) {
      unset($arr[$k]);
    }
  }
}

$port = 9999;
$sock = socket_create_listen($port);

// 待ち受けソケット+現在繋ぎに来ているソケットの配列
$socks = array($sock);

while (true) {
  $read = $socks; // socket_select は配列を破壊するので一旦別の変数に移す
  $num_changed_sockets = socket_select($read, $write = NULL, $except = NULL, NULL);

  foreach ($read as $changed) {
    if ($changed == $sock) {
      // 待ち受けソケットに変化=新しい接続が来た
      $clientsock = socket_accept($sock);
      $socks[] = $clientsock; // ソケット配列に追加
    } else {
      $buf = socket_read($changed, 1024);
      if ($buf == "") {
        // 読み込みが終わったらクローズしてソケット配列から削除
        socket_close($changed);
        remove_elem($socks, $changed);
      } else {
        echo $buf;
      }
    }
  }
}

socket_close($sock);

?>

プログラムが現在管理しているソケットは $socks 配列に入ります。これには最初は待ち受けソケットだけが入っています。

ここに入っているソケット配列を socket_select にかけるのですが、前述したようにこの関数は渡された配列を破壊的に書き換えるので一旦 $read という配列に移して渡しています。
これが終わったあとは $read に「変化のあったソケット」が入っています。

変化のあったそれぞれのソケットについて処理を行いますが「変化のあったソケットが待ち受けソケットかどうか」で処理の分岐を行います。
待ち受けソケットに変化があったというのは新しい接続が来たということを意味しているので、その場合は socket_accept を実行して得られた新しいソケットをソケット配列に追加します。
それ以外の場合はそのソケットからデータを読み込めるということなので socket_read を実施します。
読み込みが終わったらソケットを閉じてソケット配列から該当ソケットを除去します。配列から要素を取り除くために remove_elem 関数も定義してみました。

では先ほどと同様に複数接続を実験してみましょう。

% set f [socket localhost 9999]
sock1540
% set g [socket localhost 9999]
sock1532
% puts $f f; flush $f
% puts $g g; flush $g
% puts $f f; flush $f
% puts $g g; flush $g
% puts $f f; flush $f
% puts $g g; flush $g
% close $f
% close $g

今度はサーバ側はこうなります。

F:\>php sock_try3.php
f
g
f
g
f
g

ちゃんと複数接続をハンドリングできました。

なお実際のプログラムで使うにはちゃんと socket_* 関数のエラーチェックも必要です。

[1] http://blog.so-net.ne.jp/rainyday/2007-02-17


nice!(0)  コメント(0)  トラックバック(0) 
共通テーマ:パソコン・インターネット

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

トラックバック 0

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。