【連載】Socket 4 PHP徹底研究(1)
【連載】ソケット徹底研究4 PHP (1)
2011年5月5日
ソケット(ソケット)は常にネットワーク層の基礎となるコアコンテンツであり、TCPの基礎となるプロトコルでもあります/IP および UDP チャネルを実装します。インターネット情報時代の爆発的な発展に伴い、現代のサーバーのパフォーマンスはますます課題に直面しており、有名な C10K 問題 (http://www.kegel.com/c10k.html) も出現しています。専門家たちのたゆまぬ努力のおかげで、従来の select/poll とは異なる epoll/kqueue 方式が登場し、現在では Linux 2.6 以降のカーネルで一般的にサポートされています。これはソケット分野における大きな進歩です。それは C10K 問題を解決するだけでなく、徐々に現代のインターネットの基礎となるコアテクノロジーになりました。 libevent ライブラリは、最も優れたプロジェクトの 1 つです (現在、Memcached を含む多くのオープン ソース プロジェクトで使用されています)。興味のある方はぜひ研究してみてください。
システムのこの部分を紹介する記事はインターネット上にあまりなく、PHP に関係する記事はさらに少ないため、Shitoujun はここで、「ソケットの深層探索」シリーズを通じて、この分野に興味がある読者にある程度の確実性を与えたいと考えています。 4PHP」 この問題についてさらに詳しい議論にご参加いただければ幸いです。まず、ソケット分野で現在混乱している概念、つまりブロッキング/非ブロッキング、同期/非同期、多重化などについて説明します。
1. ブロッキング/非ブロッキング: これら 2 つの概念は、IO プロセス中のプロセスのステータスに基づいています。反対に、IO のブロッキングは、呼び出し結果が返される前に現在のスレッドが一時停止されることを意味します。ブロッキングは、呼び出し結果が返される前に現在のスレッドが一時停止されることを意味します。逆に、非ブロックは、呼び出し結果が返される前に現在のスレッドが一時停止されることを意味します。この関数は、結果を取得する前に現在のスレッドをブロックしません。 、しかしすぐに戻ってきます。
2. 同期/非同期: これら 2 つの概念は、呼び出しが結果を返すかどうかに基づいています。いわゆる同期とは、逆に、関数呼び出しが発行されたときに、結果が取得されるまで呼び出しが返されないことを意味します。 、関数呼び出しが発行されると、結果が取得されるまで呼び出しは返されません。非同期プロシージャ呼び出しが発行された後、呼び出し元は、実際に呼び出しを処理するコンポーネントが完了した後、呼び出し元に通知します。ステータス、通知、コールバックを通じて。
3. 多重化 (IO/Multiplexing): ネットワーク通信回線におけるデータ情報伝送の効率を向上させるために、物理的な通信回線上に複数の論理的な通信チャネルを確立し、複数の信号を同時に送信する技術をいいます。多重化技術。 Socket の場合、複数の接続を同時に処理できるモデルを多重化と呼ぶ必要があります。現在、より一般的に使用されている IO モデルは select/poll/epoll/kqueue です (現在、Apache のような接続ごとのモデルもあります)。 IOモデルは別のプロセス/スレッドで処理されますが、効率が比較的悪く、問題が発生しやすいため、当面は導入しません)。これらの多重化モードの中で、非同期ブロッキング/ノンブロッキング モードが最も優れたスケーラビリティとパフォーマンスを備えています。
この概念は非常に抽象的だと思いますよね? 「すべての答えはその場にある」 3 つの古典的な PHP ソケット IO モデルの例から、上記の概念を再度分析してみましょう:
1. accept を使用する古代の方法。ブロッキング モデル: これは、同期ブロッキング IO モデルです。コードは次のとおりです。 $errno, $errstr; if ($port ソケット = stream_socket_server("tcp://0.0.0.0:{$port}", $errno, $errstr); if (!$socket) die("$errstr ($errno) )"); // stream_set_timeout($socket, -1); // サーバーソケットがタイムアウトしないことを確認します。役に立たないようです:) while ($conn = stream_socket_accept($socket, -1)) { // のみこの設定がタイムアウトしない場合は static $ を使用します。 id = 0; $ct_last = $ct; $ct_data = ''; Client $id Come.n"; while (! preg_match('/r?n/', $buffer)) { // 終了文字は読み込まれないので読み続けます // if (feof($conn)) Break; / / Popen および fread のバグによる無限ループを防止 $buffer = fread($conn, 1024); //読み取り数を出力 $ct += strlen($buffer); '/r?n/', '', $buffer ); } $ct_size = ($ct - $ct_last) * 8; echo "[$id] " . $ct_data . ; fwrite($conn, "$ct_size バイトのデータを受信しました。rn"); fclose($socket) } } new SocketServer(2000);ソケット テスト クライアント * James.Huang 作成 **/ function debug ($msg) { // echo $msg; error_log($msg, 3, '/tmp/socket.log') } if ($argv[1]); { $socket_client = stream_socket_client('tcp://0.0.0.0:2000', $errno, $errstr, 30); // stream_set_blocking($socket_client, 0) // stream_set_timeout($socket_client, 0, 100000); (!$socket_client) { die("$errstr ($errno)"); } else { $msg = trim($argv[1]); for ($i = 0; $i wait} fwrite($socket_client, " rn"); // 送信ターミネータ debug(fread($socket_client, 1024)); fclose($socket_client); } } else { // $phArr = array(); // for ($i = 0; $i Sendデータを送信し、最後にターミネータを送信します。サーバーソケットサーバーは、accept ブロッキング メソッドを使用してソケット接続を受信し、ターミネータが受信されるまでループしてデータを受信し、結果データ (受信したバイト数) を返します。ロジックは非常に単純ですが、分析する価値のある状況がいくつかあります。
A> デフォルトでは、phpソケット_クライアント.phpテストを実行すると、クライアントは10個のWを出力し、その後に受信したデータが出力されます。 , /tmp/socket .log には、サーバーから返された受信結果データが記録されます。この状況は理解しやすいため、詳細は説明しません。次に、telnet コマンドを使用して複数のクライアントを同時に開くことがわかります。サーバーは一度に 1 つのクライアントのみを処理し、他のクライアントは「キュー」で処理する必要があります。これがこのモードの弱点であり、効率が非常に低くなります。 B>socket_client.php の 26 行目のコメント コードのみを開き、php のソケット_client.php テスト クライアントを再度実行します。W を入力した後、サーバーは R も入力し、両方のプログラムが停止しました。これはなぜでしょうか? ロジックを分析すると、クライアントがターミネータを送信する前にデータをサーバーに返したいため、またサーバーがターミネータを受信していないため、クライアントにターミネータを要求していることがわかります。デッドロックを引き起こす。 W と R が 1 つだけ入力されているのは、fread がデフォルトでブロックしているためです。このデッドロックを解決するには、socket_client.php の 16 行目のコメント コードを開き、ソケットのタイムアウトを 0.1 秒に設定する必要があります。再度実行すると、0.1 秒ごとに W と R が表示されて終了することがわかります。正常に受信され、サーバーも正常に受信結果を返します。 fread がデフォルトでブロックしていることがわかります。タイムアウトが設定されていない場合は、デッドロックが発生しやすくなります。
C> 15 行のコメントを開いて phpソケット_クライアント.php テストを実行すると、結果は基本的にケース A と同じになります。唯一の違いは、/tmp/socket.log に戻りデータが記録されないことです。ここで、ブロッキング モードと非ブロッキング モードで実行されているクライアントの違いがわかります。もちろん、クライアントが結果の受け入れを気にしない場合は、非ブロッキング モードを使用して最大の効率を得ることができます。
D> phpsocket_client.php を実行することは、上記のロジックを 10 回連続して実行することですが、非常に奇妙なのは、35 ~ 41 行のコードを使用して 10 個のプロセスを開くことです。同時に、サーバー側で無限ループが発生しますが、これは非常に奇妙です。その後、調査の結果、PHP のソースコードを確認したところ、popen で開いたプロセスによって作成された接続がある限り、fread または Socket_read がエラーとなり、直接空の文字列を返し、無限ループに陥ることが判明しました。 PHP の Popen 関数と fread 関数は C にまったくネイティブではないことがわかり、大量の php_stream_* 実装ロジックが挿入されています。これは当初、そのバグによるソケット接続の中断が原因であると推定されています。解決策は、socket_server.php の 31 行のコードを開いて、接続が中断された場合にループから抜け出すことです。ただし、この方法では大量のデータが失われるため、この問題には特別な注意が必要です。
2. select/poll を使用した同期モデル: 同期ノンブロッキング IO モデルであり、コードは次のとおりです:
select_server.php
**/ set_time_limit(0); クラス SelectSocketServer { プライベート静的 $socket; プライベート静的 $timeout = 1024; 関数 SelectSocketServer($port); $errstr; if ($portソケット =socket_create_listen($port); if (!$socket) die("Listen $port failed");ソケットセット_nonblock($socket); // ノンブロッキング while (true) { $readfds = array_merge (self::$connections, array($socket)); $writefds = array(); // 読み取りおよび書き込み接続チャネルを取得する接続を選択します if (socket_select($readfds, $writefds, $e = null, $) t = self::$timeout)) { // 現在のサーバーのリスニング接続の場合 if (in_array($socket, $readfds)) { // クライアント接続を受け入れる $newconn =ソケット_accept($socket); int ) $newconn; $reject = ''; if (count(self::$connections) >= self::$maxconns) { $reject = "サーバーがいっぱいです。後で再試行してください。n";現在の顧客 終了接続をsocket_selectに入れて、self::$connections[$i] = $newconn; //接続リソースキャッシュコンテナを入力 $writefds[$i] = $newconn; //接続が異常です if ($reject; ) {socket_write($writefds[$i], $reject); unset($writefds[$i]); } else { echo "クライアント $i Come.n"; client-with-data 配列からリスニングソケットを削除 $key = array_search($socket, $readfds); unset($readfds[$key]); } // ラウンドロビン読み取りチャネル foreach ($readfds as $rfd) { // クライアント接続 $i = (int) $rfd; // チャネルから読み取ります $line = @socket_read($rfd, 2048, PHP_NORMAL_READ); if ($line === false) { // コンテンツを読み取ることができません、終了接続エコー "ソケット $i.n で接続が閉じられました"; self::close($i); if ($tmp != "r" && $tmp != " n ") { // さらなるデータを待つ continue; } // 処理ロジック $line = トリム($line); if ($line == "quit") { echo "Client $i quit.n"; self:: close ($i); Break; } if ($line) { echo "Client $i >>" } } // ラウンドロビン書き込みチャネル ($wfd) $ i = (int) $wfd; $w =ソケット_ライト($wfd, "ようこそクライアント $i!n"); } } } 関数 close ($i) {ソケットシャットダウン(self::$connections[$i]) ;socket_close(self::$connections[$i]); unset(self::$connections[$i]); } } new SelectSocketServer(2000); **/ 関数のデバッグ ( $ msg) { // echo $msg; error_log($msg, 3, '/tmp/socket.log') } if ($argv[1]) { $socket_client = stream_socket_client('tcp://0.0.0.0) : 2000', $errno, $errstr, 30); // stream_set_timeout($socket_client, 0, 100000); if (!$socket_client) { die("$errstr ($errno)");トリム ($argv[1]); for ($i = 0; $i wait} fwrite($socket_client, "quitn"); // エンドトークンを追加 debug(fread($socket_client, 1024)); fclose($socket_client ) ; } } else { $phArr = array(); for ($i = 0; $i ここで php select_client.php プログラムを実行すると、同時に 10 個の接続が開かれ、ログインしたユーザーの操作が同時にシミュレートされ、サーバー データに出力される内容を観察すると、サーバーが実際にこれらの接続を同時に処理していることがわかります。これは、もちろん、多重化によって実装されたノンブロッキング IO モデルです。は、最終的にはサーバー プログラムがチャネルからデータを読み取り、結果を取得した後にクライアントに同期的に返す必要があるため、実際には非同期を実装していません。今回、telnet コマンドを使用して複数のクライアントを同時に開くと、サーバーがこれらの接続を同時に処理できることがわかります。これは、もちろん、非ブロッキング IO よりもはるかに効率的です。古いブロッキング IO ですが、このモードにはまだ制限があります。読み続けるとわかります ~
B> select_server.php でいくつかのパラメータを設定しました。調整してみてください。
$timeout: を表します。 select のタイムアウト、これ 一般に、短すぎると過度の CPU 負荷が発生するため、短すぎないようにしてください。
$maxconns: クライアントがこの数を超えると、サーバーは接続の受信を拒否します。ここで注意すべき点は、select はハンドルを通じて読み書きされるため、システムのデフォルト パラメータ __FD_SETSIZE によって制限されることです。これを変更する場合は、さらにカーネルを再コンパイルする必要があります。選択モードのパフォーマンスは、接続数が増加すると直線性が低下することが判明しています (詳細については、「Socket Deep Study 4PHP (2)」を参照してください)。超高同時実行サーバーでは、次のモードを使用することをお勧めします。
3. epoll/kqueueを使用した非同期モデル: 非同期ブロッキング/ノンブロッキングIOモデルで、コードは次のとおりです:
epoll_server.php
* * 定義された定数: * * EV_TIMEOUT (整数) * EV_READ (整数) * EV_WRITE (整数) * EV_SIGNAL (整数) * EV_PERSIST (整数) * EVLOOP_NONBLOCK (整数) * EVLOOP_ONCE (整数) **/ set_time_limit(0);クラス EpollSocketServer {プライベート静的 $socket;プライベート静的 $connections;プライベート静的 $buffers; function EpollSocketServer ($port) { global $errno, $errstr; if (!extension_loaded('libevent')) { die("最初に libevent 拡張機能をインストールしてください"); if ($port ソケット, $flag, $base) { static $id = 0; $connection = stream_socket_accept($socket); stream_set_blocking($connection, 0); $id++; // 受け入れるたびに増加 $buffer =event_buffer_new($connection, array(__CLASS__, 'ev_read'), array(__CLASS__, 'ev_write'), array(__CLASS__, 'ev_error'), $id); event_buffer_base_set($buffer, $base); event_buffer_timeout_set($buffer, 30, 30); event_buffer_watermark_set($buffer, EV_READ, 0, 0xffffff);イベントバッファー優先順位セット($buffer, 10); event_buffer_enable($buffer, EV_READ | EV_PERSIST); // バッファと接続の両方を self::$connections[$id] = $connection; の外側に保存する必要があります。 self::$buffers[$id] = $buffer; } function ev_error($buffer, $error, $id) {event_buffer_disable(self::$buffers[$id], EV_READ | EV_WRITE); events_buffer_free(self::$buffers[$id]); fclose(self::$connections[$id]); unset(self::$buffers[$id], self::$connections[$id]);関数 ev_read($buffer, $id) { 静的 $ct = 0; $ct_last = $ct; $ct_data = ''; while ($read =event_buffer_read($buffer, 1024)) { $ct += strlen($read); $ct_data .= $read; $ct_size = ($ct - $ct_last) * 8; echo "[$id] " 。 __方法__ 。 " > " 。 $ct_data 。 「ん」; event_buffer_write($buffer, "$ct_size バイトのデータを受信しました。rn"); } function ev_write($buffer, $id) { echo "[$id] " 。 __方法__ 。 「ん」;新しい EpollSocketServer(2000);
epoll_client.php
**/ function debug ($msg) { // echo $msg; error_log($msg, 3, '/tmp/socket.log'); if ($argv[1]) { $socket_client = stream_socket_client('tcp://0.0.0.0:2000', $errno, $errstr, 30); // stream_set_blocking($socket_client, 0); if (!$socket_client) { die("$errstr ($errno)"); } else { $msg = トリム($argv[1]); for ($i = 0; $i は結果 (受信した文字数) を返します。ただし、php epoll_client.php を実行すると、サービスエンドの印刷結果が通知され、障害モデルが受け入れられる可能性があります。当然、実行効率も大幅に向上します。これは何のためですか?まず、epoll モードは基本的に制限されていません (cat /proc/sys/fs/file-max を参照すると、制限は 300K に達します。つまり、epoll に基づいて調整されている Erlang サービス端末は、これを同時に処理できます)。接続の根本的な原因は、PHP の理論上では考えられませんが、epoll モードのパフォーマンスも、接続数の増加に応じて選択モードのように変化することはなく、調査のパフォーマンスも非常に安定しています。
epoll は LT (レベル トリガー) と ET (エッジ トリガー) の 2 つのモードで動作します。前者は省スペース モードであり、同時にブロックと非ブロック IO モードをサポートします。後者の違いは、しかし、より厳密であり、一般に、私たちは実際の使用ではこのモードを使用しています (ET モードと WinSock は両方ともノンブロック モードです)。システムの I/O デマルチプレクス機構は、実行段階では構成に基づいて再選択されるため、サポートされていません。そのため、友人が興味を持っている場合は、libevent の詳細を確認できません。参照: http:// Monkey.org/~provos/libevent/。
ここまでで、最初の部分の内容は終了です。相信大家はソケットプロセスの重点概念といくつかの実用的な技術を理解しています。 )》私たちは、select/poll/epoll/kqueue モードについてさらに深く掘り下げた仲介および比較を検討し、さらに 2 つの重要な I/O 多重使用モード、Reactor モードと Proactor モードについても説明します。続く…