本番環境のWebsocket

10ヶ月前、私はブラりザのおもちゃを䜜り始めたした。 cocos jsはグラフィックスずしお、websocketはサヌバヌずの通信ずしお遞択されたした。 私はこのテクノロゞヌが本圓に奜きで、その䞊でサヌバヌずのゲヌムの通信をすべお敎理したした。 このためにこの蚘事を䜿甚したした。 しかし、残念ながら、その蚘事に蚘茉されおいるコヌドは実皌働では䜿甚できたせん。 刀明したように、問題のレベルは重倧ではなく、ブロックされおいたす。 すべおが非垞に悪いので、サヌバヌずのすべおの通信をWeb゜ケットからロングプヌルたで曞き盎す必芁がありたした。 最埌に、「サファリではなくブラりザを䜿甚しおいる堎合はwebsocketを䜿甚し、それ以倖の堎合はロングポヌリング」ずいうオプションを残し、このトピックに぀いおもう少し分岐したす。



そのため、実皌働環境でWeb゜ケットを䜿甚した経隓はたずもなものになりたした。 そしお最近、ハブレに関する最初の蚘事を曞くように促したむベントが起こりたした。



おもちゃが゜ヌシャルネットワヌクに公開された埌、芋぀かったすべおのクリティカル/ブロッキングバグを修正し、すべおを静かなモヌドで敎理し始めたした。 この䟋は、䞀般的に、コヌドに挿入しお䜿甚できるサヌバヌコヌドを含むむンタヌネット䞊の唯䞀のガむドであるずいう事実に泚目したいず思いたす。 さお、怜玢゚ンゞンで「php websocket server」ず入力したす-自分でむンストヌルできるものを芋぀けおください。



突然、私は䞊蚘の蚘事を読み盎し、最初に「phpdaemon」ず「ratchet」ぞのリンクを芋぀けたした。 それでは、冷静にコヌドを芋おみたしょう。 PhpDeamonでは、WebSocket接続凊理の腞内で、WebSocketプロトコルぞの小さなしかし非垞に重芁な分岐がありたす。 たた、1぀のケヌスで「Safari5ず倚くの非ブラりザヌクラむアント」ず衚瀺されたす。 私がおかしくなりそうだず蚀うこずは、䜕も蚀わないこずです。 私の目の前で数癟時間のフラッシュが発生し、倚くの手間ず苊しみがあり、プロゞェクト党䜓に疑問を投げかけたした。 信じられなかったので、確認するこずにしたした。



〜15時間以内に、PhpDeamonからWebSocketに関連する最小限のコヌドを匕き出したした最新バヌゞョンのすべおのブラりザヌで動䜜し、サヌバヌコヌド自䜓は高負荷でも動䜜したす。説明付きで公開しようず思いたす。 他の人が私が経隓しなければならない苊痛を経隓しないように。 はい、コヌドは小さくありたせんでしたが、申し蚳ありたせん。WebSocketはクラむアント偎では非垞にシンプルであり、サヌバヌ偎ではすべおが非垞に倧きくなりたすSafari開発者に別の「ありがずう」ずしたしょう。 たた、WebSocketのスコヌプは䞻にゲヌムであるずいう事実により、サヌバヌ゜ケットのノンブロッキング䜿甚の問題が重芁です-これはボヌナスの耇雑さであり、非垞に重芁ですが、 ここでは決しお考慮したせん。



より理解しやすいように、オブゞェクトのないテストアプリケヌションを䜜成したかったのです。 しかし、残念ながら、この䟋のこのアプロヌチでは倚くの繰り返しコヌドが生成されるため、1぀のクラスず3぀の継承者を远加する必芁がありたした。 残りはすべおオブゞェクトなしです。



