ãã®ãããå®çšŒåç°å¢ã§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ããšããã³ãŒãã®ã©ããã«æžããŠãã ããããã®çµæãããã«ç€ºãããŠãããœã±ãããµãŒããŒã¯ããã¹ãŠã®ææ°ã®ãã©ãŠã¶ãŒãšäºææ§ããããŸããã¡ã¢ãªãªãŒã¯ãªãã§åäœããè² è·ã®é«ãã·ã¹ãã ã§ã®äœ¿çšã«é©ããŠããŸãã
æŽæ°ïŒ
- èšäºã®èè ã®ã³ã¡ã³ããç§ã¯ããã¹ãã§åžžã«èšåããŠããŸãïŒhabrahabr.ru/post/301822/#comment_9634636
- read_lintïŒïŒã¡ãœããã«ã¯ãšã©ãŒãå«ãŸããŠããŸããããã¯ãããããŒã®ã¿ãèªã¿åãå¿ èŠãããã«ãããããããhttpãªã¯ãšã¹ãæ¬æã®æ¬æãèªã¿åã£ãŠãããšããããšã§ãã
- â .
- gitbub github.com/anlide/websocket ping-pong , select - â websocket.