├── phpsocketio ├── Debug.php ├── autoload.php ├── Parser │ ├── Parser.php │ ├── Encoder.php │ └── Decoder.php ├── Engine │ ├── Protocols │ │ ├── Http │ │ │ ├── Request.php │ │ │ └── Response.php │ │ ├── WebSocket.php │ │ ├── SocketIO.php │ │ └── WebSocket │ │ │ └── RFC6455.php │ ├── Transports │ │ ├── WebSocket.php │ │ ├── PollingJsonp.php │ │ ├── PollingXHR.php │ │ └── Polling.php │ ├── Transport.php │ ├── Parser.php │ ├── Engine.php │ └── Socket.php ├── Swoole │ ├── WorkermanApi.php │ └── TcpConnection.php ├── DefaultAdapter.php ├── Event │ └── Emitter.php ├── ChannelAdapter.php ├── Nsp.php ├── SocketIO.php ├── Client.php └── Socket.php ├── README.md ├── composer.json ├── test ├── echotest.php └── main.go └── demo └── SocketIoServer.php /phpsocketio/Debug.php: -------------------------------------------------------------------------------- 1 | =5.4" 17 | }, 18 | "require-dev": { 19 | "php": ">=5.4" 20 | }, 21 | "autoload": { 22 | "psr-4": {"SWPHPSocketIO\\": "./phpsocketio"} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/echotest.php: -------------------------------------------------------------------------------- 1 | set(array( 7 | 'worker_num' => 1, //worker process num 8 | 'dispatch_mode'=>4, 9 | )); 10 | $io = new SocketIO(); 11 | $io->swbind($worker); 12 | $io->on('connection', function($socket){ 13 | $socket->on('message', function ($data)use($socket){ 14 | $socket->send($data); 15 | }); 16 | // when the user disconnects.. perform this 17 | $socket->on('disconnect', function () use($socket) { 18 | 19 | }); 20 | $socket->send("hello"); 21 | }); 22 | $worker->start(); 23 | -------------------------------------------------------------------------------- /demo/SocketIoServer.php: -------------------------------------------------------------------------------- 1 | set(array( 7 | 'worker_num' => 6, //worker process num 8 | 'dispatch_mode'=>4, //这个重点,模式必须是4,无效 9 | )); 10 | $io = new SocketIO(); 11 | $io->swbind($worker);//绑定swoole 12 | $io->on('connection', function($socket){ 13 | $socket->on('message', function ($data)use($socket){ 14 | 15 | echo $data; 16 | $socket->send($data); 17 | }); 18 | // when the user disconnects.. perform this 19 | $socket->on('disconnect', function () use($socket) { 20 | 21 | }); 22 | $socket->send("sdfsd"); 23 | }); 24 | 25 | $worker->start(); -------------------------------------------------------------------------------- /phpsocketio/Parser/Parser.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 25 | $this->parseHead($raw_head); 26 | } 27 | 28 | public function parseHead($raw_head) 29 | { 30 | $header_data = explode("\r\n", $raw_head); 31 | list($this->method, $this->url, $protocol) = explode(' ', $header_data[0]); 32 | list($null, $this->httpVersion) = explode('/', $protocol); 33 | unset($header_data[0]); 34 | foreach($header_data as $content) 35 | { 36 | if(empty($content)) 37 | { 38 | continue; 39 | } 40 | $this->rawHeaders[] = $content; 41 | list($key, $value) = explode(':', $content, 2); 42 | $this->headers[strtolower($key)] = trim($value); 43 | } 44 | } 45 | 46 | public function destroy() 47 | { 48 | $this->onData = $this->onEnd = $this->onClose = null; 49 | $this->connection = null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | socket "github.com/webrtcn/go-socketio-client" 6 | "time" 7 | "os" 8 | "log" 9 | ) 10 | 11 | var ConnNum=0; 12 | var FailNum=0; 13 | var TestNum=8000; 14 | var Url="http://127.0.0.1:2020"; 15 | 16 | func main() { 17 | 18 | if len(os.Args)>1 { 19 | Url=os.Args[1]; 20 | } 21 | log.Println("Url:"+Url+"\r\n"); 22 | for j := 1; j <= TestNum; j++ { 23 | go client(); 24 | } 25 | go echo(); 26 | select {} 27 | } 28 | 29 | func echo(){ 30 | 31 | for { 32 | fmt.Printf("TestNum:%dConnNum:%dFailNum:%d\r\n",TestNum,ConnNum,FailNum) 33 | time.Sleep(time.Second*1) 34 | } 35 | 36 | } 37 | 38 | func client(){ 39 | options := &socket.SocketOption{ 40 | ReconnectionDelay: 3, 41 | ReconnectionAttempts: 10, 42 | } 43 | s, err := socket.Connect(Url, options) 44 | if err != nil { 45 | return 46 | } 47 | s.On(socket.OnConnection, func() { 48 | // fmt.Println("Connect to server successful.") 49 | //fmt.Println(s.GetSessionID()) //sid 50 | ConnNum++; 51 | }) 52 | s.On(socket.OnMessage, func(msg string) { //listen with ack message 53 | //fmt.Println(msg); 54 | s.Emit("message", msg) 55 | }) 56 | s.On(socket.OnConnecting, func() { 57 | //fmt.Println("connecting to server") 58 | }) 59 | s.On(socket.OnReconnectFailed, func() { 60 | fmt.Println("connect to server failed") 61 | FailNum++; 62 | }) 63 | s.On(socket.OnDisConnection, func() { 64 | //fmt.Println("server disconnect.") 65 | ConnNum--; 66 | }) 67 | if err != nil { 68 | fmt.Println(err) 69 | } 70 | } -------------------------------------------------------------------------------- /phpsocketio/Swoole/WorkermanApi.php: -------------------------------------------------------------------------------- 1 | Connections[$fd]=new TcpConnection($serv,$fd); 10 | $serv->io->engine->onConnect($serv->Connections[$fd]); 11 | } 12 | static public function WorkerMessage($serv,$fd,$data){ 13 | //此处模拟workerman的自定义协议回调 input decode 14 | if(isset($serv->Connections[$fd])){ 15 | $Connection= $serv->Connections[$fd]; 16 | $Connection->_recvBuffer= $Connection->_recvBuffer.$data; 17 | if($Connection->protocol){ 18 | $len=call_user_func_array(array($Connection->protocol,'input'),array($Connection->_recvBuffer,$Connection)); 19 | if($len>0&&strlen($Connection->_recvBuffer)>=$len){ 20 | $data=substr($Connection->_recvBuffer,0,$len); 21 | $Connection->consumeRecvBuffer($len); 22 | $data=call_user_func_array(array($Connection->protocol,'decode'),array($data,$Connection)); 23 | if($Connection->onMessage){ 24 | call_user_func_array($Connection->onMessage,array($Connection,$data)); 25 | } 26 | } 27 | } 28 | } 29 | } 30 | static public function Free($serv,$fd){ 31 | if(isset($serv->Connections[$fd])){ 32 | $Connection= $serv->Connections[$fd]; 33 | if($Connection->onClose){ 34 | call_user_func($Connection->onClose,$Connection); 35 | } 36 | if($serv->Connections[$fd]->_recvBuffer){ 37 | unset($serv->Connections[$fd]->_recvBuffer); 38 | } 39 | unset($serv->Connections[$fd]); 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /phpsocketio/Engine/Transports/WebSocket.php: -------------------------------------------------------------------------------- 1 | socket = $req->connection; 15 | $this->socket->onMessage = array($this, 'onData2'); 16 | $this->socket->onClose = array($this, 'onClose'); 17 | $this->socket->onError = array($this, 'onError2'); 18 | Debug::debug('WebSocket __construct'); 19 | } 20 | public function __destruct() 21 | { 22 | Debug::debug('WebSocket __destruct'); 23 | } 24 | public function onData2($connection, $data) 25 | { 26 | call_user_func(array($this, 'parent::onData'), $data); 27 | } 28 | 29 | public function onError2($conection, $code, $msg) 30 | { 31 | call_user_func(array($this, 'parent::onClose'), $code, $msg); 32 | } 33 | 34 | public function send($packets) 35 | { 36 | foreach($packets as $packet) 37 | { 38 | $data = Parser::encodePacket($packet, $this->supportsBinary); 39 | $this->socket->send($data); 40 | $this->emit('drain'); 41 | } 42 | } 43 | 44 | public function doClose($fn = null) 45 | { 46 | if($this->socket) 47 | { 48 | $this->socket->close(); 49 | $this->socket = null; 50 | if(!empty($fn)) 51 | { 52 | call_user_func($fn); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /phpsocketio/Engine/Transport.php: -------------------------------------------------------------------------------- 1 | req = $req; 29 | } 30 | 31 | public function close($fn = null) 32 | { 33 | $this->readyState = 'closing'; 34 | $fn = $fn ? $fn : array($this, 'noop'); 35 | $this->doClose($fn); 36 | } 37 | 38 | public function onError($msg, $desc = '') 39 | { 40 | if ($this->listeners('error')) 41 | { 42 | $err = array( 43 | 'type' => 'TransportError', 44 | 'description' => $desc, 45 | ); 46 | $this->emit('error', $err); 47 | } 48 | else 49 | { 50 | echo("ignored transport error $msg $desc\n"); 51 | } 52 | } 53 | 54 | public function onPacket($packet) 55 | { 56 | $this->emit('packet', $packet); 57 | } 58 | 59 | public function onData($data) 60 | { 61 | $this->onPacket(Parser::decodePacket($data)); 62 | } 63 | 64 | public function onClose() 65 | { 66 | $this->req = $this->res = null; 67 | $this->readyState = 'closed'; 68 | $this->emit('close'); 69 | $this->removeAllListeners(); 70 | } 71 | 72 | public function destroy() 73 | { 74 | $this->req = $this->res = null; 75 | $this->readyState = 'closed'; 76 | $this->removeAllListeners(); 77 | $this->shouldClose = null; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /phpsocketio/Parser/Encoder.php: -------------------------------------------------------------------------------- 1 | _query['j']) ? preg_replace('/[^0-9]/', '', $req->_query['j']) : ''; 12 | $this->head = "___eio[ $j ]("; 13 | Debug::debug('PollingJsonp __construct'); 14 | } 15 | public function __destruct() 16 | { 17 | Debug::debug('PollingJsonp __destruct'); 18 | } 19 | public function onData($data) 20 | { 21 | $parsed_data = null; 22 | parse_str($data, $parsed_data); 23 | $data = $parsed_data['d']; 24 | // todo check 25 | //client will send already escaped newlines as \\\\n and newlines as \\n 26 | // \\n must be replaced with \n and \\\\n with \\n 27 | /*data = data.replace(rSlashes, function(match, slashes) { 28 | return slashes ? match : '\n'; 29 | });*/ 30 | call_user_func(array($this, 'parent::onData'), preg_replace('/\\\\n/', '\\n', $data)); 31 | } 32 | 33 | public function doWrite($data) 34 | { 35 | $js = json_encode($data); 36 | //$js = preg_replace(array('/\u2028/', '/\u2029/'), array('\\u2028', '\\u2029'), $js); 37 | 38 | // prepare response 39 | $data = $this->head . $js . $this->foot; 40 | 41 | // explicit UTF-8 is required for pages not served under utf 42 | $headers = array( 43 | 'Content-Type'=> 'text/javascript; charset=UTF-8', 44 | 'Content-Length'=> strlen($data), 45 | 'X-XSS-Protection'=>'0' 46 | ); 47 | if(empty($this->res)){echo new \Exception('empty $this->res');return;} 48 | $this->res->writeHead(200, '',$this->headers($this->req, $headers)); 49 | $this->res->end($data); 50 | } 51 | 52 | public function headers($req, $headers = array()) 53 | { 54 | $listeners = $this->listeners('headers'); 55 | foreach($listeners as $listener) 56 | { 57 | $listener($headers); 58 | } 59 | return $headers; 60 | } 61 | 62 | 63 | } 64 | -------------------------------------------------------------------------------- /phpsocketio/Engine/Transports/PollingXHR.php: -------------------------------------------------------------------------------- 1 | method) 19 | { 20 | $res = $req->res; 21 | $headers = $this->headers($req); 22 | $headers['Access-Control-Allow-Headers'] = 'Content-Type'; 23 | $res->writeHead(200, '', $headers); 24 | $res->end(); 25 | } 26 | else 27 | { 28 | parent::onRequest($req); 29 | } 30 | } 31 | 32 | public function doWrite($data) 33 | { 34 | // explicit UTF-8 is required for pages not served under utf todo 35 | //$content_type = $isString 36 | // ? 'text/plain; charset=UTF-8' 37 | // : 'application/octet-stream'; 38 | $content_type = preg_match('/^\d+:/', $data) ? 'text/plain; charset=UTF-8' : 'application/octet-stream'; 39 | $content_length = strlen($data); 40 | $headers = array( 41 | 'Content-Type'=> $content_type, 42 | 'Content-Length'=> $content_length, 43 | 'X-XSS-Protection' => '0', 44 | ); 45 | if(empty($this->res)){echo new \Exception('empty this->res');return;} 46 | $this->res->writeHead(200, '', $this->headers($this->req, $headers)); 47 | $this->res->end($data); 48 | } 49 | 50 | public function headers($req, $headers = array()) 51 | { 52 | if(isset($req->headers['origin'])) 53 | { 54 | $headers['Access-Control-Allow-Credentials'] = 'true'; 55 | $headers['Access-Control-Allow-Origin'] = $req->headers['origin']; 56 | } 57 | else 58 | { 59 | $headers['Access-Control-Allow-Origin'] = '*'; 60 | } 61 | $listeners = $this->listeners('headers'); 62 | foreach($listeners as $listener) 63 | { 64 | $listener($headers); 65 | } 66 | return $headers; 67 | } 68 | 69 | 70 | } 71 | -------------------------------------------------------------------------------- /phpsocketio/Swoole/TcpConnection.php: -------------------------------------------------------------------------------- 1 | serv=$serv; 24 | $this->fd=$socket; 25 | } 26 | 27 | 28 | /** 29 | * Sends data on the connection. 30 | * 31 | * @param string $send_buffer 32 | * @param bool $raw 33 | * @return void|bool|null 34 | */ 35 | public function send($send_buffer,$raw = false) 36 | { 37 | if($this->protocol&&$raw==false){ 38 | $send_buffer=call_user_func_array(array($this->protocol,'encode'),array($send_buffer,$this)); 39 | } 40 | //没有握手会打包失败,导致发送报错(所以判断一下) 41 | strlen($send_buffer)>0 && $this->serv->send($this->fd,$send_buffer); 42 | } 43 | 44 | /** 45 | * Get remote IP. 46 | * 47 | * @return string 48 | */ 49 | public function getRemoteIp() 50 | { 51 | return $this->serv->connection_info($this->fd)['remote_ip']; 52 | } 53 | 54 | /** 55 | * Get remote port. 56 | * 57 | * @return int 58 | */ 59 | public function getRemotePort() 60 | { 61 | return $this->serv->connection_info($this->fd)['remote_port']; 62 | } 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | /** 74 | * Close connection. 75 | * 76 | * @param mixed $data 77 | * @param bool $raw 78 | * @return void 79 | */ 80 | public function close() 81 | { 82 | return $this->serv->close($this->fd); 83 | } 84 | 85 | /** 86 | * Get the real socket. 87 | * 88 | * @return resource 89 | */ 90 | public function getSocket() 91 | { 92 | return $this->fd; 93 | } 94 | 95 | 96 | 97 | 98 | /** 99 | * Remove $length of data from receive buffer. 100 | * 101 | * @param int $length 102 | * @return void 103 | * SamLuo 104 | */ 105 | public function consumeRecvBuffer($length) 106 | { 107 | $this->_recvBuffer = substr($this->_recvBuffer, $length); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /phpsocketio/Engine/Protocols/WebSocket.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | namespace PHPSocketIO\Engine\Protocols; 15 | 16 | use \PHPSocketIO\Engine\Protocols\Http\Request; 17 | use \PHPSocketIO\Engine\Protocols\Http\Response; 18 | use \PHPSocketIO\Engine\Protocols\WebSocket\RFC6455; 19 | use \Workerman\Connection\TcpConnection; 20 | 21 | /** 22 | * WebSocket 协议服务端解包和打包 23 | */ 24 | class WebSocket 25 | { 26 | /** 27 | * 最小包头 28 | * @var int 29 | */ 30 | const MIN_HEAD_LEN = 7; 31 | 32 | /** 33 | * 检查包的完整性 34 | * @param string $buffer 35 | */ 36 | public static function input($buffer, $connection) 37 | { 38 | if(strlen($buffer) < self::MIN_HEAD_LEN) 39 | { 40 | return 0; 41 | } 42 | // flash policy file 43 | if(0 === strpos($buffer,'send($policy_xml, true); 47 | $connection->consumeRecvBuffer(strlen($buffer)); 48 | return 0; 49 | } 50 | // http head 51 | $pos = strpos($buffer, "\r\n\r\n"); 52 | if(!$pos) 53 | { 54 | if(strlen($buffer)>=TcpConnection::$maxPackageSize) 55 | { 56 | $connection->close("HTTP/1.1 400 bad request\r\n\r\nheader too long"); 57 | return 0; 58 | } 59 | return 0; 60 | } 61 | $req = new Request($connection, $buffer); 62 | $res = new Response($connection); 63 | $connection->consumeRecvBuffer(strlen($buffer)); 64 | return self::dealHandshake($connection, $req, $res); 65 | $connection->consumeRecvBuffer($pos+4); 66 | return 0; 67 | } 68 | 69 | /** 70 | * 处理websocket握手 71 | * @param string $buffer 72 | * @param TcpConnection $connection 73 | * @return int 74 | */ 75 | public static function dealHandshake($connection, $req, $res) 76 | { 77 | if(isset($req->headers['sec-websocket-key1'])) 78 | { 79 | $res->writeHead(400); 80 | $res->end("Not support"); 81 | return 0; 82 | } 83 | $connection->protocol = 'PHPSocketIO\Engine\Protocols\WebSocket\RFC6455'; 84 | return RFC6455::dealHandshake($connection, $req, $res); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /phpsocketio/DefaultAdapter.php: -------------------------------------------------------------------------------- 1 | nsp = $nsp; 13 | $this->encoder = new Parser\Encoder(); 14 | Debug::debug('DefaultAdapter __construct'); 15 | } 16 | public function __destruct() 17 | { 18 | Debug::debug('DefaultAdapter __destruct'); 19 | } 20 | public function add($id, $room) 21 | { 22 | $this->sids[$id][$room] = true; 23 | $this->rooms[$room][$id] = true; 24 | } 25 | 26 | public function del($id, $room) 27 | { 28 | unset($this->sids[$id][$room]); 29 | unset($this->rooms[$room][$id]); 30 | if(empty($this->rooms[$room])) 31 | { 32 | unset($this->rooms[$room]); 33 | } 34 | } 35 | 36 | public function delAll($id) 37 | { 38 | $rooms = array_keys(isset($this->sids[$id]) ? $this->sids[$id] : array()); 39 | foreach($rooms as $room) 40 | { 41 | $this->del($id, $room); 42 | } 43 | unset($this->sids[$id]); 44 | } 45 | 46 | public function broadcast($packet, $opts, $remote = false) 47 | { 48 | $rooms = isset($opts['rooms']) ? $opts['rooms'] : array(); 49 | $except = isset($opts['except']) ? $opts['except'] : array(); 50 | $flags = isset($opts['flags']) ? $opts['flags'] : array(); 51 | $packetOpts = array( 52 | 'preEncoded' => true, 53 | 'volatile' => isset($flags['volatile']) ? $flags['volatile'] : null, 54 | 'compress' => isset($flags['compress']) ? $flags['compress'] : null 55 | ); 56 | $packet['nsp'] = $this->nsp->name; 57 | $encodedPackets = $this->encoder->encode($packet); 58 | if($rooms) 59 | { 60 | $ids = array(); 61 | foreach($rooms as $i=>$room) 62 | { 63 | if(!isset($this->rooms[$room])) 64 | { 65 | continue; 66 | } 67 | 68 | $room = $this->rooms[$room]; 69 | foreach($room as $id=>$item) 70 | { 71 | if(isset($ids[$id]) || isset($except[$id])) 72 | { 73 | continue; 74 | } 75 | if(isset($this->nsp->connected[$id])) 76 | { 77 | $ids[$id] = true; 78 | $this->nsp->connected[$id]->packet($encodedPackets, $packetOpts); 79 | } 80 | } 81 | } 82 | } else { 83 | foreach($this->sids as $id=>$sid) 84 | { 85 | if(isset($except[$id])) continue; 86 | if(isset($this->nsp->connected[$id])) 87 | { 88 | $socket = $this->nsp->connected[$id]; 89 | $volatile = isset($flags['volatile']) ? $flags['volatile'] : null; 90 | $socket->packet($encodedPackets, true, $volatile); 91 | } 92 | } 93 | } 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /phpsocketio/Event/Emitter.php: -------------------------------------------------------------------------------- 1 | [[listener1, once?], [listener2,once?], ..], ..] 18 | */ 19 | protected $_eventListenerMap = array(); 20 | 21 | public function on($event_name, $listener) 22 | { 23 | $this->emit('newListener', $event_name, $listener); 24 | $this->_eventListenerMap[$event_name][] = array($listener, 0); 25 | return $this; 26 | } 27 | 28 | public function once($event_name, $listener) 29 | { 30 | $this->_eventListenerMap[$event_name][] = array($listener, 1); 31 | return $this; 32 | } 33 | 34 | public function removeListener($event_name, $listener) 35 | { 36 | if(!isset($this->_eventListenerMap[$event_name])) 37 | { 38 | return $this; 39 | } 40 | foreach($this->_eventListenerMap[$event_name] as $key=>$item) 41 | { 42 | if($item[0] === $listener) 43 | { 44 | $this->emit('removeListener', $event_name, $listener); 45 | unset($this->_eventListenerMap[$event_name][$key]); 46 | } 47 | } 48 | if(empty($this->_eventListenerMap[$event_name])) 49 | { 50 | unset($this->_eventListenerMap[$event_name]); 51 | } 52 | return $this; 53 | } 54 | 55 | public function removeAllListeners($event_name = null) 56 | { 57 | $this->emit('removeListener', $event_name); 58 | if(null === $event_name) 59 | { 60 | $this->_eventListenerMap = array(); 61 | return $this; 62 | } 63 | unset($this->_eventListenerMap[$event_name]); 64 | return $this; 65 | } 66 | 67 | public function listeners($event_name) 68 | { 69 | if(empty($this->_eventListenerMap[$event_name])) 70 | { 71 | return array(); 72 | } 73 | $listeners = array(); 74 | foreach($this->_eventListenerMap[$event_name] as $item) 75 | { 76 | $listeners[] = $item[0]; 77 | } 78 | return $listeners; 79 | } 80 | 81 | public function emit($event_name = null) 82 | { 83 | //var_dump($event_name); 84 | if(empty($event_name) || empty($this->_eventListenerMap[$event_name])) 85 | { 86 | return false; 87 | } 88 | foreach($this->_eventListenerMap[$event_name] as $key=>$item) 89 | { 90 | $args = func_get_args(); 91 | unset($args[0]); 92 | call_user_func_array($item[0], $args); 93 | // once ? 94 | if($item[1]) 95 | { 96 | unset($this->_eventListenerMap[$event_name][$key]); 97 | if(empty($this->_eventListenerMap[$event_name])) 98 | { 99 | unset($this->_eventListenerMap[$event_name]); 100 | } 101 | } 102 | } 103 | return true; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /phpsocketio/ChannelAdapter.php: -------------------------------------------------------------------------------- 1 | _channelId = (function_exists('random_int') ? random_int(1, 10000000): rand(1, 10000000)) . "-" . (function_exists('posix_getpid') ? posix_getpid(): 1); 15 | \Channel\Client::connect(self::$ip, self::$port); 16 | \Channel\Client::$onMessage = array($this, 'onChannelMessage'); 17 | \Channel\Client::subscribe("socket.io#/#"); 18 | Debug::debug('ChannelAdapter __construct'); 19 | } 20 | 21 | public function __destruct() 22 | { 23 | Debug::debug('ChannelAdapter __destruct'); 24 | } 25 | 26 | public function add($id ,$room) 27 | { 28 | $this->sids[$id][$room] = true; 29 | $this->rooms[$room][$id] = true; 30 | $channel = "socket.io#/#$room#"; 31 | \Channel\Client::subscribe($channel); 32 | } 33 | 34 | public function del($id, $room) 35 | { 36 | unset($this->sids[$id][$room]); 37 | unset($this->rooms[$room][$id]); 38 | if(empty($this->rooms[$room])) 39 | { 40 | unset($this->rooms[$room]); 41 | $channel = "socket.io#/#$room#"; 42 | \Channel\Client::unsubscribe($channel); 43 | } 44 | } 45 | 46 | public function delAll($id) 47 | { 48 | $rooms = isset($this->sids[$id]) ? $this->sids[$id] : array(); 49 | if($rooms) 50 | { 51 | foreach($rooms as $room) 52 | { 53 | if(isset($this->rooms[$room][$id])) 54 | { 55 | unset($this->rooms[$room][$id]); 56 | $channel = "socket.io#/#$room#"; 57 | \Channel\Client::unsubscribe($channel); 58 | } 59 | } 60 | } 61 | if(empty($this->rooms[$room])) 62 | { 63 | unset($this->rooms[$room]); 64 | } 65 | unset($this->sids[$id]); 66 | } 67 | 68 | public function onChannelMessage($channel, $msg) 69 | { 70 | if($this->_channelId === array_shift($msg)) 71 | { 72 | //echo "ignore same channel_id \n"; 73 | return; 74 | } 75 | 76 | $packet = $msg[0]; 77 | 78 | $opts = $msg[1]; 79 | 80 | if(!$packet) 81 | { 82 | echo "invalid channel:$channel packet \n"; 83 | return; 84 | } 85 | 86 | if(empty($packet['nsp'])) 87 | { 88 | $packet['nsp'] = '/'; 89 | } 90 | 91 | if($packet['nsp'] != $this->nsp->name) 92 | { 93 | echo "ignore different namespace {$packet['nsp']} != {$this->nsp->name}\n"; 94 | return; 95 | } 96 | 97 | $this->broadcast($packet, $opts, true); 98 | } 99 | 100 | public function broadcast($packet, $opts, $remote = false) 101 | { 102 | parent::broadcast($packet, $opts); 103 | if (!$remote) 104 | { 105 | $packet['nsp'] = '/'; 106 | 107 | if(!empty($opts['rooms'])) 108 | { 109 | foreach($opts['rooms'] as $room) 110 | { 111 | $chn = "socket.io#/#$room#"; 112 | $msg = array($this->_channelId, $packet, $opts); 113 | \Channel\Client::publish($chn, $msg); 114 | } 115 | } 116 | else 117 | { 118 | $chn = "socket.io#/#"; 119 | $msg = array($this->_channelId, $packet, $opts); 120 | \Channel\Client::publish($chn, $msg); 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /phpsocketio/Parser/Decoder.php: -------------------------------------------------------------------------------- 1 | reconstructor = new BinaryReconstructor(packet); 27 | 28 | // no attachments, labeled binary but no binary data to follow 29 | if ($this->reconstructor->reconPack->attachments === 0) 30 | { 31 | $this->emit('decoded', $packet); 32 | } 33 | } else { // non-binary full packet 34 | $this->emit('decoded', $packet); 35 | } 36 | } 37 | else if (isBuf($obj) || !empty($obj['base64'])) 38 | { // raw binary data 39 | if (!$this->reconstructor) 40 | { 41 | throw new \Exception('got binary data when not reconstructing a packet'); 42 | } else { 43 | $packet = $this->reconstructor->takeBinaryData($obj); 44 | if ($packet) 45 | { // received final buffer 46 | $this->reconstructor = null; 47 | $this->emit('decoded', $packet); 48 | } 49 | } 50 | } 51 | else { 52 | throw new \Exception('Unknown type: ' + obj); 53 | } 54 | } 55 | 56 | public function decodeString($str) 57 | { 58 | $p = array(); 59 | $i = 0; 60 | 61 | // look up type 62 | $p['type'] = $str[0]; 63 | if(!isset(Parser::$types[$p['type']])) return self::error(); 64 | 65 | // look up attachments if type binary 66 | if(Parser::BINARY_EVENT == $p['type'] || Parser::BINARY_ACK == $p['type']) 67 | { 68 | $buf = ''; 69 | while ($str[++$i] != '-') 70 | { 71 | $buf .= $str[$i]; 72 | if($i == strlen(str)) break; 73 | } 74 | if ($buf != intval($buf) || $str[$i] != '-') 75 | { 76 | throw new \Exception('Illegal attachments'); 77 | } 78 | $p['attachments'] = intval($buf); 79 | } 80 | 81 | // look up namespace (if any) 82 | if(isset($str[$i + 1]) && '/' === $str[$i + 1]) 83 | { 84 | $p['nsp'] = ''; 85 | while (++$i) 86 | { 87 | if ($i === strlen($str)) break; 88 | $c = $str[$i]; 89 | if (',' === $c) break; 90 | $p['nsp'] .= $c; 91 | } 92 | } else { 93 | $p['nsp'] = '/'; 94 | } 95 | 96 | // look up id 97 | if(isset($str[$i+1])) 98 | { 99 | $next = $str[$i+1]; 100 | if ('' !== $next && strval((int)$next) === strval($next)) 101 | { 102 | $p['id'] = ''; 103 | while (++$i) 104 | { 105 | $c = $str[$i]; 106 | if (null == $c || strval((int)$c) != strval($c)) 107 | { 108 | --$i; 109 | break; 110 | } 111 | $p['id'] .= $str[$i]; 112 | if($i == strlen($str)) break; 113 | } 114 | $p['id'] = (int)$p['id']; 115 | } 116 | } 117 | 118 | // look up json data 119 | if (isset($str[++$i])) 120 | { 121 | // todo try 122 | $p['data'] = json_decode(substr($str, $i), true); 123 | } 124 | 125 | return $p; 126 | } 127 | 128 | public static function error() 129 | { 130 | return array( 131 | 'type'=> Parser::ERROR, 132 | 'data'=> 'parser error' 133 | ); 134 | } 135 | 136 | public function destroy() 137 | { 138 | 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /phpsocketio/Nsp.php: -------------------------------------------------------------------------------- 1 | 'connect', // for symmetry with client 18 | 'connection' => 'connection', 19 | 'newListener' => 'newListener' 20 | ); 21 | 22 | //public static $flags = array('json','volatile'); 23 | 24 | public function __construct($server, $name) 25 | { 26 | $this->name = $name; 27 | $this->server = $server; 28 | $this->initAdapter(); 29 | Debug::debug('Nsp __construct'); 30 | } 31 | 32 | public function __destruct() 33 | { 34 | Debug::debug('Nsp __destruct'); 35 | } 36 | 37 | public function initAdapter() 38 | { 39 | $adapter_name = $this->server->adapter(); 40 | $this->adapter = new $adapter_name($this); 41 | } 42 | 43 | public function to($name) 44 | { 45 | if(!isset($this->rooms[$name])) 46 | { 47 | $this->rooms[$name] = $name; 48 | } 49 | return $this; 50 | } 51 | 52 | public function in($name) 53 | { 54 | return $this->to($name); 55 | } 56 | 57 | 58 | public function add($client, $nsp, $fn) 59 | { 60 | $socket_name = $this->server->socket(); 61 | $socket = new $socket_name($this, $client); 62 | if('open' === $client->conn->readyState) 63 | { 64 | $this->sockets[$socket->id]=$socket; 65 | $socket->onconnect(); 66 | if(!empty($fn)) call_user_func($fn, $socket, $nsp); 67 | $this->emit('connect', $socket); 68 | $this->emit('connection', $socket); 69 | } 70 | else 71 | { 72 | echo('next called after client was closed - ignoring socket'); 73 | } 74 | } 75 | 76 | 77 | /** 78 | * Removes a client. Called by each `Socket`. 79 | * 80 | * @api private 81 | */ 82 | 83 | public function remove($socket) 84 | { 85 | // todo $socket->id 86 | unset($this->sockets[$socket->id]); 87 | } 88 | 89 | 90 | /** 91 | * Emits to all clients. 92 | * 93 | * @return {Namespace} self 94 | * @api public 95 | */ 96 | 97 | public function emit($ev = null) 98 | { 99 | $args = func_get_args(); 100 | if (isset(self::$events[$ev])) 101 | { 102 | call_user_func_array(array(__CLASS__, 'parent::emit'), $args); 103 | } 104 | else 105 | { 106 | // set up packet object 107 | 108 | $parserType = Parser::EVENT; // default 109 | //if (self::hasBin($args)) { $parserType = Parser::BINARY_EVENT; } // binary 110 | 111 | $packet = array('type'=> $parserType, 'data'=> $args ); 112 | 113 | if (is_callable(end($args))) 114 | { 115 | echo('Callbacks are not supported when broadcasting'); 116 | return; 117 | } 118 | 119 | $this->adapter->broadcast($packet, array( 120 | 'rooms'=> $this->rooms, 121 | 'flags'=> $this->flags 122 | )); 123 | 124 | $this->rooms = array(); 125 | $this->flags = array();; 126 | } 127 | return $this; 128 | } 129 | 130 | public function send() 131 | { 132 | $args = func_get_args(); 133 | array_unshift($args, 'message'); 134 | $this->emit($args); 135 | return $this; 136 | } 137 | 138 | public function write() 139 | { 140 | $args = func_get_args(); 141 | return call_user_func_array(array($this, 'send'), $args); 142 | } 143 | 144 | public function clients($fn) 145 | { 146 | $this->adapter->clients($this->rooms, $fn); 147 | return $this; 148 | } 149 | 150 | /** 151 | * Sets the compress flag. 152 | * 153 | * @param {Boolean} if `true`, compresses the sending data 154 | * @return {Socket} self 155 | * @api public 156 | */ 157 | 158 | public function compress($compress) 159 | { 160 | $this->flags['compress'] = $compress; 161 | return $this; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /phpsocketio/SocketIO.php: -------------------------------------------------------------------------------- 1 | nsp($nsp); 20 | 21 | $socket = isset($opts['socket']) ? $opts['socket'] : '\PHPSocketIO\Socket'; 22 | $this->socket($socket); 23 | 24 | $adapter = isset($opts['adapter']) ? $opts['adapter'] : '\PHPSocketIO\DefaultAdapter'; 25 | $this->adapter($adapter); 26 | if(isset($opts['origins'])) 27 | { 28 | $this->origins($opts['origins']); 29 | } 30 | 31 | unset($opts['nsp'], $opts['socket'], $opts['adapter'], $opts['origins']); 32 | 33 | $this->sockets = $this->of('/'); 34 | 35 | if(!class_exists('Protocols\SocketIO')) 36 | { 37 | class_alias('PHPSocketIO\Engine\Protocols\SocketIO', 'Protocols\SocketIO'); 38 | } 39 | 40 | } 41 | 42 | public function nsp($v = null) 43 | { 44 | if (empty($v)) return $this->_nsp; 45 | $this->_nsp = $v; 46 | return $this; 47 | } 48 | 49 | public function socket($v = null) 50 | { 51 | if (empty($v)) return $this->_socket; 52 | $this->_socket = $v; 53 | return $this; 54 | } 55 | 56 | public function adapter($v = null) 57 | { 58 | if (empty($v)) return $this->_adapter; 59 | $this->_adapter = $v; 60 | foreach($this->nsps as $nsp) 61 | { 62 | $nsp->initAdapter(); 63 | } 64 | return $this; 65 | } 66 | 67 | public function origins($v = null) 68 | { 69 | if ($v === null) return $this->_origins; 70 | $this->_origins = $v; 71 | if(isset($this->engine)) { 72 | $this->engine->origins = $this->_origins; 73 | } 74 | return $this; 75 | } 76 | 77 | /*Dosgo*/ 78 | function swbind($serv){ 79 | $this->attach($serv); 80 | $serv->Connections=array();// 81 | $serv->io=$this; 82 | $serv->on('connect', function ($serv, $fd){ 83 | call_user_func_array(array('Swoole\WorkermanApi','WorkerConnect'),[$serv,$fd]); 84 | }); 85 | $serv->on('receive', function ($serv, $fd, $from_id, $data){ 86 | call_user_func_array(array('Swoole\WorkermanApi','WorkerMessage'),[$serv,$fd,$data]); 87 | }); 88 | $serv->on('close', function ($serv, $fd) { 89 | call_user_func_array(array('Swoole\WorkermanApi','Free'),[$serv,$fd]); 90 | }); 91 | } 92 | 93 | 94 | 95 | public function attach($serv) 96 | { 97 | $engine = new Engine(); 98 | $this->eio = $engine->attach($serv); 99 | 100 | // Export http server 101 | $this->worker = $serv; 102 | 103 | // bind to engine events 104 | $this->bind($engine); 105 | 106 | return $this; 107 | } 108 | 109 | public function bind($engine) 110 | { 111 | $this->engine = $engine; 112 | $this->engine->on('connection', array($this, 'onConnection')); 113 | $this->engine->origins = $this->_origins; 114 | return $this; 115 | } 116 | 117 | public function of($name, $fn = null) 118 | { 119 | if($name[0] !== '/') 120 | { 121 | $name = "/$name"; 122 | } 123 | if(empty($this->nsps[$name])) 124 | { 125 | $nsp_name = $this->nsp(); 126 | $this->nsps[$name] = new $nsp_name($this, $name); 127 | } 128 | if ($fn) 129 | { 130 | $this->nsps[$name]->on('connect', $fn); 131 | } 132 | return $this->nsps[$name]; 133 | } 134 | 135 | public function onConnection($engine_socket) 136 | { 137 | $client = new Client($this, $engine_socket); 138 | $client->connect('/'); 139 | return $this; 140 | } 141 | 142 | public function on() 143 | { 144 | $args = array_pad(func_get_args(), 2, null); 145 | 146 | if ($args[0] === 'workerStart') { 147 | $this->worker->onWorkerStart = $args[1]; 148 | } else if ($args[0] === 'workerStop') { 149 | $this->worker->onWorkerStop = $args[1]; 150 | } else if ($args[0] !== null) { 151 | return call_user_func_array(array($this->sockets, 'on'), $args); 152 | } 153 | } 154 | 155 | public function in() 156 | { 157 | return call_user_func_array(array($this->sockets, 'in'), func_get_args()); 158 | } 159 | 160 | public function to() 161 | { 162 | return call_user_func_array(array($this->sockets, 'to'), func_get_args()); 163 | } 164 | 165 | public function emit() 166 | { 167 | return call_user_func_array(array($this->sockets, 'emit'), func_get_args()); 168 | } 169 | 170 | public function send() 171 | { 172 | return call_user_func_array(array($this->sockets, 'send'), func_get_args()); 173 | } 174 | 175 | public function write() 176 | { 177 | return call_user_func_array(array($this->sockets, 'write'), func_get_args()); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /phpsocketio/Engine/Transports/Polling.php: -------------------------------------------------------------------------------- 1 | res; 15 | if ('GET' === $req->method) 16 | { 17 | $this->onPollRequest($req, $res); 18 | } 19 | else if('POST' === $req->method) 20 | { 21 | $this->onDataRequest($req, $res); 22 | } 23 | else 24 | { 25 | $res->writeHead(500); 26 | $res->end(); 27 | } 28 | } 29 | 30 | public function onPollRequest($req, $res) 31 | { 32 | if($this->req) 33 | { 34 | echo ('request overlap'); 35 | // assert: this.res, '.req and .res should be (un)set together' 36 | $this->onError('overlap from client'); 37 | $res->writeHead(500); 38 | return; 39 | } 40 | 41 | $this->req = $req; 42 | $this->res = $res; 43 | 44 | 45 | $req->onClose = array($this, 'pollRequestOnClose'); 46 | $req->cleanup = array($this, 'pollRequestClean'); 47 | 48 | $this->writable = true; 49 | $this->emit('drain'); 50 | 51 | // if we're still writable but had a pending close, trigger an empty send 52 | if ($this->writable && $this->shouldClose) 53 | { 54 | echo('triggering empty send to append close packet'); 55 | $this->send(array(array('type'=>'noop'))); 56 | } 57 | } 58 | 59 | public function pollRequestOnClose() 60 | { 61 | $this->onError('poll connection closed prematurely'); 62 | $this->pollRequestClean(); 63 | } 64 | 65 | public function pollRequestClean() 66 | { 67 | if(isset($this->req)) 68 | { 69 | $this->req->res = null; 70 | $this->req->onClose = $this->req->cleanup = null; 71 | $this->req = $this->res = null; 72 | } 73 | } 74 | 75 | public function onDataRequest($req, $res) 76 | { 77 | if(isset($this->dataReq)) 78 | { 79 | // assert: this.dataRes, '.dataReq and .dataRes should be (un)set together' 80 | $this->onError('data request overlap from client'); 81 | $res->writeHead(500); 82 | return; 83 | } 84 | 85 | $this->dataReq = $req; 86 | $this->dataRes = $res; 87 | $req->onClose = array($this, 'dataRequestOnClose'); 88 | $req->onData = array($this, 'dataRequestOnData'); 89 | $req->onEnd = array($this, 'dataRequestOnEnd'); 90 | } 91 | 92 | public function dataRequestCleanup() 93 | { 94 | $this->chunks = ''; 95 | $this->dataReq->res = null; 96 | $this->dataReq->onClose = $this->dataReq->onData = $this->dataReq->onEnd = null; 97 | $this->dataReq = $this->dataRes = null; 98 | } 99 | 100 | public function dataRequestOnClose() 101 | { 102 | $this->dataRequestCleanup(); 103 | $this->onError('data request connection closed prematurely'); 104 | } 105 | 106 | public function dataRequestOnData($req, $data) 107 | { 108 | $this->chunks .= $data; 109 | // todo maxHttpBufferSize 110 | /*if(strlen($this->chunks) > $this->maxHttpBufferSize) 111 | { 112 | $this->chunks = ''; 113 | $req->connection->destroy(); 114 | }*/ 115 | } 116 | 117 | public function dataRequestOnEnd () 118 | { 119 | $this->onData($this->chunks); 120 | 121 | $headers = array( 122 | 'Content-Type'=> 'text/html', 123 | 'Content-Length'=> 2, 124 | 'X-XSS-Protection' => '0', 125 | ); 126 | 127 | $this->dataRes->writeHead(200, '', $this->headers($this->dataReq, $headers)); 128 | $this->dataRes->end('ok'); 129 | $this->dataRequestCleanup(); 130 | } 131 | 132 | public function onData($data) 133 | { 134 | $packets = Parser::decodePayload($data); 135 | if(isset($packets['type'])) 136 | { 137 | if('close' === $packets['type']) 138 | { 139 | $this->onClose(); 140 | return false; 141 | } 142 | else 143 | { 144 | $packets = array($packets); 145 | } 146 | } 147 | 148 | foreach($packets as $packet) 149 | { 150 | $this->onPacket($packet); 151 | } 152 | } 153 | 154 | public function onClose() 155 | { 156 | if($this->writable) 157 | { 158 | // close pending poll request 159 | $this->send(array(array('type'=> 'noop'))); 160 | } 161 | parent::onClose(); 162 | } 163 | 164 | public function send($packets) 165 | { 166 | $this->writable = false; 167 | if($this->shouldClose) 168 | { 169 | echo('appending close packet to payload'); 170 | $packets[] = array('type'=>'close'); 171 | call_user_func($this->shouldClose); 172 | $this->shouldClose = null; 173 | } 174 | $data = Parser::encodePayload($packets, $this->supportsBinary); 175 | $this->write($data); 176 | } 177 | 178 | public function write($data) 179 | { 180 | $this->doWrite($data); 181 | if(!empty($this->req->cleanup)) 182 | { 183 | call_user_func($this->req->cleanup); 184 | } 185 | } 186 | 187 | public function doClose($fn) 188 | { 189 | if(!empty($this->dataReq)) 190 | { 191 | //echo('aborting ongoing data request'); 192 | $this->dataReq->destroy(); 193 | } 194 | 195 | if($this->writable) 196 | { 197 | //echo('transport writable - closing right away'); 198 | $this->send(array(array('type'=> 'close'))); 199 | call_user_func($fn); 200 | } 201 | else 202 | { 203 | //echo("transport not writable - buffering orderly close\n"); 204 | $this->shouldClose = $fn; 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /phpsocketio/Engine/Protocols/Http/Response.php: -------------------------------------------------------------------------------- 1 | _connection = $connection; 23 | } 24 | 25 | protected function initHeader() 26 | { 27 | $this->_headers['Connection'] = 'keep-alive'; 28 | $this->_headers['Content-Type'] = 'Content-Type: text/html;charset=utf-8'; 29 | } 30 | 31 | public function writeHead($status_code, $reason_phrase = '', $headers = null) 32 | { 33 | if($this->headersSent) 34 | { 35 | echo "header has already send\n"; 36 | return false; 37 | } 38 | $this->statusCode = $status_code; 39 | if($reason_phrase) 40 | { 41 | $this->_statusPhrase = $reason_phrase; 42 | } 43 | if($headers) 44 | { 45 | foreach($headers as $key=>$val) 46 | { 47 | $this->_headers[$key] = $val; 48 | } 49 | } 50 | $this->_buffer = $this->getHeadBuffer(); 51 | $this->headersSent = true; 52 | } 53 | 54 | public function getHeadBuffer() 55 | { 56 | if(!$this->_statusPhrase) 57 | { 58 | $this->_statusPhrase = isset(self::$codes[$this->statusCode]) ? self::$codes[$this->statusCode] : ''; 59 | } 60 | $head_buffer = "HTTP/1.1 $this->statusCode $this->_statusPhrase\r\n"; 61 | if(!isset($this->_headers['Content-Length']) && !isset($this->_headers['Transfer-Encoding'])) 62 | { 63 | $head_buffer .= "Transfer-Encoding: chunked\r\n"; 64 | } 65 | if(!isset($this->_headers['Connection'])) 66 | { 67 | $head_buffer .= "Connection: keep-alive\r\n"; 68 | } 69 | foreach($this->_headers as $key=>$val) 70 | { 71 | if($key === 'Set-Cookie' && is_array($val)) 72 | { 73 | foreach($val as $v) 74 | { 75 | $head_buffer .= "Set-Cookie: $v\r\n"; 76 | } 77 | continue; 78 | } 79 | $head_buffer .= "$key: $val\r\n"; 80 | } 81 | return $head_buffer."\r\n"; 82 | } 83 | 84 | public function setHeader($key, $val) 85 | { 86 | $this->_headers[$key] = $val; 87 | } 88 | 89 | public function getHeader($name) 90 | { 91 | return isset($this->_headers[$name]) ? $this->_headers[$name] : ''; 92 | } 93 | 94 | public function removeHeader($name) 95 | { 96 | unset($this->_headers[$name]); 97 | } 98 | 99 | public function write($chunk) 100 | { 101 | if(!isset($this->_headers['Content-Length'])) 102 | { 103 | $chunk = dechex(strlen($chunk)) . "\r\n" . $chunk . "\r\n"; 104 | } 105 | if(!$this->headersSent) 106 | { 107 | $head_buffer = $this->getHeadBuffer(); 108 | $this->_buffer = $head_buffer . $chunk; 109 | $this->headersSent = true; 110 | } 111 | else 112 | { 113 | $this->_buffer .= $chunk; 114 | } 115 | } 116 | 117 | public function end($data = null) 118 | { 119 | if(!$this->writable) 120 | { 121 | echo new \Exception('unwirtable'); 122 | return false; 123 | } 124 | if($data !== null) 125 | { 126 | $this->write($data); 127 | } 128 | 129 | if(!$this->headersSent) 130 | { 131 | $head_buffer = $this->getHeadBuffer(); 132 | $this->_buffer = $head_buffer; 133 | $this->headersSent = true; 134 | } 135 | 136 | if(!isset($this->_headers['Content-Length'])) 137 | { 138 | $ret = $this->_connection->send($this->_buffer . "0\r\n\r\n", true); 139 | $this->destroy(); 140 | return $ret; 141 | } 142 | $ret = $this->_connection->send($this->_buffer, true); 143 | $this->destroy(); 144 | return $ret; 145 | } 146 | 147 | public function destroy() 148 | { 149 | if(!empty($this->_connection->httpRequest)) 150 | { 151 | $this->_connection->httpRequest->destroy(); 152 | } 153 | $this->_connection->httpResponse = $this->_connection->httpRequest = null; 154 | $this->_connection = null; 155 | $this->writable = false; 156 | } 157 | 158 | public static $codes = array( 159 | 100 => 'Continue', 160 | 101 => 'Switching Protocols', 161 | 200 => 'OK', 162 | 201 => 'Created', 163 | 202 => 'Accepted', 164 | 203 => 'Non-Authoritative Information', 165 | 204 => 'No Content', 166 | 205 => 'Reset Content', 167 | 206 => 'Partial Content', 168 | 300 => 'Multiple Choices', 169 | 301 => 'Moved Permanently', 170 | 302 => 'Found', 171 | 303 => 'See Other', 172 | 304 => 'Not Modified', 173 | 305 => 'Use Proxy', 174 | 306 => '(Unused)', 175 | 307 => 'Temporary Redirect', 176 | 400 => 'Bad Request', 177 | 401 => 'Unauthorized', 178 | 402 => 'Payment Required', 179 | 403 => 'Forbidden', 180 | 404 => 'Not Found', 181 | 405 => 'Method Not Allowed', 182 | 406 => 'Not Acceptable', 183 | 407 => 'Proxy Authentication Required', 184 | 408 => 'Request Timeout', 185 | 409 => 'Conflict', 186 | 410 => 'Gone', 187 | 411 => 'Length Required', 188 | 412 => 'Precondition Failed', 189 | 413 => 'Request Entity Too Large', 190 | 414 => 'Request-URI Too Long', 191 | 415 => 'Unsupported Media Type', 192 | 416 => 'Requested Range Not Satisfiable', 193 | 417 => 'Expectation Failed', 194 | 422 => 'Unprocessable Entity', 195 | 423 => 'Locked', 196 | 500 => 'Internal Server Error', 197 | 501 => 'Not Implemented', 198 | 502 => 'Bad Gateway', 199 | 503 => 'Service Unavailable', 200 | 504 => 'Gateway Timeout', 201 | 505 => 'HTTP Version Not Supported', 202 | ); 203 | } 204 | -------------------------------------------------------------------------------- /phpsocketio/Engine/Protocols/SocketIO.php: -------------------------------------------------------------------------------- 1 | hasReadedHead)) 12 | { 13 | return strlen($http_buffer); 14 | } 15 | $pos = strpos($http_buffer, "\r\n\r\n"); 16 | if(!$pos) 17 | { 18 | if(strlen($http_buffer)>=TcpConnection::$maxPackageSize) 19 | { 20 | $connection->close("HTTP/1.1 400 bad request\r\n\r\nheader too long"); 21 | return 0; 22 | } 23 | return 0; 24 | } 25 | $head_len = $pos + 4; 26 | $raw_head = substr($http_buffer, 0, $head_len); 27 | $raw_body = substr($http_buffer, $head_len); 28 | $req = new Request($connection, $raw_head); 29 | $res = new Response($connection); 30 | $connection->httpRequest = $req; 31 | $connection->httpResponse = $res; 32 | $connection->hasReadedHead = true; 33 | //samluo 34 | //TcpConnection::$statistics['total_request']++; 35 | $connection->onClose = '\PHPSocketIO\Engine\Protocols\SocketIO::emitClose'; 36 | if(isset($req->headers['upgrade']) && strtolower($req->headers['upgrade']) === 'websocket') 37 | { 38 | 39 | 40 | $connection->consumeRecvBuffer(strlen($http_buffer)); 41 | WebSocket::dealHandshake($connection, $req, $res); 42 | self::cleanup($connection); 43 | return 0; 44 | } 45 | if(!empty($connection->onRequest)) 46 | { 47 | $connection->consumeRecvBuffer(strlen($http_buffer)); 48 | self::emitRequest($connection, $req, $res); 49 | if($req->method === 'GET' || $req->method === 'OPTIONS') 50 | { 51 | self::emitEnd($connection, $req); 52 | return 0; 53 | } 54 | 55 | // POST 56 | if(!isset($connection->onMessage)||'\PHPSocketIO\Engine\Protocols\SocketIO::onData' !== $connection->onMessage) 57 | { 58 | $connection->onMessage = '\PHPSocketIO\Engine\Protocols\SocketIO::onData'; 59 | } 60 | if(!$raw_body) 61 | { 62 | return 0; 63 | } 64 | self::onData($connection, $raw_body); 65 | return 0; 66 | } 67 | else 68 | { 69 | if($req->method === 'GET') 70 | { 71 | return $pos + 4; 72 | } 73 | elseif(isset($req->headers['content-length'])) 74 | { 75 | return $req->headers['content-length']; 76 | } 77 | else 78 | { 79 | $connection->close("HTTP/1.1 400 bad request\r\n\r\ntrunk not support"); 80 | return 0; 81 | } 82 | } 83 | } 84 | 85 | public static function onData($connection, $data) 86 | { 87 | $req = $connection->httpRequest; 88 | self::emitData($connection, $req, $data); 89 | if((isset($req->headers['content-length']) && $req->headers['content-length'] <= strlen($data)) 90 | || substr($data, -5) === "0\r\n\r\n") 91 | { 92 | self::emitEnd($connection, $req); 93 | } 94 | } 95 | 96 | protected static function emitRequest($connection, $req, $res) 97 | { 98 | try 99 | { 100 | call_user_func($connection->onRequest, $req, $res); 101 | } 102 | catch(\Exception $e) 103 | { 104 | echo $e; 105 | } 106 | } 107 | 108 | public static function emitClose($connection) 109 | { 110 | $req = $connection->httpRequest; 111 | if(isset($req->onClose)) 112 | { 113 | try 114 | { 115 | call_user_func($req->onClose, $req); 116 | } 117 | catch(\Exception $e) 118 | { 119 | echo $e; 120 | } 121 | } 122 | $res = $connection->httpResponse; 123 | if(isset($res->onClose)) 124 | { 125 | try 126 | { 127 | call_user_func($res->onClose, $res); 128 | } 129 | catch(\Exception $e) 130 | { 131 | echo $e; 132 | } 133 | } 134 | self::cleanup($connection); 135 | } 136 | 137 | public static function cleanup($connection) 138 | { 139 | if(!empty($connection->onRequest)) 140 | { 141 | $connection->onRequest = null; 142 | } 143 | if(!empty($connection->onWebSocketConnect)) 144 | { 145 | $connection->onWebSocketConnect = null; 146 | } 147 | if(!empty($connection->httpRequest)) 148 | { 149 | $connection->httpRequest->destroy(); 150 | $connection->httpRequest = null; 151 | } 152 | if(!empty($connection->httpResponse)) 153 | { 154 | $connection->httpResponse->destroy(); 155 | $connection->httpResponse = null; 156 | } 157 | } 158 | 159 | public static function emitData($connection, $req, $data) 160 | { 161 | if(isset($req->onData)) 162 | { 163 | try 164 | { 165 | call_user_func($req->onData, $req, $data); 166 | } 167 | catch(\Exception $e) 168 | { 169 | echo $e; 170 | } 171 | } 172 | } 173 | 174 | public static function emitEnd($connection, $req) 175 | { 176 | if(isset($req->onEnd)) 177 | { 178 | try 179 | { 180 | call_user_func($req->onEnd, $req); 181 | } 182 | catch(\Exception $e) 183 | { 184 | echo $e; 185 | } 186 | } 187 | $connection->hasReadedHead = false; 188 | } 189 | 190 | public static function encode($buffer, $connection) 191 | { 192 | if(!isset($connection->onRequest)) 193 | { 194 | $connection->httpResponse->setHeader('Content-Length', strlen($buffer)); 195 | return $connection->httpResponse->getHeadBuffer() . $buffer; 196 | } 197 | return $buffer; 198 | } 199 | 200 | public static function decode($http_buffer, $connection) 201 | { 202 | if(isset($connection->onRequest)) 203 | { 204 | return $http_buffer; 205 | } 206 | else 207 | { 208 | list($head, $body) = explode("\r\n\r\n", $http_buffer, 2); 209 | return $body; 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /phpsocketio/Client.php: -------------------------------------------------------------------------------- 1 | server = $server; 18 | $this->conn = $conn; 19 | $this->encoder = new \PHPSocketIO\Parser\Encoder(); 20 | $this->decoder = new \PHPSocketIO\Parser\Decoder(); 21 | $this->id = $conn->id; 22 | $this->request = $conn->request; 23 | $this->setup(); 24 | Debug::debug('Client __construct'); 25 | } 26 | 27 | public function __destruct() 28 | { 29 | Debug::debug('Client __destruct'); 30 | } 31 | 32 | /** 33 | * Sets up event listeners. 34 | * 35 | * @api private 36 | */ 37 | 38 | public function setup(){ 39 | $this->decoder->on('decoded', array($this,'ondecoded')); 40 | $this->conn->on('data', array($this,'ondata')); 41 | $this->conn->on('error', array($this, 'onerror')); 42 | $this->conn->on('close' ,array($this, 'onclose')); 43 | } 44 | 45 | /** 46 | * Connects a client to a namespace. 47 | * 48 | * @param {String} namespace name 49 | * @api private 50 | */ 51 | 52 | public function connect($name){ 53 | if (!isset($this->server->nsps[$name])) 54 | { 55 | $this->packet(array('type'=> Parser::ERROR, 'nsp'=> $name, 'data'=> 'Invalid namespace')); 56 | return; 57 | } 58 | $nsp = $this->server->of($name); 59 | if ('/' !== $name && !isset($this->nsps['/'])) 60 | { 61 | $this->connectBuffer[$name] = $name; 62 | return; 63 | } 64 | $nsp->add($this, $nsp, array($this, 'nspAdd')); 65 | } 66 | 67 | public function nspAdd($socket, $nsp) 68 | { 69 | $this->sockets[$socket->id] = $socket; 70 | $this->nsps[$nsp->name] = $socket; 71 | if ('/' === $nsp->name && $this->connectBuffer) 72 | { 73 | foreach($this->connectBuffer as $name) 74 | { 75 | $this->connect($name); 76 | } 77 | $this->connectBuffer = array(); 78 | } 79 | } 80 | 81 | 82 | 83 | /** 84 | * Disconnects from all namespaces and closes transport. 85 | * 86 | * @api private 87 | */ 88 | 89 | public function disconnect() 90 | { 91 | foreach($this->sockets as $socket) 92 | { 93 | $socket->disconnect(); 94 | } 95 | $this->sockets = array(); 96 | $this->close(); 97 | } 98 | 99 | /** 100 | * Removes a socket. Called by each `Socket`. 101 | * 102 | * @api private 103 | */ 104 | 105 | public function remove($socket) 106 | { 107 | if(isset($this->sockets[$socket->id])) 108 | { 109 | $nsp = $this->sockets[$socket->id]->nsp->name; 110 | unset($this->sockets[$socket->id]); 111 | unset($this->nsps[$nsp]); 112 | } else { 113 | //echo('ignoring remove for '. $socket->id); 114 | } 115 | } 116 | 117 | /** 118 | * Closes the underlying connection. 119 | * 120 | * @api private 121 | */ 122 | 123 | public function close() 124 | { 125 | if (empty($this->conn)) return; 126 | if('open' === $this->conn->readyState) 127 | { 128 | //echo('forcing transport close'); 129 | $this->conn->close(); 130 | $this->onclose('forced server close'); 131 | } 132 | } 133 | 134 | /** 135 | * Writes a packet to the transport. 136 | * 137 | * @param {Object} packet object 138 | * @param {Object} options 139 | * @api private 140 | */ 141 | public function packet($packet, $preEncoded = false, $volatile = false) 142 | { 143 | if(!empty($this->conn) && 'open' === $this->conn->readyState) 144 | { 145 | if (!$preEncoded) 146 | { 147 | // not broadcasting, need to encode 148 | $encodedPackets = $this->encoder->encode($packet); 149 | $this->writeToEngine($encodedPackets, $volatile); 150 | } else { // a broadcast pre-encodes a packet 151 | $this->writeToEngine($packet); 152 | } 153 | } else { 154 | // todo check 155 | // echo('ignoring packet write ' . var_export($packet, true)); 156 | } 157 | } 158 | 159 | public function writeToEngine($encodedPackets, $volatile = false) 160 | { 161 | if($volatile)echo new \Exception('volatile'); 162 | if ($volatile && !$this->conn->transport->writable) return; 163 | // todo check 164 | if(isset($encodedPackets['nsp']))unset($encodedPackets['nsp']); 165 | foreach($encodedPackets as $packet) 166 | { 167 | $this->conn->write($packet); 168 | } 169 | } 170 | 171 | 172 | /** 173 | * Called with incoming transport data. 174 | * 175 | * @api private 176 | */ 177 | 178 | public function ondata($data) 179 | { 180 | try { 181 | // todo chek '2["chat message","2"]' . "\0" . '' 182 | $this->decoder->add(trim($data)); 183 | } catch(\Exception $e) { 184 | $this->onerror($e); 185 | } 186 | } 187 | 188 | /** 189 | * Called when parser fully decodes a packet. 190 | * 191 | * @api private 192 | */ 193 | 194 | public function ondecoded($packet) 195 | { 196 | if(Parser::CONNECT == $packet['type']) 197 | { 198 | $this->connect($packet['nsp']); 199 | } else { 200 | if(isset($this->nsps[$packet['nsp']])) 201 | { 202 | $this->nsps[$packet['nsp']]->onpacket($packet); 203 | } else { 204 | //echo('no socket for namespace ' . $packet['nsp']); 205 | } 206 | } 207 | } 208 | 209 | /** 210 | * Handles an error. 211 | * 212 | * @param {Objcet} error object 213 | * @api private 214 | */ 215 | 216 | public function onerror($err) 217 | { 218 | foreach($this->sockets as $socket) 219 | { 220 | $socket->onerror($err); 221 | } 222 | $this->onclose('client error'); 223 | } 224 | 225 | /** 226 | * Called upon transport close. 227 | * 228 | * @param {String} reason 229 | * @api private 230 | */ 231 | 232 | public function onclose($reason) 233 | { 234 | if (empty($this->conn)) return; 235 | // ignore a potential subsequent `close` event 236 | $this->destroy(); 237 | 238 | // `nsps` and `sockets` are cleaned up seamlessly 239 | foreach($this->sockets as $socket) 240 | { 241 | $socket->onclose($reason); 242 | } 243 | $this->sockets = null; 244 | } 245 | 246 | /** 247 | * Cleans up event listeners. 248 | * 249 | * @api private 250 | */ 251 | 252 | public function destroy() 253 | { 254 | if (!$this->conn) return; 255 | $this->conn->removeAllListeners(); 256 | $this->decoder->removeAllListeners(); 257 | $this->encoder->removeAllListeners(); 258 | $this->server = $this->conn = $this->encoder = $this->decoder = $this->request = $this->nsps = null; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /phpsocketio/Engine/Parser.php: -------------------------------------------------------------------------------- 1 | 0 // non-ws 14 | , 'close'=> 1 // non-ws 15 | , 'ping'=> 2 16 | , 'pong'=> 3 17 | , 'message'=> 4 18 | , 'upgrade'=> 5 19 | , 'noop'=> 6 20 | ); 21 | 22 | public static $packetsList = array( 23 | 'open', 24 | 'close', 25 | 'ping', 26 | 'pong', 27 | 'message', 28 | 'upgrade', 29 | 'noop' 30 | ); 31 | 32 | public static $err = array( 33 | 'type' => 'error', 34 | 'data' => 'parser error' 35 | ); 36 | 37 | public static function encodePacket($packet) 38 | { 39 | $data = !isset($packet['data']) ? '' : $packet['data']; 40 | return self::$packets[$packet['type']].$data; 41 | } 42 | 43 | 44 | /** 45 | * Encodes a packet with binary data in a base64 string 46 | * 47 | * @param {Object} packet, has `type` and `data` 48 | * @return {String} base64 encoded message 49 | */ 50 | 51 | public static function encodeBase64Packet($packet) 52 | { 53 | $data = isset($packet['data']) ? '' : $packet['data']; 54 | return $message = 'b' . self::$packets[$packet['type']] . base64_encode($packet['data']); 55 | } 56 | 57 | /** 58 | * Decodes a packet. Data also available as an ArrayBuffer if requested. 59 | * 60 | * @return {Object} with `type` and `data` (if any) 61 | * @api private 62 | */ 63 | 64 | public static function decodePacket($data, $binaryType = null, $utf8decode = true) 65 | { 66 | // String data todo check if (typeof data == 'string' || data === undefined) 67 | if ($data[0] === 'b') 68 | { 69 | return self::decodeBase64Packet(substr($data, 1), $binaryType); 70 | } 71 | 72 | $type = $data[0]; 73 | if (!isset(self::$packetsList[$type])) 74 | { 75 | return self::$err; 76 | } 77 | 78 | if (isset($data[1])) 79 | { 80 | return array('type'=> self::$packetsList[$type], 'data'=> substr($data, 1)); 81 | } 82 | else 83 | { 84 | return array('type'=> self::$packetsList[$type]); 85 | } 86 | } 87 | 88 | /** 89 | * Decodes a packet encoded in a base64 string. 90 | * 91 | * @param {String} base64 encoded message 92 | * @return {Object} with `type` and `data` (if any) 93 | */ 94 | 95 | public static function decodeBase64Packet($msg, $binaryType) 96 | { 97 | $type = self::$packetsList[$msg[0]]; 98 | $data = base64_decode(substr($data, 1)); 99 | return array('type'=> $type, 'data'=> $data); 100 | } 101 | 102 | /** 103 | * Encodes multiple messages (payload). 104 | * 105 | * :data 106 | * 107 | * Example: 108 | * 109 | * 11:hello world2:hi 110 | * 111 | * If any contents are binary, they will be encoded as base64 strings. Base64 112 | * encoded strings are marked with a b before the length specifier 113 | * 114 | * @param {Array} packets 115 | * @api private 116 | */ 117 | 118 | public static function encodePayload($packets, $supportsBinary = null) 119 | { 120 | if ($supportsBinary) 121 | { 122 | return self::encodePayloadAsBinary($packets); 123 | } 124 | 125 | if (!$packets) 126 | { 127 | return '0:'; 128 | } 129 | 130 | $results = ''; 131 | foreach($packets as $msg) 132 | { 133 | $results .= self::encodeOne($msg, $supportsBinary); 134 | } 135 | return $results; 136 | } 137 | 138 | 139 | public static function encodeOne($packet, $supportsBinary = null, $result = null) 140 | { 141 | $message = self::encodePacket($packet, $supportsBinary, true); 142 | return strlen($message) . ':' . $message; 143 | } 144 | 145 | 146 | /* 147 | * Decodes data when a payload is maybe expected. Possible binary contents are 148 | * decoded from their base64 representation 149 | * 150 | * @api public 151 | */ 152 | 153 | public static function decodePayload($data, $binaryType = null) 154 | { 155 | if(!preg_match('/^\d+:\d/',$data)) 156 | { 157 | return self::decodePayloadAsBinary($data, $binaryType); 158 | } 159 | 160 | if ($data === '') 161 | { 162 | // parser error - ignoring payload 163 | return self::$err; 164 | } 165 | 166 | $length = '';//, n, msg; 167 | 168 | for ($i = 0, $l = strlen($data); $i < $l; $i++) 169 | { 170 | $chr = $data[$i]; 171 | 172 | if (':' != $chr) 173 | { 174 | $length .= $chr; 175 | } 176 | else 177 | { 178 | if ('' == $length || ($length != ($n = intval($length)))) 179 | { 180 | // parser error - ignoring payload 181 | return self::$err; 182 | } 183 | 184 | $msg = substr($data, $i + 1/*, $n*/); 185 | 186 | /*if ($length != strlen($msg)) 187 | { 188 | // parser error - ignoring payload 189 | return self::$err; 190 | }*/ 191 | 192 | if (isset($msg[0])) 193 | { 194 | $packet = self::decodePacket($msg, $binaryType, true); 195 | 196 | if (self::$err['type'] == $packet['type'] && self::$err['data'] == $packet['data']) 197 | { 198 | // parser error in individual packet - ignoring payload 199 | return self::$err; 200 | } 201 | 202 | return $packet; 203 | } 204 | 205 | // advance cursor 206 | $i += $n; 207 | $length = ''; 208 | } 209 | } 210 | 211 | if ($length !== '') 212 | { 213 | // parser error - ignoring payload 214 | echo new \Exception('parser error'); 215 | return self::$err; 216 | } 217 | } 218 | 219 | /** 220 | * Encodes multiple messages (payload) as binary. 221 | * 222 | * <1 = binary, 0 = string>[...] 224 | * 225 | * Example: 226 | * 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers 227 | * 228 | * @param {Array} packets 229 | * @return {Buffer} encoded payload 230 | * @api private 231 | */ 232 | 233 | public static function encodePayloadAsBinary($packets) 234 | { 235 | $results = ''; 236 | foreach($packets as $msg) 237 | { 238 | $results .= self::encodeOneAsBinary($msg); 239 | } 240 | return $results; 241 | } 242 | 243 | public static function encodeOneAsBinary($p) 244 | { 245 | // todo is string or arraybuf 246 | $packet = self::encodePacket($p, true, true); 247 | $encodingLength = ''.strlen($packet); 248 | $sizeBuffer = chr(0); 249 | for ($i = 0; $i < strlen($encodingLength); $i++) 250 | { 251 | $sizeBuffer .= chr($encodingLength[$i]); 252 | } 253 | $sizeBuffer .= chr(255); 254 | return $sizeBuffer.$packet; 255 | } 256 | 257 | /* 258 | * Decodes data when a payload is maybe expected. Strings are decoded by 259 | * interpreting each byte as a key code for entries marked to start with 0. See 260 | * description of encodePayloadAsBinary 261 | * @api public 262 | */ 263 | 264 | public static function decodePayloadAsBinary($data, $binaryType = null) 265 | { 266 | $bufferTail = $data; 267 | $buffers = array(); 268 | 269 | while (strlen($bufferTail) > 0) 270 | { 271 | $strLen = ''; 272 | $isString = $bufferTail[0] == 0; 273 | $numberTooLong = false; 274 | for ($i = 1; ; $i++) 275 | { 276 | $tail = ord($bufferTail[$i]); 277 | if ($tail === 255) break; 278 | // 310 = char length of Number.MAX_VALUE 279 | if (strlen($strLen) > 310) 280 | { 281 | $numberTooLong = true; 282 | break; 283 | } 284 | $strLen .= $tail; 285 | } 286 | if($numberTooLong) return self::$err; 287 | $bufferTail = substr($bufferTail, strlen($strLen) + 1); 288 | 289 | $msgLength = intval($strLen, 10); 290 | 291 | $msg = substr($bufferTail, 1, $msgLength + 1); 292 | $buffers[] = $msg; 293 | $bufferTail = substr($bufferTail, $msgLength + 1); 294 | } 295 | $total = count($buffers); 296 | $packets = array(); 297 | foreach($buffers as $i => $buffer) 298 | { 299 | $packets[] = self::decodePacket($buffer, $binaryType, true); 300 | } 301 | return $packets; 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /phpsocketio/Engine/Engine.php: -------------------------------------------------------------------------------- 1 | 'polling', 20 | 'websocket' => 'websocket' 21 | ); 22 | 23 | public static $errorMessages = array( 24 | 'Transport unknown', 25 | 'Session ID unknown', 26 | 'Bad handshake method', 27 | 'Bad request' 28 | ); 29 | 30 | const ERROR_UNKNOWN_TRANSPORT = 0; 31 | 32 | const ERROR_UNKNOWN_SID = 1; 33 | 34 | const ERROR_BAD_HANDSHAKE_METHOD = 2; 35 | 36 | const ERROR_BAD_REQUEST = 3; 37 | 38 | public function __construct($opts = array()) 39 | { 40 | $ops_map = array( 41 | 'pingTimeout', 42 | 'pingInterval', 43 | 'upgradeTimeout', 44 | 'transports', 45 | 'allowUpgrades', 46 | 'allowRequest' 47 | ); 48 | foreach($ops_map as $key) 49 | { 50 | if(isset($opts[$key])) 51 | { 52 | $this->$key = $opts[$key]; 53 | } 54 | } 55 | Debug::debug('Engine __construct'); 56 | } 57 | 58 | public function __destruct() 59 | { 60 | Debug::debug('Engine __destruct'); 61 | } 62 | 63 | public function handleRequest($req, $res) 64 | { 65 | $this->prepare($req); 66 | $req->res = $res; 67 | $this->verify($req, $res, false, array($this, 'dealRequest')); 68 | } 69 | 70 | public function dealRequest($err, $success, $req) 71 | { 72 | 73 | if (!$success) 74 | { 75 | self::sendErrorMessage($req, $req->res, $err); 76 | return; 77 | } 78 | if(isset($req->_query['sid'])) 79 | { 80 | $this->clients[$req->_query['sid']]->transport->onRequest($req); 81 | } 82 | else 83 | { 84 | $this->handshake($req->_query['transport'], $req); 85 | } 86 | } 87 | 88 | protected function sendErrorMessage($req, $res, $code) 89 | { 90 | $headers = array('Content-Type'=> 'application/json'); 91 | if(isset($req->headers['origin'])) 92 | { 93 | $headers['Access-Control-Allow-Credentials'] = 'true'; 94 | $headers['Access-Control-Allow-Origin'] = $req->headers['origin']; 95 | } 96 | else 97 | { 98 | $headers['Access-Control-Allow-Origin'] = '*'; 99 | } 100 | 101 | $res->writeHead(403, '', $headers); 102 | $res->end(json_encode(array( 103 | 'code' => $code, 104 | 'message' => isset(self::$errorMessages[$code]) ? self::$errorMessages[$code] : $code 105 | ))); 106 | } 107 | 108 | protected function verify($req, $res, $upgrade, $fn) 109 | { 110 | if(!isset($req->_query['transport']) || !isset(self::$allowTransports[$req->_query['transport']])) 111 | { 112 | return call_user_func($fn, self::ERROR_UNKNOWN_TRANSPORT, false, $req, $res); 113 | } 114 | $transport = $req->_query['transport']; 115 | $sid = isset($req->_query['sid']) ? $req->_query['sid'] : ''; 116 | if($sid) 117 | { 118 | if(!isset($this->clients[$sid])) 119 | { 120 | return call_user_func($fn, self::ERROR_UNKNOWN_SID, false, $req, $res); 121 | } 122 | if(!$upgrade && $this->clients[$sid]->transport->name !== $transport) 123 | { 124 | return call_user_func($fn, self::ERROR_BAD_REQUEST, false, $req, $res); 125 | } 126 | } 127 | else 128 | { 129 | if('GET' !== $req->method) 130 | { 131 | return call_user_func($fn, self::ERROR_BAD_HANDSHAKE_METHOD, false, $req, $res); 132 | } 133 | return $this->checkRequest($req, $res, $fn); 134 | } 135 | call_user_func($fn, null, true, $req, $res); 136 | } 137 | 138 | public function checkRequest($req, $res, $fn) 139 | { 140 | if ($this->origins === "*:*" || empty($this->origins)) 141 | { 142 | return call_user_func($fn, null, true, $req, $res); 143 | } 144 | $origin = null; 145 | if (isset($req->headers['origin'])) 146 | { 147 | $origin = $req->headers['origin']; 148 | } 149 | else if(isset($req->headers['referer'])) 150 | { 151 | $origin = $req->headers['referer']; 152 | } 153 | 154 | // file:// URLs produce a null Origin which can't be authorized via echo-back 155 | if ('null' === $origin || null === $origin) { 156 | return call_user_func($fn, null, true, $req, $res); 157 | } 158 | 159 | if ($origin) 160 | { 161 | $parts = parse_url($origin); 162 | $defaultPort = 'https:' === $parts['scheme'] ? 443 : 80; 163 | $parts['port'] = isset($parts['port']) ? $parts['port'] : $defaultPort; 164 | $allowed_origins = explode(' ', $this->origins); 165 | foreach( $allowed_origins as $allow_origin ){ 166 | $ok = 167 | $allow_origin === $parts['scheme'] . '://' . $parts['host'] . ':' . $parts['port'] || 168 | $allow_origin === $parts['scheme'] . '://' . $parts['host'] || 169 | $allow_origin === $parts['scheme'] . '://' . $parts['host'] . ':*' || 170 | $allow_origin === '*:' . $parts['port']; 171 | return call_user_func($fn, null, $ok, $req, $res); 172 | } 173 | } 174 | call_user_func($fn, null, false, $req, $res); 175 | } 176 | 177 | protected function prepare($req) 178 | { 179 | if(!isset($req->_query)) 180 | { 181 | $info = parse_url($req->url); 182 | if(isset($info['query'])) 183 | { 184 | parse_str($info['query'], $req->_query); 185 | } 186 | } 187 | } 188 | 189 | public function handshake($transport, $req) 190 | { 191 | $id = bin2hex(pack('d', microtime(true)).pack('N', function_exists('random_int') ? random_int(1, 100000000): rand(1, 100000000))); 192 | if ($transport == 'websocket') { 193 | $transport = '\\PHPSocketIO\\Engine\\Transports\\WebSocket'; 194 | } 195 | elseif (isset($req->_query['j'])) 196 | { 197 | $transport = '\\PHPSocketIO\\Engine\\Transports\\PollingJsonp'; 198 | } 199 | else 200 | { 201 | $transport = '\\PHPSocketIO\\Engine\\Transports\\PollingXHR'; 202 | } 203 | 204 | $transport = new $transport($req); 205 | 206 | $transport->supportsBinary = !isset($req->_query['b64']); 207 | 208 | $socket = new Socket($id, $this, $transport, $req); 209 | 210 | /* $transport->on('headers', function(&$headers)use($id) 211 | { 212 | $headers['Set-Cookie'] = "io=$id"; 213 | }); */ 214 | 215 | $transport->onRequest($req); 216 | 217 | $this->clients[$id] = $socket; 218 | $socket->once('close', array($this, 'onSocketClose')); 219 | $this->emit('connection', $socket); 220 | } 221 | 222 | public function onSocketClose($id) 223 | { 224 | unset($this->clients[$id]); 225 | } 226 | 227 | public function attach($worker) 228 | { 229 | $this->server = $worker; 230 | } 231 | 232 | public function onConnect($connection) 233 | { 234 | $connection->onRequest = array($this, 'handleRequest'); 235 | $connection->protocol = 'PHPSocketIO\Engine\Protocols\SocketIO'; 236 | $connection->onWebSocketConnect = array($this, 'onWebSocketConnect'); 237 | // clean 238 | $connection->onClose = function($connection) 239 | { 240 | if(!empty($connection->httpRequest)) 241 | { 242 | $connection->httpRequest->destroy(); 243 | $connection->httpRequest = null; 244 | } 245 | if(!empty($connection->httpResponse)) 246 | { 247 | $connection->httpResponse->destroy(); 248 | $connection->httpResponse = null; 249 | } 250 | if(!empty($connection->onRequest)) 251 | { 252 | $connection->onRequest = null; 253 | } 254 | if(!empty($connection->onWebSocketConnect)) 255 | { 256 | $connection->onWebSocketConnect = null; 257 | } 258 | }; 259 | } 260 | 261 | public function onWebSocketConnect($connection, $req, $res) 262 | { 263 | $this->prepare($req); 264 | $this->verify($req, $res, true, array($this, 'dealWebSocketConnect')); 265 | } 266 | 267 | public function dealWebSocketConnect($err, $success, $req, $res) 268 | { 269 | if (!$success) 270 | { 271 | self::sendErrorMessage($req, $res, $err); 272 | return; 273 | } 274 | 275 | 276 | if(isset($req->_query['sid'])) 277 | { 278 | if(!isset($this->clients[$req->_query['sid']])) 279 | { 280 | self::sendErrorMessage($req, $res, 'upgrade attempt for closed client'); 281 | return; 282 | } 283 | $client = $this->clients[$req->_query['sid']]; 284 | if($client->upgrading) 285 | { 286 | self::sendErrorMessage($req, $res, 'transport has already been trying to upgrade'); 287 | return; 288 | } 289 | if($client->upgraded) 290 | { 291 | self::sendErrorMessage($req, $res, 'transport had already been upgraded'); 292 | return; 293 | } 294 | $transport = new WebSocket($req); 295 | $client->maybeUpgrade($transport); 296 | } 297 | else 298 | { 299 | $this->handshake($req->_query['transport'], $req); 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /phpsocketio/Engine/Protocols/WebSocket/RFC6455.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | namespace PHPSocketIO\Engine\Protocols\WebSocket; 15 | 16 | 17 | 18 | /** 19 | * WebSocket 协议服务端解包和打包 20 | */ 21 | class RFC6455 22 | { 23 | /** 24 | * websocket头部最小长度 25 | * @var int 26 | */ 27 | const MIN_HEAD_LEN = 6; 28 | 29 | /** 30 | * websocket blob类型 31 | * @var char 32 | */ 33 | const BINARY_TYPE_BLOB = "\x81"; 34 | 35 | /** 36 | * websocket arraybuffer类型 37 | * @var char 38 | */ 39 | const BINARY_TYPE_ARRAYBUFFER = "\x82"; 40 | 41 | /** 42 | * 检查包的完整性 43 | * @param string $buffer 44 | */ 45 | public static function input($buffer, $connection) 46 | { 47 | // 数据长度 48 | $recv_len = strlen($buffer); 49 | // 长度不够 50 | if($recv_len < self::MIN_HEAD_LEN) 51 | { 52 | return 0; 53 | } 54 | 55 | // $connection->websocketCurrentFrameLength有值说明当前fin为0,则缓冲websocket帧数据 56 | if($connection->websocketCurrentFrameLength) 57 | { 58 | // 如果当前帧数据未收全,则继续收 59 | if($connection->websocketCurrentFrameLength > $recv_len) 60 | { 61 | // 返回0,因为不清楚完整的数据包长度,需要等待fin=1的帧 62 | return 0; 63 | } 64 | } 65 | else 66 | { 67 | $data_len = ord($buffer[1]) & 127; 68 | $firstbyte = ord($buffer[0]); 69 | $is_fin_frame = $firstbyte>>7; 70 | $opcode = $firstbyte & 0xf; 71 | switch($opcode) 72 | { 73 | // 附加数据帧 @todo 实现附加数据帧 74 | case 0x0: 75 | break; 76 | // 文本数据帧 77 | case 0x1: 78 | break; 79 | // 二进制数据帧 80 | case 0x2: 81 | break; 82 | // 关闭的包 83 | case 0x8: 84 | // 如果有设置onWebSocketClose回调,尝试执行 85 | if(isset($connection->onWebSocketClose)) 86 | { 87 | call_user_func($connection->onWebSocketClose, $connection); 88 | } 89 | // 默认行为是关闭连接 90 | else 91 | { 92 | $connection->close(); 93 | } 94 | return 0; 95 | // ping的包 96 | case 0x9: 97 | // 如果有设置onWebSocketPing回调,尝试执行 98 | if(isset($connection->onWebSocketPing)) 99 | { 100 | call_user_func($connection->onWebSocketPing, $connection); 101 | } 102 | // 默认发送pong 103 | else 104 | { 105 | $connection->send(pack('H*', '8a00'), true); 106 | } 107 | // 从接受缓冲区中消费掉该数据包 108 | if(!$data_len) 109 | { 110 | $connection->consumeRecvBuffer(self::MIN_HEAD_LEN); 111 | return 0; 112 | } 113 | break; 114 | // pong的包 115 | case 0xa: 116 | // 如果有设置onWebSocketPong回调,尝试执行 117 | if(isset($connection->onWebSocketPong)) 118 | { 119 | call_user_func($connection->onWebSocketPong, $connection); 120 | } 121 | // 从接受缓冲区中消费掉该数据包 122 | if(!$data_len) 123 | { 124 | $connection->consumeRecvBuffer(self::MIN_HEAD_LEN); 125 | return 0; 126 | } 127 | break; 128 | // 错误的opcode 129 | default : 130 | echo "error opcode $opcode and close websocket connection\n"; 131 | $connection->close(); 132 | return 0; 133 | } 134 | 135 | // websocket二进制数据 136 | $head_len = self::MIN_HEAD_LEN; 137 | if ($data_len === 126) { 138 | $head_len = 8; 139 | if($head_len > $recv_len) 140 | { 141 | return 0; 142 | } 143 | $pack = unpack('ntotal_len', substr($buffer, 2, 2)); 144 | $data_len = $pack['total_len']; 145 | } else if ($data_len === 127) { 146 | $head_len = 14; 147 | if($head_len > $recv_len) 148 | { 149 | return 0; 150 | } 151 | $arr = unpack('N2', substr($buffer, 2, 8)); 152 | $data_len = $arr[1]*4294967296 + $arr[2]; 153 | } 154 | $current_frame_length = $head_len + $data_len; 155 | if($is_fin_frame) 156 | { 157 | return $current_frame_length; 158 | } 159 | else 160 | { 161 | $connection->websocketCurrentFrameLength = $current_frame_length; 162 | } 163 | } 164 | 165 | // 收到的数据刚好是一个frame 166 | if($connection->websocketCurrentFrameLength == $recv_len) 167 | { 168 | self::decode($buffer, $connection); 169 | $connection->consumeRecvBuffer($connection->websocketCurrentFrameLength); 170 | $connection->websocketCurrentFrameLength = 0; 171 | return 0; 172 | } 173 | // 收到的数据大于一个frame 174 | elseif($connection->websocketCurrentFrameLength < $recv_len) 175 | { 176 | self::decode(substr($buffer, 0, $connection->websocketCurrentFrameLength), $connection); 177 | $connection->consumeRecvBuffer($connection->websocketCurrentFrameLength); 178 | $current_frame_length = $connection->websocketCurrentFrameLength; 179 | $connection->websocketCurrentFrameLength = 0; 180 | // 继续读取下一个frame 181 | return self::input(substr($buffer, $current_frame_length), $connection); 182 | } 183 | // 收到的数据不足一个frame 184 | else 185 | { 186 | return 0; 187 | } 188 | } 189 | 190 | /** 191 | * 打包 192 | * @param string $buffer 193 | * @return string 194 | */ 195 | public static function encode($buffer, $connection) 196 | { 197 | $len = strlen($buffer); 198 | if(empty($connection->websocketHandshake)) 199 | { 200 | // 默认是utf8文本格式 201 | $connection->websocketType = self::BINARY_TYPE_BLOB; 202 | } 203 | 204 | $first_byte = $connection->websocketType; 205 | 206 | if($len<=125) 207 | { 208 | $encode_buffer = $first_byte.chr($len).$buffer; 209 | } 210 | else if($len<=65535) 211 | { 212 | $encode_buffer = $first_byte.chr(126).pack("n", $len).$buffer; 213 | } 214 | else 215 | { 216 | $encode_buffer = $first_byte.chr(127).pack("xxxxN", $len).$buffer; 217 | } 218 | 219 | // 还没握手不能发数据,先将数据缓冲起来,等握手完毕后发送 220 | if(empty($connection->websocketHandshake)) 221 | { 222 | if(empty($connection->websocketTmpData)) 223 | { 224 | // 临时数据缓冲 225 | $connection->websocketTmpData = ''; 226 | } 227 | $connection->websocketTmpData .= $encode_buffer; 228 | // 返回空,阻止发送 229 | return ''; 230 | } 231 | 232 | return $encode_buffer; 233 | } 234 | 235 | /** 236 | * 解包 237 | * @param string $buffer 238 | * @return string 239 | */ 240 | public static function decode($buffer, $connection) 241 | { 242 | $len = $masks = $data = $decoded = null; 243 | $len = ord($buffer[1]) & 127; 244 | if ($len === 126) { 245 | $masks = substr($buffer, 4, 4); 246 | $data = substr($buffer, 8); 247 | } else if ($len === 127) { 248 | $masks = substr($buffer, 10, 4); 249 | $data = substr($buffer, 14); 250 | } else { 251 | $masks = substr($buffer, 2, 4); 252 | $data = substr($buffer, 6); 253 | } 254 | for ($index = 0; $index < strlen($data); $index++) { 255 | $decoded .= $data[$index] ^ $masks[$index % 4]; 256 | } 257 | if($connection->websocketCurrentFrameLength) 258 | { 259 | $connection->websocketDataBuffer .= $decoded; 260 | return $connection->websocketDataBuffer; 261 | } 262 | else 263 | { 264 | $decoded = $connection->websocketDataBuffer . $decoded; 265 | $connection->websocketDataBuffer = ''; 266 | return $decoded; 267 | } 268 | } 269 | 270 | /** 271 | * 处理websocket握手 272 | * @param string $buffer 273 | * @param TcpConnection $connection 274 | * @return int 275 | */ 276 | public static function dealHandshake($connection, $req, $res) 277 | { 278 | $headers = array(); 279 | if(isset($connection->onWebSocketConnect)) 280 | { 281 | try 282 | { 283 | call_user_func_array($connection->onWebSocketConnect, array($connection, $req, $res)); 284 | } 285 | catch (\Exception $e) 286 | { 287 | echo $e; 288 | } 289 | if(!$res->writable) 290 | { 291 | return false; 292 | } 293 | } 294 | 295 | if(isset($req->headers['sec-websocket-key'])) 296 | { 297 | $sec_websocket_key = $req->headers['sec-websocket-key']; 298 | } 299 | else 300 | { 301 | $res->writeHead(400); 302 | $res->end('400 Bad Request
Upgrade to websocket but Sec-WebSocket-Key not found.'); 303 | return 0; 304 | } 305 | 306 | // 标记已经握手 307 | $connection->websocketHandshake = true; 308 | // 缓冲fin为0的包,直到fin为1 309 | $connection->websocketDataBuffer = ''; 310 | // 当前数据帧的长度,可能是fin为0的帧,也可能是fin为1的帧 311 | $connection->websocketCurrentFrameLength = 0; 312 | // 当前帧的数据缓冲 313 | $connection->websocketCurrentFrameBuffer = ''; 314 | // blob or arraybuffer 315 | $connection->websocketType = self::BINARY_TYPE_BLOB; 316 | 317 | $sec_websocket_accept = base64_encode(sha1($sec_websocket_key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11',true)); 318 | $headers['Content-Length'] = 0; 319 | $headers['Upgrade'] = 'websocket'; 320 | $headers['Sec-WebSocket-Version'] = 13; 321 | $headers['Connection'] = 'Upgrade'; 322 | $headers['Sec-WebSocket-Accept'] = $sec_websocket_accept; 323 | $res->writeHead(101, '', $headers); 324 | $res->end(); 325 | 326 | // 握手后有数据要发送 327 | if(!empty($connection->websocketTmpData)) 328 | { 329 | $connection->send($connection->websocketTmpData, true); 330 | $connection->websocketTmpData = ''; 331 | } 332 | 333 | return 0; 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /phpsocketio/Socket.php: -------------------------------------------------------------------------------- 1 | 'error', 24 | 'connect' => 'connect', 25 | 'disconnect' => 'disconnect', 26 | 'newListener' => 'newListener', 27 | 'removeListener' => 'removeListener' 28 | ); 29 | 30 | public static $flagsMap = array( 31 | 'json' => 'json', 32 | 'volatile' => 'volatile', 33 | 'broadcast' => 'broadcast' 34 | ); 35 | 36 | public function __construct($nsp, $client) 37 | { 38 | $this->nsp = $nsp; 39 | $this->server = $nsp->server; 40 | $this->adapter = $this->nsp->adapter; 41 | $this->id = ($nsp->name !== '/') ? $nsp->name .'#' .$client->id : $client->id; 42 | $this->request = $client->request; 43 | $this->client = $client; 44 | $this->conn = $client->conn; 45 | $this->handshake = $this->buildHandshake(); 46 | Debug::debug('IO Socket __construct'); 47 | } 48 | 49 | public function __destruct() 50 | { 51 | Debug::debug('IO Socket __destruct'); 52 | } 53 | 54 | public function buildHandshake() 55 | { 56 | //todo check this->request->_query 57 | $info = !empty($this->request->url) ? parse_url($this->request->url) : array(); 58 | $query = array(); 59 | if(isset($info['query'])) 60 | { 61 | parse_str($info['query'], $query); 62 | } 63 | return array( 64 | 'headers' => isset($this->request->headers) ? $this->request->headers : array(), 65 | 'time'=> date('D M d Y H:i:s') . ' GMT', 66 | 'address'=> $this->conn->remoteAddress, 67 | 'xdomain'=> isset($this->request->headers['origin']), 68 | 'secure' => !empty($this->request->connection->encrypted), 69 | 'issued' => time(), 70 | 'url' => isset($this->request->url) ? $this->request->url : '', 71 | 'query' => $query, 72 | ); 73 | } 74 | 75 | public function __get($name) 76 | { 77 | if($name === 'broadcast') 78 | { 79 | $this->flags['broadcast'] = true; 80 | return $this; 81 | } 82 | return null; 83 | } 84 | 85 | public function emit($ev = null) 86 | { 87 | $args = func_get_args(); 88 | if (isset(self::$events[$ev])) 89 | { 90 | call_user_func_array(array(__CLASS__, 'parent::emit'), $args); 91 | } 92 | else 93 | { 94 | $packet = array(); 95 | // todo check 96 | //$packet['type'] = hasBin($args) ? Parser::BINARY_EVENT : Parser::EVENT; 97 | $packet['type'] = Parser::EVENT; 98 | $packet['data'] = $args; 99 | $flags = $this->flags; 100 | // access last argument to see if it's an ACK callback 101 | if (is_callable(end($args))) 102 | { 103 | if ($this->_rooms || isset($flags['broadcast'])) 104 | { 105 | throw new \Exception('Callbacks are not supported when broadcasting'); 106 | } 107 | echo('emitting packet with ack id ' . $this->nsp->ids); 108 | $this->acks[$this->nsp->ids] = array_pop($args); 109 | $packet['id'] = $this->nsp->ids++; 110 | } 111 | 112 | if ($this->_rooms || !empty($flags['broadcast'])) 113 | { 114 | $this->adapter->broadcast($packet, array( 115 | 'except' => array($this->id => $this->id), 116 | 'rooms'=> $this->_rooms, 117 | 'flags' => $flags 118 | )); 119 | } 120 | else 121 | { 122 | // dispatch packet 123 | $this->packet($packet); 124 | } 125 | 126 | // reset flags 127 | $this->_rooms = array(); 128 | $this->flags = array(); 129 | } 130 | return $this; 131 | } 132 | 133 | 134 | /** 135 | * Targets a room when broadcasting. 136 | * 137 | * @param {String} name 138 | * @return {Socket} self 139 | * @api public 140 | */ 141 | 142 | public function to($name) 143 | { 144 | if(!isset($this->_rooms[$name])) 145 | { 146 | $this->_rooms[$name] = $name; 147 | } 148 | return $this; 149 | } 150 | 151 | public function in($name) 152 | { 153 | return $this->to($name); 154 | } 155 | 156 | /** 157 | * Sends a `message` event. 158 | * 159 | * @return {Socket} self 160 | * @api public 161 | */ 162 | 163 | public function send() 164 | { 165 | $args = func_get_args(); 166 | array_unshift($args, 'message'); 167 | call_user_func_array(array($this, 'emit'), $args); 168 | return $this; 169 | } 170 | 171 | public function write() 172 | { 173 | $args = func_get_args(); 174 | array_unshift($args, 'message'); 175 | call_user_func_array(array($this, 'emit'), $args); 176 | return $this; 177 | } 178 | 179 | /** 180 | * Writes a packet. 181 | * 182 | * @param {Object} packet object 183 | * @param {Object} options 184 | * @api private 185 | */ 186 | 187 | public function packet($packet, $preEncoded = false) 188 | { 189 | if (!$this->nsp || !$this->client) return; 190 | $packet['nsp'] = $this->nsp->name; 191 | //$volatile = !empty(self::$flagsMap['volatile']); 192 | $volatile = false; 193 | $this->client->packet($packet, $preEncoded, $volatile); 194 | } 195 | 196 | /** 197 | * Joins a room. 198 | * 199 | * @param {String} room 200 | * @param {Function} optional, callback 201 | * @return {Socket} self 202 | * @api private 203 | */ 204 | 205 | public function join($room) 206 | { 207 | if(isset($this->rooms[$room])) return $this; 208 | $this->adapter->add($this->id, $room); 209 | $this->rooms[$room] = $room; 210 | return $this; 211 | } 212 | 213 | /** 214 | * Leaves a room. 215 | * 216 | * @param {String} room 217 | * @param {Function} optional, callback 218 | * @return {Socket} self 219 | * @api private 220 | */ 221 | 222 | public function leave($room) 223 | { 224 | $this->adapter->del($this->id, $room); 225 | unset($this->rooms[$room]); 226 | return $this; 227 | } 228 | 229 | /** 230 | * Leave all rooms. 231 | * 232 | * @api private 233 | */ 234 | 235 | public function leaveAll() 236 | { 237 | $this->adapter->delAll($this->id); 238 | $this->rooms = array(); 239 | } 240 | 241 | /** 242 | * Called by `Namespace` upon succesful 243 | * middleware execution (ie: authorization). 244 | * 245 | * @api private 246 | */ 247 | 248 | public function onconnect() 249 | { 250 | $this->nsp->connected[$this->id] = $this; 251 | $this->join($this->id); 252 | $this->packet(array( 253 | 'type' => Parser::CONNECT) 254 | ); 255 | } 256 | 257 | /** 258 | * Called with each packet. Called by `Client`. 259 | * 260 | * @param {Object} packet 261 | * @api private 262 | */ 263 | 264 | public function onpacket($packet) 265 | { 266 | switch ($packet['type']) 267 | { 268 | case Parser::EVENT: 269 | $this->onevent($packet); 270 | break; 271 | 272 | case Parser::BINARY_EVENT: 273 | $this->onevent($packet); 274 | break; 275 | 276 | case Parser::ACK: 277 | $this->onack($packet); 278 | break; 279 | 280 | case Parser::BINARY_ACK: 281 | $this->onack($packet); 282 | break; 283 | 284 | case Parser::DISCONNECT: 285 | $this->ondisconnect(); 286 | break; 287 | 288 | case Parser::ERROR: 289 | $this->emit('error', $packet['data']); 290 | } 291 | } 292 | 293 | /** 294 | * Called upon event packet. 295 | * 296 | * @param {Object} packet object 297 | * @api private 298 | */ 299 | 300 | public function onevent($packet) 301 | { 302 | $args = isset($packet['data']) ? $packet['data'] : array(); 303 | if (!empty($packet['id']) || (isset($packet['id']) && $packet['id'] === 0)) 304 | { 305 | $args[] = $this->ack($packet['id']); 306 | } 307 | call_user_func_array(array(__CLASS__, 'parent::emit'), $args); 308 | } 309 | 310 | /** 311 | * Produces an ack callback to emit with an event. 312 | * 313 | * @param {Number} packet id 314 | * @api private 315 | */ 316 | 317 | public function ack($id) 318 | { 319 | $self = $this; 320 | $sent = false; 321 | return function()use(&$sent, $id, $self){ 322 | // prevent double callbacks 323 | if ($sent) return; 324 | $args = func_get_args(); 325 | $type = $this->hasBin($args) ? Parser::BINARY_ACK : Parser::ACK; 326 | $self->packet(array( 327 | 'id' => $id, 328 | 'type' => $type, 329 | 'data' => $args 330 | )); 331 | }; 332 | } 333 | 334 | /** 335 | * Called upon ack packet. 336 | * 337 | * @api private 338 | */ 339 | 340 | public function onack($packet) 341 | { 342 | $ack = $this->acks[$packet['id']]; 343 | if (is_callable($ack)) 344 | { 345 | call_user_func($ack, $packet['data']); 346 | unset($this->acks[$packet['id']]); 347 | } else { 348 | echo ('bad ack '. packet.id); 349 | } 350 | } 351 | 352 | /** 353 | * Called upon client disconnect packet. 354 | * 355 | * @api private 356 | */ 357 | 358 | public function ondisconnect() 359 | { 360 | echo('got disconnect packet'); 361 | $this->onclose('client namespace disconnect'); 362 | } 363 | 364 | /** 365 | * Handles a client error. 366 | * 367 | * @api private 368 | */ 369 | 370 | public function onerror($err) 371 | { 372 | if ($this->listeners('error')) 373 | { 374 | $this->emit('error', $err); 375 | } 376 | else 377 | { 378 | //echo('Missing error handler on `socket`.'); 379 | } 380 | } 381 | 382 | /** 383 | * Called upon closing. Called by `Client`. 384 | * 385 | * @param {String} reason 386 | * @param {Error} optional error object 387 | * @api private 388 | */ 389 | 390 | public function onclose($reason) 391 | { 392 | if (!$this->connected) return $this; 393 | $this->emit('disconnect', $reason); 394 | $this->leaveAll(); 395 | $this->nsp->remove($this); 396 | $this->client->remove($this); 397 | $this->connected = false; 398 | $this->disconnected = true; 399 | unset($this->nsp->connected[$this->id]); 400 | // .... 401 | $this->nsp = null; 402 | $this->server = null; 403 | $this->adapter = null; 404 | $this->request = null; 405 | $this->client = null; 406 | $this->conn = null; 407 | $this->removeAllListeners(); 408 | } 409 | 410 | /** 411 | * Produces an `error` packet. 412 | * 413 | * @param {Object} error object 414 | * @api private 415 | */ 416 | 417 | public function error($err) 418 | { 419 | $this->packet(array( 420 | 'type' => Parser::ERROR, 'data' => $err ) 421 | ); 422 | } 423 | 424 | /** 425 | * Disconnects this client. 426 | * 427 | * @param {Boolean} if `true`, closes the underlying connection 428 | * @return {Socket} self 429 | * @api public 430 | */ 431 | 432 | public function disconnect( $close = false ) 433 | { 434 | if (!$this->connected) return $this; 435 | if ($close) 436 | { 437 | $this->client->disconnect(); 438 | } else { 439 | $this->packet(array( 440 | 'type'=> Parser::DISCONNECT 441 | )); 442 | $this->onclose('server namespace disconnect'); 443 | } 444 | return $this; 445 | } 446 | 447 | /** 448 | * Sets the compress flag. 449 | * 450 | * @param {Boolean} if `true`, compresses the sending data 451 | * @return {Socket} self 452 | * @api public 453 | */ 454 | 455 | public function compress($compress) 456 | { 457 | $this->flags['compress'] = $compress; 458 | return $this; 459 | } 460 | 461 | protected function hasBin($args) { 462 | $hasBin = false; 463 | 464 | array_walk_recursive($args, function($item, $key) use ($hasBin) { 465 | if (!ctype_print($item)) { 466 | $hasBin = true; 467 | } 468 | }); 469 | 470 | return $hasBin; 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /phpsocketio/Engine/Socket.php: -------------------------------------------------------------------------------- 1 | id = $id; 24 | $this->server = $server; 25 | $this->request = $req; 26 | $this->remoteAddress = $req->connection->getRemoteIp().':'.$req->connection->getRemotePort(); 27 | $this->setTransport($transport); 28 | $this->onOpen(); 29 | Debug::debug('Engine/Socket __construct'); 30 | } 31 | 32 | public function __destruct() 33 | { 34 | Debug::debug('Engine/Socket __destruct'); 35 | } 36 | 37 | public function maybeUpgrade($transport) 38 | { 39 | $this->upgrading = true; 40 | 41 | 42 | //SamLuo 43 | $this->upgradeTimeoutTimer =$this->server->server->tick($this->server->upgradeTimeout*1000, array($this, 'upgradeTimeoutCallback'),$transport); 44 | 45 | 46 | 47 | $this->upgradeTransport = $transport; 48 | $transport->on('packet', array($this, 'onUpgradePacket')); 49 | $transport->once('close', array($this, 'onUpgradeTransportClose')); 50 | $transport->once('error', array($this, 'onUpgradeTransportError')); 51 | $this->once('close', array($this, 'onUpgradeTransportClose')); 52 | } 53 | 54 | public function onUpgradePacket($packet) 55 | { 56 | if(empty($this->upgradeTransport)) 57 | { 58 | $this->onError('upgradeTransport empty'); 59 | return; 60 | } 61 | if('ping' === $packet['type'] && (isset($packet['data']) && 'probe' === $packet['data'])) 62 | { 63 | $this->upgradeTransport->send(array(array('type'=> 'pong', 'data'=> 'probe'))); 64 | //$this->transport->shouldClose = function(){}; 65 | if ($this->checkIntervalTimer) { 66 | //SamLuo 67 | $this->server->server->clearTimer($this->checkIntervalTimer); 68 | } 69 | // $this->checkIntervalTimer = Timer::add(0.5, array($this, 'check')); 70 | // 71 | //SamLuo 72 | $this->checkIntervalTimer =$this->server->server->tick(500, array($this, 'check')); 73 | } 74 | else if('upgrade' === $packet['type'] && $this->readyState !== 'closed') 75 | { 76 | $this->upgradeCleanup(); 77 | $this->upgraded = true; 78 | $this->clearTransport(); 79 | $this->transport->destroy(); 80 | $this->setTransport($this->upgradeTransport); 81 | $this->emit('upgrade', $this->upgradeTransport); 82 | $this->upgradeTransport = null; 83 | $this->setPingTimeout(); 84 | $this->flush(); 85 | if($this->readyState === 'closing') 86 | { 87 | $this->transport->close(array($this, 'onClose')); 88 | } 89 | } 90 | else 91 | { 92 | if(!empty($this->upgradeTransport)) 93 | { 94 | $this->upgradeCleanup(); 95 | $this->upgradeTransport->close(); 96 | $this->upgradeTransport = null; 97 | } 98 | } 99 | 100 | } 101 | 102 | 103 | public function upgradeCleanup() 104 | { 105 | $this->upgrading = false; 106 | //Timer::del($this->checkIntervalTimer); 107 | //Timer::del($this->upgradeTimeoutTimer); 108 | //SamLuo 109 | $this->checkIntervalTimer && $this->server->server->clearTimer($this->checkIntervalTimer); 110 | $this->checkIntervalTimer = NULL; 111 | $this->upgradeTimeoutTimer && $this->server->server->clearTimer($this->upgradeTimeoutTimer); 112 | $this->upgradeTimeoutTimer = NULL; 113 | if(!empty($this->upgradeTransport)) 114 | { 115 | $this->upgradeTransport->removeListener('packet', array($this, 'onUpgradePacket')); 116 | $this->upgradeTransport->removeListener('close', array($this, 'onUpgradeTransportClose')); 117 | $this->upgradeTransport->removeListener('error', array($this, 'onUpgradeTransportError')); 118 | } 119 | $this->removeListener('close', array($this, 'onUpgradeTransportClose')); 120 | } 121 | 122 | public function onUpgradeTransportClose() 123 | { 124 | $this->onUpgradeTransportError('transport closed'); 125 | } 126 | 127 | public function onUpgradeTransportError($err) 128 | { 129 | //echo $err; 130 | $this->upgradeCleanup(); 131 | if($this->upgradeTransport) 132 | { 133 | $this->upgradeTransport->close(); 134 | $this->upgradeTransport = null; 135 | } 136 | } 137 | 138 | public function upgradeTimeoutCallback($timer_id,$transport) 139 | { 140 | //SamLuo 141 | $this->server->server->clearTimer($timer_id); 142 | //echo("client did not complete upgrade - closing transport\n"); 143 | $this->upgradeCleanup(); 144 | if('open' === $transport->readyState) 145 | { 146 | $transport->close(); 147 | } 148 | } 149 | 150 | public function setTransport($transport) 151 | { 152 | $this->transport = $transport; 153 | $this->transport->once('error', array($this, 'onError')); 154 | $this->transport->on('packet', array($this, 'onPacket')); 155 | $this->transport->on('drain', array($this, 'flush')); 156 | $this->transport->once('close', array($this, 'onClose')); 157 | //this function will manage packet events (also message callbacks) 158 | $this->setupSendCallback(); 159 | } 160 | 161 | public function onOpen() 162 | { 163 | $this->readyState = 'open'; 164 | 165 | // sends an `open` packet 166 | $this->transport->sid = $this->id; 167 | $this->sendPacket('open', json_encode(array( 168 | 'sid'=> $this->id 169 | , 'upgrades' => $this->getAvailableUpgrades() 170 | , 'pingInterval'=> $this->server->pingInterval*1000 171 | , 'pingTimeout'=> $this->server->pingTimeout*1000 172 | ))); 173 | 174 | $this->emit('open'); 175 | $this->setPingTimeout(); 176 | } 177 | 178 | public function onPacket($packet) 179 | { 180 | if ('open' === $this->readyState) { 181 | // export packet event 182 | $this->emit('packet', $packet); 183 | 184 | // Reset ping timeout on any packet, incoming data is a good sign of 185 | // other side's liveness 186 | $this->setPingTimeout(); 187 | switch ($packet['type']) { 188 | 189 | case 'ping': 190 | $this->sendPacket('pong'); 191 | $this->emit('heartbeat'); 192 | break; 193 | 194 | case 'error': 195 | $this->onClose('parse error'); 196 | break; 197 | 198 | case 'message': 199 | $this->emit('data', $packet['data']); 200 | $this->emit('message', $packet['data']); 201 | break; 202 | } 203 | } 204 | else 205 | { 206 | echo('packet received with closed socket'); 207 | } 208 | } 209 | 210 | public function check() 211 | { 212 | if('polling' == $this->transport->name && $this->transport->writable) 213 | { 214 | $this->transport->send(array(array('type' => 'noop'))); 215 | } 216 | } 217 | 218 | public function onError($err) 219 | { 220 | $this->onClose('transport error', $err); 221 | } 222 | 223 | public function setPingTimeout() 224 | { 225 | if ($this->pingTimeoutTimer) { 226 | $this->server->server->clearTimer($this->pingTimeoutTimer); 227 | } 228 | //SamLuo 229 | $this->pingTimeoutTimer = $this->server->server->after(($this->server->pingInterval + $this->server->pingTimeout)*1000 , array($this, 'pingTimeoutCallback')); 230 | } 231 | 232 | public function pingTimeoutCallback() 233 | { 234 | $this->transport && $this->transport->close(); 235 | $this->onClose('ping timeout'); 236 | } 237 | 238 | 239 | public function clearTransport() 240 | { 241 | $this->transport->close(); 242 | } 243 | 244 | public function onClose($reason = '', $description = null) 245 | { 246 | if ('closed' !== $this->readyState) 247 | { 248 | //Timer::del($this->checkIntervalTimer); 249 | $this->checkIntervalTimer && $this->server->server->clearTimer($this->checkIntervalTimer); 250 | $this->checkIntervalTimer = null; 251 | // Timer::del($this->upgradeTimeoutTimer); 252 | //SamLuo 253 | $this->upgradeTimeoutTimer && $this->server->server->clearTimer($this->upgradeTimeoutTimer); 254 | $this->upgradeTimeoutTimer = null; 255 | // clean writeBuffer in next tick, so developers can still 256 | // grab the writeBuffer on 'close' event 257 | $this->writeBuffer = array(); 258 | $this->packetsFn = array(); 259 | $this->sentCallbackFn = array(); 260 | $this->clearTransport(); 261 | $this->readyState = 'closed'; 262 | $this->emit('close', $this->id, $reason, $description); 263 | $this->server = null; 264 | $this->request = null; 265 | $this->upgradeTransport = null; 266 | $this->removeAllListeners(); 267 | if(!empty($this->transport)) 268 | { 269 | $this->transport->removeAllListeners(); 270 | $this->transport = null; 271 | } 272 | } 273 | } 274 | 275 | public function send($data, $options, $callback) 276 | { 277 | $this->sendPacket('message', $data, $options, $callback); 278 | return $this; 279 | } 280 | 281 | public function write($data, $options = array(), $callback = null) 282 | { 283 | return $this->send($data, $options, $callback); 284 | } 285 | 286 | public function sendPacket($type, $data = null, $callback = null) 287 | { 288 | if('closing' !== $this->readyState) 289 | { 290 | $packet = array( 291 | 'type'=> $type 292 | ); 293 | if($data !== null) 294 | { 295 | $packet['data'] = $data; 296 | } 297 | // exports packetCreate event 298 | $this->emit('packetCreate', $packet); 299 | $this->writeBuffer[] = $packet; 300 | //add send callback to object 301 | if($callback) 302 | { 303 | $this->packetsFn[] = $callback; 304 | } 305 | $this->flush(); 306 | } 307 | } 308 | 309 | public function flush() 310 | { 311 | if ('closed' !== $this->readyState && $this->transport->writable 312 | && $this->writeBuffer) 313 | { 314 | $this->emit('flush', $this->writeBuffer); 315 | $this->server->emit('flush', $this, $this->writeBuffer); 316 | $wbuf = $this->writeBuffer; 317 | $this->writeBuffer = array(); 318 | if($this->packetsFn) 319 | { 320 | if(!empty($this->transport->supportsFraming)) 321 | { 322 | $this->sentCallbackFn[] = $this->packetsFn; 323 | } 324 | else 325 | { 326 | // @todo check 327 | $this->sentCallbackFn[]=$this->packetsFn; 328 | } 329 | } 330 | $this->packetsFn = array(); 331 | $this->transport->send($wbuf); 332 | $this->emit('drain'); 333 | if($this->server) 334 | { 335 | $this->server->emit('drain', $this); 336 | } 337 | } 338 | } 339 | 340 | public function getAvailableUpgrades() 341 | { 342 | return array('websocket'); 343 | } 344 | 345 | public function close() 346 | { 347 | if ('open' !== $this->readyState) 348 | { 349 | return; 350 | } 351 | 352 | $this->readyState = 'closing'; 353 | 354 | if ($this->writeBuffer) { 355 | $this->once('drain', array($this, 'closeTransport')); 356 | return; 357 | } 358 | 359 | $this->closeTransport(); 360 | } 361 | 362 | public function closeTransport() 363 | { 364 | //todo onClose.bind(this, 'forced close')); 365 | $this->transport->close(array($this, 'onClose')); 366 | } 367 | 368 | public function setupSendCallback() 369 | { 370 | $self = $this; 371 | //the message was sent successfully, execute the callback 372 | $this->transport->on('drain', array($this, 'onDrainCallback')); 373 | } 374 | 375 | public function onDrainCallback() 376 | { 377 | if ($this->sentCallbackFn) 378 | { 379 | $seqFn = array_shift($this->sentCallbackFn); 380 | if(is_callable($seqFn)) 381 | { 382 | echo('executing send callback'); 383 | call_user_func($seqFn, $this->transport); 384 | }else if (is_array($seqFn)) { 385 | echo('executing batch send callback'); 386 | foreach($seqFn as $fn) 387 | { 388 | call_user_func($fn, $this->transport); 389 | } 390 | } 391 | } 392 | } 393 | } 394 | --------------------------------------------------------------------------------