たず、クラむアント郚分
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>WebSocket test page</title> </head> <body onload="create();"> <script type="text/javascript"> function create() { // Example ws = new WebSocket('ws://'+document.domain+':8081/'); ws.onopen = function () {document.getElementById('log').innerHTML += 'WebSocket opened <br/>';} ws.onmessage = function (e) {document.getElementById('log').innerHTML += 'WebSocket message: '+e.data+' <br/>';} ws.onclose = function () {document.getElementById('log').innerHTML += 'WebSocket closed <br/>';} } </script> <button onclick="create();">Create WebSocket</button> <button onclick="ws.send('ping');">Send ping</button> <button onclick="ws.close();">Close WebSocket</button> <div id="log" style="width:300px; height: 300px; border: 1px solid #999999; overflow:auto;"></div> </body> </html>
      
      







私のゲヌムでは、3぀のサヌバヌ゜ケットを䜿甚する必芁がありたした。 WebSocket、ワヌカヌ甚、およびロングプヌル甚。 ゲヌムにはたくさんの数孊がありたすので、私たちはワヌカヌをしお、コンピュヌティングのためのタスクを䞎えなければなりたせんでした。 それが目的です。 そのstream_selectは、それらすべおに共通である必芁がありたす。そうでないず、遅延や狂ったプロセッサヌ䜿甚が発生したす。 この知識は、䜿甚枈みの神経の山ず匕き換えに埗られたした。



メむンサヌビスサむクル
 $master = stream_socket_server("tcp://127.0.0.1:8081", $errno, $errstr); if (!$master) die("$errstr ($errno)\n"); $sockets = array($master); stream_set_blocking($master, false); //      ,         ,     "stream_socket_accept". ,         - ,      -  . while (true) { $read = $sockets; $write = $except = array(); if (($num_changed_streams = stream_select($read, $write, $except, 0, 1000000)) === false) { var_dump('stream_select error'); break; //    ,   "die",                "/etc/init.d/game restart"  100%   case,     "pcntl"       . } foreach ($read as $socket) { $index_socket = array_search($socket, $sockets); if ($index_socket == 0) { //   continue; } //      } }
      
      







新しいクラむアントずの接続は非垞に暙準的なコヌドですが、゜ケットが非ブロックモヌドであるずいう事実により、すべおの着信デヌタを断片的に収集し、十分なデヌタがある堎合にそれを凊理し、必芁なプロトコルを理解するコヌドの束を蚘述する必芁がありたすこのプロトコルを䜿甚するには、䜿甚しお切り替えたす。 この1぀のタスクはすでに倧量のコヌドであり、PhpDeamonではWebSocketずは関係のないコヌドを倧量に積みたした8぀の異なるサヌバヌをそこに䞊げるこずもできたす。 このトピックでは、倚くの郚分を切り離しお単玔化するこずができたした。 圌はWebSocketに関連するものだけを残したした。



切り捚おられたファむル<ws.php>
 class ws { const MAX_BUFFER_SIZE = 1024 * 1024; protected $socket; /** * @var array _SERVER */ public $server = []; protected $headers = []; protected $closed = false; protected $unparsed_data = ''; private $current_header; private $unread_lines = array(); protected $extensions = []; protected $extensionsCleanRegex = '/(?:^|\W)x-webkit-/iS'; /** * @var integer Current state */ protected $state = 0; // stream state of the connection (application protocol level) /** * Alias of STATE_STANDBY */ const STATE_ROOT = 0; /** * Standby state (default state) */ const STATE_STANDBY = 0; /** * State: first line */ const STATE_FIRSTLINE = 1; /** * State: headers */ const STATE_HEADERS = 2; /** * State: content */ const STATE_CONTENT = 3; /** * State: prehandshake */ const STATE_PREHANDSHAKE = 5; /** * State: handshaked */ const STATE_HANDSHAKED = 6; public function get_state() { return $this->state; } public function closed() { return $this->closed; } protected function close() { if ($this->closed) return; var_dump('self close'); fclose($this->socket); $this->closed = true; } public function __construct($socket) { stream_set_blocking($socket, false); $this->socket = $socket; } private function read_line() { $lines = explode(PHP_EOL, $this->unparsed_data); $last_line = $lines[count($lines)-1]; unset($lines[count($lines) - 1]); foreach ($lines as $line) { $this->unread_lines[] = $line; } $this->unparsed_data = $last_line; if (count($this->unread_lines) != 0) { return array_shift($this->unread_lines); } else { return null; } } public function on_receive_data() { if ($this->closed) return; $data = stream_socket_recvfrom($this->socket, MAX_BUFFER_SIZE); if (is_string($data)) { $this->unparsed_data .= $data; } } /** * Called when new data received. * @return void */ public function on_read() { if ($this->closed) return; if ($this->state === self::STATE_STANDBY) { $this->state = self::STATE_FIRSTLINE; } if ($this->state === self::STATE_FIRSTLINE) { if (!$this->http_read_first_line()) { return; } $this->state = self::STATE_HEADERS; } if ($this->state === self::STATE_HEADERS) { if (!$this->http_read_headers()) { return; } if (!$this->http_process_headers()) { $this->close(); return; } $this->state = self::STATE_CONTENT; } if ($this->state === self::STATE_CONTENT) { $this->state = self::STATE_PREHANDSHAKE; } } /** * Read first line of HTTP request * @return boolean|null Success */ protected function http_read_first_line() { if (($l = $this->read_line()) === null) { return null; } $e = explode(' ', $l); $u = isset($e[1]) ? parse_url($e[1]) : false; if ($u === false) { $this->bad_request(); return false; } if (!isset($u['path'])) { $u['path'] = null; } if (isset($u['host'])) { $this->server['HTTP_HOST'] = $u['host']; } $srv = & $this->server; $srv['REQUEST_METHOD'] = $e[0]; $srv['REQUEST_TIME'] = time(); $srv['REQUEST_TIME_FLOAT'] = microtime(true); $srv['REQUEST_URI'] = $u['path'] . (isset($u['query']) ? '?' . $u['query'] : ''); $srv['DOCUMENT_URI'] = $u['path']; $srv['PHP_SELF'] = $u['path']; $srv['QUERY_STRING'] = isset($u['query']) ? $u['query'] : null; $srv['SCRIPT_NAME'] = $srv['DOCUMENT_URI'] = isset($u['path']) ? $u['path'] : '/'; $srv['SERVER_PROTOCOL'] = isset($e[2]) ? $e[2] : 'HTTP/1.1'; $srv['REMOTE_ADDR'] = null; $srv['REMOTE_PORT'] = null; return true; } /** * Read headers line-by-line * @return boolean|null Success */ protected function http_read_headers() { while (($l = $this->read_line()) !== null) { if ($l === '') { return true; } $e = explode(': ', $l); if (isset($e[1])) { $this->current_header = 'HTTP_' . strtoupper(strtr($e[0], ['-' => '_'])); $this->server[$this->current_header] = $e[1]; } elseif (($e[0][0] === "\t" || $e[0][0] === "\x20") && $this->current_header) { // multiline header continued $this->server[$this->current_header] .= $e[0]; } else { // whatever client speaks is not HTTP anymore $this->bad_request(); return false; } } } /** * Process headers * @return bool */ protected function http_process_headers() { $this->state = self::STATE_PREHANDSHAKE; if (isset($this->server['HTTP_SEC_WEBSOCKET_EXTENSIONS'])) { $str = strtolower($this->server['HTTP_SEC_WEBSOCKET_EXTENSIONS']); $str = preg_replace($this->extensionsCleanRegex, '', $str); $this->extensions = explode(', ', $str); } if (!isset($this->server['HTTP_CONNECTION']) || (!preg_match('~(?:^|\W)Upgrade(?:\W|$)~i', $this->server['HTTP_CONNECTION'])) // "Upgrade" is not always alone (ie. "Connection: Keep-alive, Upgrade") || !isset($this->server['HTTP_UPGRADE']) || (strtolower($this->server['HTTP_UPGRADE']) !== 'websocket') // Lowercase comparison iss important ) { $this->close(); return false; } if (isset($this->server['HTTP_COOKIE'])) { self::parse_str(strtr($this->server['HTTP_COOKIE'], self::$hvaltr), $this->cookie); } if (isset($this->server['QUERY_STRING'])) { self::parse_str($this->server['QUERY_STRING'], $this->get); } // ---------------------------------------------------------- // Protocol discovery, based on HTTP headers... // ---------------------------------------------------------- if (isset($this->server['HTTP_SEC_WEBSOCKET_VERSION'])) { // HYBI if ($this->server['HTTP_SEC_WEBSOCKET_VERSION'] === '8') { // Version 8 (FF7, Chrome14) $this->switch_to_protocol('v13'); } elseif ($this->server['HTTP_SEC_WEBSOCKET_VERSION'] === '13') { // newest protocol $this->switch_to_protocol('v13'); } else { error_log(get_class($this) . '::' . __METHOD__ . " : Websocket protocol version " . $this->server['HTTP_SEC_WEBSOCKET_VERSION'] . ' is not yet supported for client "addr"'); // $this->addr $this->close(); return false; } } elseif (!isset($this->server['HTTP_SEC_WEBSOCKET_KEY1']) || !isset($this->server['HTTP_SEC_WEBSOCKET_KEY2'])) { $this->switch_to_protocol('ve'); } else { // Defaulting to HIXIE (Safari5 and many non-browser clients...) $this->switch_to_protocol('v0'); } // ---------------------------------------------------------- // End of protocol discovery // ---------------------------------------------------------- return true; } private function switch_to_protocol($protocol) { $class = 'ws_'.$protocol; $this->new_instance = new $class($this->socket); $this->new_instance->state = $this->state; $this->new_instance->unparsed_data = $this->unparsed_data; $this->new_instance->server = $this->server; } /** * Send Bad request * @return void */ public function bad_request() { $this->write("400 Bad Request\r\n\r\n<html><head><title>400 Bad Request</title></head><body bgcolor=\"white\"><center><h1>400 Bad Request</h1></center></body></html>"); $this->close(); } /** * Replacement for default parse_str(), it supoorts UCS-2 like this: %uXXXX * @param string $s String to parse * @param array &$var Reference to the resulting array * @param boolean $header Header-style string * @return void */ public static function parse_str($s, &$var, $header = false) { static $cb; if ($cb === null) { $cb = function ($m) { return urlencode(html_entity_decode('&#' . hexdec($m[1]) . ';', ENT_NOQUOTES, 'utf-8')); }; } if ($header) { $s = strtr($s, self::$hvaltr); } if ( (stripos($s, '%u') !== false) && preg_match('~(%u[af\d]{4}|%[cf][af\d](?!%[89a-f][af\d]))~is', $s, $m) ) { $s = preg_replace_callback('~%(u[af\d]{4}|[af\d]{2})~i', $cb, $s); } parse_str($s, $var); } /** * Send data to the connection. Note that it just writes to buffer that flushes at every baseloop * @param string $data Data to send * @return boolean Success */ public function write($data) { if ($this->closed) return false; return stream_socket_sendto($this->socket, $data) == 0; } }
      
      







このような切り捚おられた圢匏でのこのクラスの意味は、クラむアントに接続するためにコンストラクタヌで非ブロックモヌドを蚭定するこずです。 次に、メむンルヌプで、デヌタが到着するたびに、すぐに読み取り、「 unparsed_data 」倉数に远加远加したすこれはon_receive_dataメ゜ッドです。 MAX_BUFFER_SIZEの次元を超えおも、悪いこずはたったく起こらないこずを理解するこずが重芁です。 最埌の䟋では、ここで䜕が起こるか、その倀を「5」ず蚀っお、すべおが匕き続き機胜するこずを確認できたす。 単玔に、最初のステップでバッファからのデヌタは無芖されたす-結局、それらは䞍完党であり、2番目、5番目、たたは100番目のアプロヌチから、最埌に、すべおの受信デヌタが入力され、凊理されたす。 同時に、stream_selectは、すべおのデヌタが取埗されるたで、メむンルヌプでマむクロ秒も埅機したせん。 予想されるデヌタの95が完党に読み取られるような定数を遞択する必芁がありたす。

さらにメむンルヌプデヌタの次の郚分を受け取った埌で、蓄積されたデヌタの凊理を詊みたすこれはon_readメ゜ッドです。 wsクラスでは、on_readメ゜ッドは基本的に「最初の行を読み取り、環境倉数を準備する」、「すべおのヘッダヌを読み取る」、「すべおのヘッダヌを凊理する」ずいう3぀のステップで構成されたす。 最初の2぀は説明する必芁はありたせんが、非ブロッキングモヌドであり、デヌタがどこでも匕き裂かれおいるずいう事実に備えなければならないため、かなり倧量に曞かれおいたす。 ヘッダヌの凊理では、最初に芁求の圢匏が正しいかどうかをチェックし、次にヘッダヌがクラむアントず通信するプロトコルを決定したす。 その結果、 switch_to_protocolメ゜ッドをプルする必芁がありたす 。 内郚のこのメ゜ッドは、クラス「ws_ <protocol>」のむンスタンスを圢成し、メむンルヌプに戻る準備をしたす。



メむンルヌプでは、実際にさらに確認する必芁がありたす。オブゞェクトを眮き換える必芁があるかどうか誰かがこの堎所の実装をより適切に提䟛できる堎合-垞に歓迎したす。



次に、メむンルヌプで、゜ケットが閉じおいるかどうかを確認する必芁がありたす。 閉じおいる堎合は、メモリをクリアしたす詳现は次のブロックで。



<deamon.php>ファむルのフルバヌゞョン
 require('ws.php'); require('ws_v0.php'); require('ws_v13.php'); require('ws_ve.php'); $master = stream_socket_server("tcp://127.0.0.1:8081", $errno, $errstr); if (!$master) die("$errstr ($errno)\n"); $sockets = array($master); /** * @var ws[] $connections */ $connections = array(); stream_set_blocking($master, false); /** * @param ws $connection * @param $data * @param $type */ $my_callback = function($connection, $data, $type) { var_dump('my ws data: ['.$data.'/'.$type.']'); $connection->send_frame('test '.time()); }; while (true) { $read = $sockets; $write = $except = array(); if (($num_changed_streams = stream_select($read, $write, $except, 0, 1000000)) === false) { var_dump('stream_select error'); break; } foreach ($read as $socket) { $index_socket = array_search($socket, $sockets); if ($index_socket == 0) { //   if ($socket_new = stream_socket_accept($master, -1)) { $connection = new ws($socket_new, $my_callback); $sockets[] = $socket_new; $index_new_socket = array_search($socket_new, $sockets); $connections[$index_new_socket] = &$connection; $index_socket = $index_new_socket; } else { //            error_log('stream_socket_accept'); var_dump('error stream_socket_accept'); continue; } } $connection = &$connections[$index_socket]; $connection->on_receive_data(); $connection->on_read(); if ($connection->get_state() == ws::STATE_PREHANDSHAKE) { $connection = $connection->get_new_instance(); $connections[$index_socket] = &$connection; $connection->on_read(); } if ($connection->closed()) { unset($sockets[$index_socket]); unset($connections[$index_socket]); unset($connection); var_dump('close '.$index_socket); } } }
      
      







「$ My_callback」がここに远加されたす-これはクラむアントからのカスタムメッセヌゞハンドラです。 もちろん、本番環境ではあらゆる皮類のオブゞェクトにすべおをラップできたすが、ここでは関数倉数だけを理解しやすくしたす。 圌女に぀いおは少し埌で。



新しい接続の凊理を実装し、ルヌプの本䜓を実装したした。これに぀いおはもう少し䞊に曞きたした。



ここでサヌバヌコヌドに泚意したいです。 ゜ケットから読み取られたデヌタが空の文字列である堎合もちろん、曎新で空の文字列のチェックを芋た堎合、゜ケットを閉じる必芁がありたす。 ああ、私はこの勢いがどれだけ私に血を飲んだのか、そしお䜕人のナヌザヌを倱ったのかさえ知りたせん。 突然、Safariは空の文字列を送信し、これを暙準ず芋なし、このコヌドはナヌザヌぞの接続を取埗しお閉じたす。 Yandexブラりザは時々同じように動䜜したす。 理由はわかりたせんが、この堎合、Safariの堎合、WebSocketはフリヌズしたたたです。぀たり、閉じず、開かず、ハングするだけです。 私はこの魔法のブラりザに無関心ではないこずに気づきたしたか 私はIE6をどのように䜜り䞊げたか芚えおいたす-同じ感芚です。



次に、 array_searchを䜿甚しお、$ sockets配列ず$ connections配列を同期する理由に぀いお説明したす。 事実、stream_selectはクリヌンな$゜ケット配列に䞍可欠であり、他には䜕もありたせん。 しかし、どういうわけか、$ sockets配列の特定の゜ケットをwsオブゞェクトに関連付ける必芁がありたす。 たくさんのオプションを詊しおみたした-最終的に、キヌで垞に同期される2぀のアレむがあるようなオプションで停止したした。 1぀の配列では、stream_selectに必芁なクリヌン゜ケット、2番目の配列では、wsクラスたたはその子孫のむンスタンス。 誰かがより良いこの堎所を提䟛できる堎合-提䟛しおいたす。



泚意すべきもう1぀のケヌスは、 stream_socket_acceptに欠陥がある堎合です。 私が理解しおいるように、理論的には、マスタヌ゜ケットが非ブロッキングモヌドであり、クラむアントを接続するのに十分なデヌタが到着しおいない堎合にのみ可胜です。 したがっお、䜕もしたせん。



ファむル<ws.php>のフルバヌゞョン
 class ws { private static $hvaltr = ['; ' => '&', ';' => '&', ' ' => '%20']; const maxAllowedPacket = 1024 * 1024 * 1024; const MAX_BUFFER_SIZE = 1024 * 1024; protected $socket; /** * @var array _SERVER */ public $server = []; protected $on_frame_user = null; protected $handshaked = false; protected $headers = []; protected $headers_sent = false; protected $closed = false; protected $unparsed_data = ''; private $current_header; private $unread_lines = array(); /** * @var ws|null */ private $new_instance = null; protected $extensions = []; protected $extensionsCleanRegex = '/(?:^|\W)x-webkit-/iS'; /** * @var integer Current state */ protected $state = 0; // stream state of the connection (application protocol level) /** * Alias of STATE_STANDBY */ const STATE_ROOT = 0; /** * Standby state (default state) */ const STATE_STANDBY = 0; /** * State: first line */ const STATE_FIRSTLINE = 1; /** * State: headers */ const STATE_HEADERS = 2; /** * State: content */ const STATE_CONTENT = 3; /** * State: prehandshake */ const STATE_PREHANDSHAKE = 5; /** * State: handshaked */ const STATE_HANDSHAKED = 6; public function get_state() { return $this->state; } public function get_new_instance() { return $this->new_instance; } public function closed() { return $this->closed; } protected function close() { if ($this->closed) return; var_dump('self close'); fclose($this->socket); $this->closed = true; } public function __construct($socket, $on_frame_user = null) { stream_set_blocking($socket, false); $this->socket = $socket; $this->on_frame_user = $on_frame_user; } private function read_line() { $lines = explode(PHP_EOL, $this->unparsed_data); $last_line = $lines[count($lines)-1]; unset($lines[count($lines) - 1]); foreach ($lines as $line) { $this->unread_lines[] = $line; } $this->unparsed_data = $last_line; if (count($this->unread_lines) != 0) { return array_shift($this->unread_lines); } else { return null; } } public function on_receive_data() { if ($this->closed) return; $data = stream_socket_recvfrom($this->socket, self::MAX_BUFFER_SIZE); if (is_string($data)) { $this->unparsed_data .= $data; } } /** * Called when new data received. * @return void */ public function on_read() { if ($this->closed) return; if ($this->state === self::STATE_STANDBY) { $this->state = self::STATE_FIRSTLINE; } if ($this->state === self::STATE_FIRSTLINE) { if (!$this->http_read_first_line()) { return; } $this->state = self::STATE_HEADERS; } if ($this->state === self::STATE_HEADERS) { if (!$this->http_read_headers()) { return; } if (!$this->http_process_headers()) { $this->close(); return; } $this->state = self::STATE_CONTENT; } if ($this->state === self::STATE_CONTENT) { $this->state = self::STATE_PREHANDSHAKE; } } /** * Read first line of HTTP request * @return boolean|null Success */ protected function http_read_first_line() { if (($l = $this->read_line()) === null) { return null; } $e = explode(' ', $l); $u = isset($e[1]) ? parse_url($e[1]) : false; if ($u === false) { $this->bad_request(); return false; } if (!isset($u['path'])) { $u['path'] = null; } if (isset($u['host'])) { $this->server['HTTP_HOST'] = $u['host']; } $address = explode(':', stream_socket_get_name($this->socket, true)); //   $srv = & $this->server; $srv['REQUEST_METHOD'] = $e[0]; $srv['REQUEST_TIME'] = time(); $srv['REQUEST_TIME_FLOAT'] = microtime(true); $srv['REQUEST_URI'] = $u['path'] . (isset($u['query']) ? '?' . $u['query'] : ''); $srv['DOCUMENT_URI'] = $u['path']; $srv['PHP_SELF'] = $u['path']; $srv['QUERY_STRING'] = isset($u['query']) ? $u['query'] : null; $srv['SCRIPT_NAME'] = $srv['DOCUMENT_URI'] = isset($u['path']) ? $u['path'] : '/'; $srv['SERVER_PROTOCOL'] = isset($e[2]) ? $e[2] : 'HTTP/1.1'; $srv['REMOTE_ADDR'] = $address[0]; $srv['REMOTE_PORT'] = $address[1]; return true; } /** * Read headers line-by-line * @return boolean|null Success */ protected function http_read_headers() { while (($l = $this->read_line()) !== null) { if ($l === '') { return true; } $e = explode(': ', $l); if (isset($e[1])) { $this->current_header = 'HTTP_' . strtoupper(strtr($e[0], ['-' => '_'])); $this->server[$this->current_header] = $e[1]; } elseif (($e[0][0] === "\t" || $e[0][0] === "\x20") && $this->current_header) { // multiline header continued $this->server[$this->current_header] .= $e[0]; } else { // whatever client speaks is not HTTP anymore $this->bad_request(); return false; } } } /** * Process headers * @return bool */ protected function http_process_headers() { $this->state = self::STATE_PREHANDSHAKE; if (isset($this->server['HTTP_SEC_WEBSOCKET_EXTENSIONS'])) { $str = strtolower($this->server['HTTP_SEC_WEBSOCKET_EXTENSIONS']); $str = preg_replace($this->extensionsCleanRegex, '', $str); $this->extensions = explode(', ', $str); } if (!isset($this->server['HTTP_CONNECTION']) || (!preg_match('~(?:^|\W)Upgrade(?:\W|$)~i', $this->server['HTTP_CONNECTION'])) // "Upgrade" is not always alone (ie. "Connection: Keep-alive, Upgrade") || !isset($this->server['HTTP_UPGRADE']) || (strtolower($this->server['HTTP_UPGRADE']) !== 'websocket') // Lowercase comparison iss important ) { $this->close(); return false; } /* if (isset($this->server['HTTP_COOKIE'])) { self::parse_str(strtr($this->server['HTTP_COOKIE'], self::$hvaltr), $this->cookie); } if (isset($this->server['QUERY_STRING'])) { self::parse_str($this->server['QUERY_STRING'], $this->get); } */ // ---------------------------------------------------------- // Protocol discovery, based on HTTP headers... // ---------------------------------------------------------- if (isset($this->server['HTTP_SEC_WEBSOCKET_VERSION'])) { // HYBI if ($this->server['HTTP_SEC_WEBSOCKET_VERSION'] === '8') { // Version 8 (FF7, Chrome14) $this->switch_to_protocol('v13'); } elseif ($this->server['HTTP_SEC_WEBSOCKET_VERSION'] === '13') { // newest protocol $this->switch_to_protocol('v13'); } else { error_log(get_class($this) . '::' . __METHOD__ . " : Websocket protocol version " . $this->server['HTTP_SEC_WEBSOCKET_VERSION'] . ' is not yet supported for client "addr"'); // $this->addr $this->close(); return false; } } elseif (!isset($this->server['HTTP_SEC_WEBSOCKET_KEY1']) || !isset($this->server['HTTP_SEC_WEBSOCKET_KEY2'])) { $this->switch_to_protocol('ve'); } else { // Defaulting to HIXIE (Safari5 and many non-browser clients...) $this->switch_to_protocol('v0'); } // ---------------------------------------------------------- // End of protocol discovery // ---------------------------------------------------------- return true; } private function switch_to_protocol($protocol) { $class = 'ws_'.$protocol; $this->new_instance = new $class($this->socket); $this->new_instance->state = $this->state; $this->new_instance->unparsed_data = $this->unparsed_data; $this->new_instance->server = $this->server; $this->new_instance->on_frame_user = $this->on_frame_user; } /** * Send Bad request * @return void */ public function bad_request() { $this->write("400 Bad Request\r\n\r\n<html><head><title>400 Bad Request</title></head><body bgcolor=\"white\"><center><h1>400 Bad Request</h1></center></body></html>"); $this->close(); } /** * Replacement for default parse_str(), it supoorts UCS-2 like this: %uXXXX * @param string $s String to parse * @param array &$var Reference to the resulting array * @param boolean $header Header-style string * @return void */ public static function parse_str($s, &$var, $header = false) { static $cb; if ($cb === null) { $cb = function ($m) { return urlencode(html_entity_decode('&#' . hexdec($m[1]) . ';', ENT_NOQUOTES, 'utf-8')); }; } if ($header) { $s = strtr($s, self::$hvaltr); } if ( (stripos($s, '%u') !== false) && preg_match('~(%u[af\d]{4}|%[cf][af\d](?!%[89a-f][af\d]))~is', $s, $m) ) { $s = preg_replace_callback('~%(u[af\d]{4}|[af\d]{2})~i', $cb, $s); } parse_str($s, $var); } /** * Send data to the connection. Note that it just writes to buffer that flushes at every baseloop * @param string $data Data to send * @return boolean Success */ public function write($data) { if ($this->closed) return false; return stream_socket_sendto($this->socket, $data) == 0; } /** *         * @return bool */ protected function send_handshake_reply() { return false; } /** * Called when we're going to handshake. * @return boolean Handshake status */ public function handshake() { $extra_headers = ''; foreach ($this->headers as $k => $line) { if ($k !== 'STATUS') { $extra_headers .= $line . "\r\n"; } } if (!$this->send_handshake_reply($extra_headers)) { error_log(get_class($this) . '::' . __METHOD__ . ' : Handshake protocol failure for client ""'); // $this->addr $this->close(); return false; } $this->handshaked = true; $this->headers_sent = true; $this->state = static::STATE_HANDSHAKED; return true; } /** * Read from buffer without draining * @param integer $n Number of bytes to read * @param integer $o Offset * @return string|false */ public function look($n, $o = 0) { if (strlen($this->unparsed_data) <= $o) { return ''; } return substr($this->unparsed_data, $o, $n); } /** * Convert bytes into integer * @param string $str Bytes * @param boolean $l Little endian? Default is false * @return integer */ public static function bytes2int($str, $l = false) { if ($l) { $str = strrev($str); } $dec = 0; $len = strlen($str); for ($i = 0; $i < $len; ++$i) { $dec += ord(substr($str, $i, 1)) * pow(0x100, $len - $i - 1); } return $dec; } /** * Drains buffer * @param integer $n Numbers of bytes to drain * @return boolean Success */ public function drain($n) { $ret = substr($this->unparsed_data, 0, $n); $this->unparsed_data = substr($this->unparsed_data, $n); return $ret; } /** * Read data from the connection's buffer * @param integer $n Max. number of bytes to read * @return string|false Readed data */ public function read($n) { if ($n <= 0) { return ''; } $read = $this->drain($n); if ($read === '') { return false; } return $read; } /** * Reads all data from the connection's buffer * @return string Readed data */ public function read_unlimited() { $ret = $this->unparsed_data; $this->unparsed_data = ''; return $ret; } /** * Searches first occurence of the string in input buffer * @param string $what Needle * @param integer $start Offset start * @param integer $end Offset end * @return integer Position */ public function search($what, $start = 0, $end = -1) { return strpos($this->unparsed_data, $what, $start); } /** * Called when new frame received. * @param string $data Frame's data. * @param string $type Frame's type ("STRING" OR "BINARY"). * @return boolean Success. */ public function on_frame($data, $type) { if (is_callable($this->on_frame_user)) { call_user_func($this->on_frame_user, $this, $data, $type); } return true; } public function send_frame($data, $type = null, $cb = null) { return false; } /** * Get real frame type identificator * @param $type * @return integer */ public function get_frame_type($type) { if (is_int($type)) { return $type; } if ($type === null) { $type = 'STRING'; } $frametype = @constant(get_class($this) . '::' . $type); if ($frametype === null) { error_log(__METHOD__ . ' : Undefined frametype "' . $type . '"'); } return $frametype; } }
      
      







実際には、「Web゜ケットレベルでクラむアントに接続する」、「クラむアントからメッセヌゞを受信する」、「クラむアントにメッセヌゞを送信する」ずいう3぀のこずが远加されたす。



たず、いく぀かの理論ず甚語。 「ハンドシェむク」は、Web゜ケットの芳点から、httpを介した接続を確立するための手順です。 結局のずころ、䞀連の質問を解決する必芁がありたす。プロキシずキャッシュの厚い郚分を突砎する方法、邪悪なハッカヌから身を守る方法です。 「フレヌム」ずいう甚語は、埩号化された圢匏のデヌタです。これは、クラむアントからのメッセヌゞたたはクラむアントぞのメッセヌゞです。 おそらく蚘事の冒頭でこれに぀いお曞く䟡倀はありたしたが、これらの「フレヌム」のために、゜ケットサヌバヌをブロッキング゜ケットモヌドにするこずは意味がありたせん。 この瞬間がここで行われた方法 -それは私が䞀晩以䞊睡眠を奪った。 その蚘事では、オプションはフレヌムが完党に到着しなかった、たたは2぀が䞀床に到着したずは芋なされたせん。 ちなみに、ゲヌムログが瀺したように、これずそれは非垞に兞型的な状況です。



さお詳现に。



Web゜ケットレベルでのクラむアントぞの接続 -プロトコルws_v0などがon_readメ゜ッドをブロックし、十分なデヌタがあるずきに内郚でハンドシェむクをプルするず想定されたす。次は、芪の「握手」です。次に、send_handshake_replyメ゜ッドが䜜動したす。これはプロトコルに実装する必芁がありたす。この「send_handshake_reply」は、「接続が確立されたこず」、通垞のブラりザ-通垞の回答、およびSafari-特別な回答を理解するような方法でクラむアントに応答する必芁がありたす。



クラむアントからメッセヌゞを受信する。愚かなクラむアントは、接続が確立されず、ナヌザヌからのメッセヌゞがすでに到着しおいるようなオプションを実装できるこずに泚意しおください。したがっお、「unparsed_data」倉数を慎重に扱う必芁がありたす。各プロトコルで、on_readメ゜ッドは送信されたフレヌムのサむズを理解し、フレヌム党䜓が到着したこずを確認し、ナヌザヌのメッセヌゞで到着したフレヌムを埩号化する必芁がありたす。各プロトコルで、これは非垞に異なっお非垞にカヌリヌに行われたすフレヌムが完党に到着したかどうかはわかりたせんが、次のフレヌムのバむトを噛むこずはできたせん。さらに「on_read」の内郚で、クラむアントデヌタが受信および埩号化され、そのタむプが決定されるずはい、そのように提䟛されたす、「on_frame」メ゜ッドをプルしたす。 my_callback、メむンルヌプの前その結果、$ my_callbackはクラむアントからメッセヌゞを受け取りたす。



クラむアントにメッセヌゞを送信したす。「send_frame」メ゜ッドは、プロトコル内に実装する必芁があるだけで、ひき぀りたす。ここでは、メッセヌゞを暗号化しおナヌザヌに送信するだけです。異なるプロトコルは異なる方法で暗号化したす。



珟圚、3぀のプロトコル「v13」、「v0」、「ve」を添付しおいたす。



ファむル<ws_v13.php>
 class ws_v13 extends ws { const CONTINUATION = 0; const STRING = 0x1; const BINARY = 0x2; const CONNCLOSE = 0x8; const PING = 0x9; const PONG = 0xA; protected static $opcodes = [ 0 => 'CONTINUATION', 0x1 => 'STRING', 0x2 => 'BINARY', 0x8 => 'CONNCLOSE', 0x9 => 'PING', 0xA => 'PONG', ]; protected $outgoingCompression = 0; protected $framebuf = ''; /** * Apply mask * @param $data * @param string|false $mask * @return mixed */ public function mask($data, $mask) { for ($i = 0, $l = strlen($data), $ml = strlen($mask); $i < $l; $i++) { $data[$i] = $data[$i] ^ $mask[$i % $ml]; } return $data; } /** * Sends a frame. * @param string $data Frame's data. * @param string $type Frame's type. ("STRING" OR "BINARY") * @param callable $cb Optional. Callback called when the frame is received by client. * @callback $cb ( ) * @return boolean Success. */ public function send_frame($data, $type = null, $cb = null) { if (!$this->handshaked) { return false; } if ($this->closed && $type !== 'CONNCLOSE') { return false; } /*if (in_array($type, ['STRING', 'BINARY']) && ($this->outgoingCompression > 0) && in_array('deflate-frame', $this->extensions)) { //$data = gzcompress($data, $this->outgoingCompression); //$rsv1 = 1; }*/ $fin = 1; $rsv1 = 0; $rsv2 = 0; $rsv3 = 0; $this->write(chr(bindec($fin . $rsv1 . $rsv2 . $rsv3 . str_pad(decbin($this->get_frame_type($type)), 4, '0', STR_PAD_LEFT)))); $dataLength = strlen($data); $isMasked = false; $isMaskedInt = $isMasked ? 128 : 0; if ($dataLength <= 125) { $this->write(chr($dataLength + $isMaskedInt)); } elseif ($dataLength <= 65535) { $this->write(chr(126 + $isMaskedInt) . // 126 + 128 chr($dataLength >> 8) . chr($dataLength & 0xFF)); } else { $this->write(chr(127 + $isMaskedInt) . // 127 + 128 chr($dataLength >> 56) . chr($dataLength >> 48) . chr($dataLength >> 40) . chr($dataLength >> 32) . chr($dataLength >> 24) . chr($dataLength >> 16) . chr($dataLength >> 8) . chr($dataLength & 0xFF)); } if ($isMasked) { $mask = chr(mt_rand(0, 0xFF)) . chr(mt_rand(0, 0xFF)) . chr(mt_rand(0, 0xFF)) . chr(mt_rand(0, 0xFF)); $this->write($mask . $this->mask($data, $mask)); } else { $this->write($data); } if ($cb !== null) { $cb(); } return true; } /** * Sends a handshake message reply * @param string Received data (no use in this class) * @return boolean OK? */ public function send_handshake_reply($extraHeaders = '') { if (!isset($this->server['HTTP_SEC_WEBSOCKET_KEY']) || !isset($this->server['HTTP_SEC_WEBSOCKET_VERSION'])) { return false; } if ($this->server['HTTP_SEC_WEBSOCKET_VERSION'] !== '13' && $this->server['HTTP_SEC_WEBSOCKET_VERSION'] !== '8') { return false; } if (isset($this->server['HTTP_ORIGIN'])) { $this->server['HTTP_SEC_WEBSOCKET_ORIGIN'] = $this->server['HTTP_ORIGIN']; } if (!isset($this->server['HTTP_SEC_WEBSOCKET_ORIGIN'])) { $this->server['HTTP_SEC_WEBSOCKET_ORIGIN'] = ''; } $this->write("HTTP/1.1 101 Switching Protocols\r\n" . "Upgrade: WebSocket\r\n" . "Connection: Upgrade\r\n" . "Date: " . date('r') . "\r\n" . "Sec-WebSocket-Origin: " . $this->server['HTTP_SEC_WEBSOCKET_ORIGIN'] . "\r\n" . "Sec-WebSocket-Location: ws://" . $this->server['HTTP_HOST'] . $this->server['REQUEST_URI'] . "\r\n" . "Sec-WebSocket-Accept: " . base64_encode(sha1(trim($this->server['HTTP_SEC_WEBSOCKET_KEY']) . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true)) . "\r\n" ); if (isset($this->server['HTTP_SEC_WEBSOCKET_PROTOCOL'])) { $this->write("Sec-WebSocket-Protocol: " . $this->server['HTTP_SEC_WEBSOCKET_PROTOCOL']."\r\n"); } $this->write($extraHeaders."\r\n"); return true; } /** * Called when new data received * @see http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10#page-16 * @return void */ public function on_read() { if ($this->closed) return; if ($this->state === self::STATE_PREHANDSHAKE) { if (!$this->handshake()) { return; } } if ($this->state === self::STATE_HANDSHAKED) { while (($buflen = strlen($this->unparsed_data)) >= 2) { $first = ord($this->look(1)); // first byte integer (fin, opcode) $firstBits = decbin($first); $opcode = (int)bindec(substr($firstBits, 4, 4)); if ($opcode === 0x8) { // CLOSE $this->close(); return; } $opcodeName = isset(static::$opcodes[$opcode]) ? static::$opcodes[$opcode] : false; if (!$opcodeName) { error_log(get_class($this) . ': Undefined opcode ' . $opcode); $this->close(); return; } $second = ord($this->look(1, 1)); // second byte integer (masked, payload length) $fin = (bool)($first >> 7); $isMasked = (bool)($second >> 7); $dataLength = $second & 0x7f; $p = 2; if ($dataLength === 0x7e) { // 2 bytes-length if ($buflen < $p + 2) { return; // not enough data yet } $dataLength = self::bytes2int($this->look(2, $p), false); $p += 2; } elseif ($dataLength === 0x7f) { // 8 bytes-length if ($buflen < $p + 8) { return; // not enough data yet } $dataLength = self::bytes2int($this->look(8, $p)); $p += 8; } if (self::maxAllowedPacket <= $dataLength) { // Too big packet $this->close(); return; } if ($isMasked) { if ($buflen < $p + 4) { return; // not enough data yet } $mask = $this->look(4, $p); $p += 4; } if ($buflen < $p + $dataLength) { return; // not enough data yet } $this->drain($p); $data = $this->read($dataLength); if ($isMasked) { $data = $this->mask($data, $mask); } //Daemon::log(Debug::dump(array('ext' => $this->extensions, 'rsv1' => $firstBits[1], 'data' => Debug::exportBytes($data)))); /*if ($firstBits[1] && in_array('deflate-frame', $this->extensions)) { // deflate frame $data = gzuncompress($data, $this->pool->maxAllowedPacket); }*/ if (!$fin) { $this->framebuf .= $data; } else { $this->on_frame($this->framebuf . $data, $opcodeName); $this->framebuf = ''; } } } } }
      
      







ファむル<ws_v0.php>
 class ws_v0 extends ws { const STRING = 0x00; const BINARY = 0x80; protected $key; /** * Sends a handshake message reply * @param string Received data (no use in this class) * @return boolean OK? */ public function send_handshake_reply($extraHeaders = '') { if (!isset($this->server['HTTP_SEC_WEBSOCKET_KEY1']) || !isset($this->server['HTTP_SEC_WEBSOCKET_KEY2'])) { return false; } $final_key = $this->_computeFinalKey($this->server['HTTP_SEC_WEBSOCKET_KEY1'], $this->server['HTTP_SEC_WEBSOCKET_KEY2'], $this->key); $this->key = null; if (!$final_key) { return false; } if (!isset($this->server['HTTP_SEC_WEBSOCKET_ORIGIN'])) { $this->server['HTTP_SEC_WEBSOCKET_ORIGIN'] = ''; } $this->write("HTTP/1.1 101 Web Socket Protocol Handshake\r\n" . "Upgrade: WebSocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Origin: " . $this->server['HTTP_ORIGIN'] . "\r\n" . "Sec-WebSocket-Location: ws://" . $this->server['HTTP_HOST'] . $this->server['REQUEST_URI'] . "\r\n"); if (isset($this->server['HTTP_SEC_WEBSOCKET_PROTOCOL'])) { $this->write("Sec-WebSocket-Protocol: " . $this->server['HTTP_SEC_WEBSOCKET_PROTOCOL']."\r\n"); } $this->write($extraHeaders . "\r\n" . $final_key); return true; } /** * Computes final key for Sec-WebSocket. * @param string Key1 * @param string Key2 * @param string Data * @return string Result */ protected function _computeFinalKey($key1, $key2, $data) { if (strlen($data) < 8) { error_log(get_class($this) . '::' . __METHOD__ . ' : Invalid handshake data for client ""'); // $this->addr return false; } return md5($this->_computeKey($key1) . $this->_computeKey($key2) . substr($data, 0, 8), true); } /** * Computes key for Sec-WebSocket. * @param string Key * @return string Result */ protected function _computeKey($key) { $spaces = 0; $digits = ''; for ($i = 0, $s = strlen($key); $i < $s; ++$i) { $c = substr($key, $i, 1); if ($c === "\x20") { ++$spaces; } elseif (ctype_digit($c)) { $digits .= $c; } } if ($spaces > 0) { $result = (float)floor($digits / $spaces); } else { $result = (float)$digits; } return pack('N', $result); } /** * Sends a frame. * @param string $data Frame's data. * @param string $type Frame's type. ("STRING" OR "BINARY") * @param callable $cb Optional. Callback called when the frame is received by client. * @callback $cb ( ) * @return boolean Success. */ public function send_frame($data, $type = null, $cb = null) { if (!$this->handshaked) { return false; } if ($this->closed && $type !== 'CONNCLOSE') { return false; } if ($type === 'CONNCLOSE') { if ($cb !== null) { $cb($this); return true; } } $type = $this->get_frame_type($type); // Binary if (($type & self::BINARY) === self::BINARY) { $n = strlen($data); $len = ''; $pos = 0; char: ++$pos; $c = $n >> 0 & 0x7F; $n >>= 7; if ($pos !== 1) { $c += 0x80; } if ($c !== 0x80) { $len = chr($c) . $len; goto char; }; $this->write(chr(self::BINARY) . $len . $data); } // String else { $this->write(chr(self::STRING) . $data . "\xFF"); } if ($cb !== null) { $cb(); } return true; } /** * Called when new data received * @return void */ public function on_read() { if ($this->state === self::STATE_PREHANDSHAKE) { if (strlen($this->unparsed_data) < 8) { return; } $this->key = $this->read_unlimited(); $this->handshake(); } if ($this->state === self::STATE_HANDSHAKED) { while (($buflen = strlen($this->unparsed_data)) >= 2) { $hdr = $this->look(10); $frametype = ord(substr($hdr, 0, 1)); if (($frametype & 0x80) === 0x80) { $len = 0; $i = 0; do { if ($buflen < $i + 1) { // not enough data yet return; } $b = ord(substr($hdr, ++$i, 1)); $n = $b & 0x7F; $len *= 0x80; $len += $n; } while ($b > 0x80); if (self::maxAllowedPacket <= $len) { // Too big packet $this->close(); return; } if ($buflen < $len + $i + 1) { // not enough data yet return; } $this->drain($i + 1); $this->on_frame($this->read($len), 'BINARY'); } else { if (($p = $this->search("\xFF")) !== false) { if (self::maxAllowedPacket <= $p - 1) { // Too big packet $this->close(); return; } $this->drain(1); $data = $this->read($p); $this->drain(1); $this->on_frame($data, 'STRING'); } else { if (self::maxAllowedPacket < $buflen - 1) { // Too big packet $this->close(); return; } // not enough data yet return; } } } } } }
      
      







ファむル<ws_ve.php>
 class ws_ve extends ws { const STRING = 0x00; const BINARY = 0x80; /** * Sends a handshake message reply * @param string Received data (no use in this class) * @return boolean OK? */ public function send_handshake_reply($extraHeaders = '') { if (!isset($this->server['HTTP_SEC_WEBSOCKET_ORIGIN'])) { $this->server['HTTP_SEC_WEBSOCKET_ORIGIN'] = ''; } $this->write("HTTP/1.1 101 Web Socket Protocol Handshake\r\n" . "Upgrade: WebSocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Origin: " . $this->server['HTTP_ORIGIN'] . "\r\n" . "Sec-WebSocket-Location: ws://" . $this->server['HTTP_HOST'] . $this->server['REQUEST_URI'] . "\r\n" ); if (isset($this->server['HTTP_SEC_WEBSOCKET_PROTOCOL'])) { $this->write("Sec-WebSocket-Protocol: " . $this->server['HTTP_SEC_WEBSOCKET_PROTOCOL']."\r\n"); } $this->write($extraHeaders."\r\n"); return true; } /** * Computes key for Sec-WebSocket. * @param string Key * @return string Result */ protected function _computeKey($key) { $spaces = 0; $digits = ''; for ($i = 0, $s = strlen($key); $i < $s; ++$i) { $c = substr($key, $i, 1); if ($c === "\x20") { ++$spaces; } elseif (ctype_digit($c)) { $digits .= $c; } } if ($spaces > 0) { $result = (float)floor($digits / $spaces); } else { $result = (float)$digits; } return pack('N', $result); } /** * Sends a frame. * @param string $data Frame's data. * @param string $type Frame's type. ("STRING" OR "BINARY") * @param callable $cb Optional. Callback called when the frame is received by client. * @callback $cb ( ) * @return boolean Success. */ public function send_frame($data, $type = null, $cb = null) { if (!$this->handshaked) { return false; } if ($this->closed && $type !== 'CONNCLOSE') { return false; } if ($type === 'CONNCLOSE') { if ($cb !== null) { $cb($this); return true; } } // Binary $type = $this->get_frame_type($type); if (($type & self::BINARY) === self::BINARY) { $n = strlen($data); $len = ''; $pos = 0; char: ++$pos; $c = $n >> 0 & 0x7F; $n >>= 7; if ($pos !== 1) { $c += 0x80; } if ($c !== 0x80) { $len = chr($c) . $len; goto char; }; $this->write(chr(self::BINARY) . $len . $data); } // String else { $this->write(chr(self::STRING) . $data . "\xFF"); } if ($cb !== null) { $cb(); } return true; } /** * Called when new data received * @return void */ public function on_read() { while (($buflen = strlen($this->unparsed_data)) >= 2) { $hdr = $this->look(10); $frametype = ord(substr($hdr, 0, 1)); if (($frametype & 0x80) === 0x80) { $len = 0; $i = 0; do { if ($buflen < $i + 1) { return; } $b = ord(substr($hdr, ++$i, 1)); $n = $b & 0x7F; $len *= 0x80; $len += $n; } while ($b > 0x80); if (self::maxAllowedPacket <= $len) { // Too big packet $this->close(); return; } if ($buflen < $len + $i + 1) { // not enough data yet return; } $this->drain($i + 1); $this->on_frame($this->read($len), $frametype); } else { if (($p = $this->search("\xFF")) !== false) { if (self::maxAllowedPacket <= $p - 1) { // Too big packet $this->close(); return; } $this->drain(1); $data = $this->read($p); $this->drain(1); $this->on_frame($data, 'STRING'); } else { if (self::maxAllowedPacket < $buflen - 1) { // Too big packet $this->close(); return; } } } } } }
      
      







VEプロトコルはただテストされおいないこずに泚意しおください-誰がそれを䜿甚するのかわかりたせん。しかし、PhpDeamonからコヌドを忠実に倉換しおカットしたした。



V13プロトコルは、すべおの通垞のブラりザヌFireFox、Opera、Chrome、Yandexで䜿甚されたす。IEでも䜿甚したすIE6以降、申し蚳ありたせんが、IEが「ブラりザ」になるこずはありたせん。IE開発チヌムでさえ、「ブラりザではなくシンクラむアント」ず述べおいたす。V0プロトコルはSafariブラりザヌを䜿甚したす。



結論の代わりに



泚意しおください、健康のために䞊蚘のすべおのコヌドを䜿甚しおくださいもちろん、通垞のオブゞェクトにラップするこずをお勧めしたす。すべおが理解のために単玔化されおいたす。このコヌドを䜿甚する堎合は、「Thanks Anlide and PhpDeamon」ずいうコヌドのどこかに曞いおください。その結果、ここに瀺されおいる゜ケットサヌバヌは、すべおの最新のブラりザヌず互換性がありたす。メモリリヌクなしで動䜜し、負荷の高いシステムでの䜿甚に適しおいたす。



曎新




All Articles