├── README.md ├── composer.json ├── docs ├── README.md └── zh │ └── README.md ├── examples └── chat │ ├── README.md │ ├── public │ ├── index.html │ ├── jquery.min.js │ ├── main.js │ ├── socket.io-client │ │ ├── LICENSE │ │ ├── lib │ │ │ ├── index.js │ │ │ ├── manager.js │ │ │ ├── on.js │ │ │ ├── socket.js │ │ │ └── url.js │ │ └── socket.io.js │ └── style.css │ ├── start_for_win.bat │ ├── start_io.php │ └── start_web.php ├── src ├── ChannelAdapter.php ├── Client.php ├── Debug.php ├── DefaultAdapter.php ├── Engine │ ├── Engine.php │ ├── Parser.php │ ├── Protocols │ │ ├── Http │ │ │ ├── Request.php │ │ │ └── Response.php │ │ ├── SocketIO.php │ │ ├── WebSocket.php │ │ └── WebSocket │ │ │ └── RFC6455.php │ ├── Socket.php │ ├── Transport.php │ └── Transports │ │ ├── Polling.php │ │ ├── PollingJsonp.php │ │ ├── PollingXHR.php │ │ └── WebSocket.php ├── Event │ └── Emitter.php ├── Nsp.php ├── Parser │ ├── Decoder.php │ ├── Encoder.php │ └── Parser.php ├── Socket.php ├── SocketIO.php └── autoload.php └── tests └── emitter.php /README.md: -------------------------------------------------------------------------------- 1 | # phpsocket.io for win 2 | 3 | # Install 4 | 5 | composer require workerman/phpsocket.io-for-win 6 | 7 | 8 | # Run example 9 | 10 | Enter the examples/chat/ directory, and then double-click start_for_win.bat. 11 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "workerman/phpsocket.io-for-win", 3 | "type" : "project", 4 | "keywords": ["socket.io"], 5 | "homepage": "http://www.workerman.net", 6 | "license" : "MIT", 7 | "require": { 8 | "workerman/workerman-for-win" : ">=3.1.8" 9 | }, 10 | "autoload": { 11 | "psr-4": {"PHPSocketIO\\": "./src"} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Documentation 2 | 3 | [中文](./zh/) 4 | 5 | [English](./en/) 6 | -------------------------------------------------------------------------------- /docs/zh/README.md: -------------------------------------------------------------------------------- 1 | ## 安装 2 | 请使用composer集成phpsocket.io。 3 | 4 | 脚本中引用vendor中的autoload.php实现SocketIO相关类的加载。例如 5 | ```php 6 | require_once '/你的vendor路径/autoload.php'; 7 | ``` 8 | 9 | ### 服务端和客户端连接 10 | 创建一个SocketIO服务端 11 | ```php 12 | on('connection', function($connection)use($io){ 21 | echo "new connection coming\n"; 22 | }); 23 | 24 | Worker::runAll(); 25 | ``` 26 | 客户端 27 | ```javascript 28 | 29 | 37 | ``` 38 | 39 | ### 自定义事件 40 | socket.io主要是通过事件来进行通讯交互的。 41 | 42 | 除了自带的connect,message,disconnect三个事件以外,在服务端和客户端用户可以自定义事件。 43 | 44 | 服务端和客户端都通过emit方法触发对端的事件。 45 | 46 | 例如下面的代码在服务端定义了一个```chat message```事件,事件参数为```$msg```。 47 | ```php 48 | on('connection', function($connection)use($io){ 56 | // 定义chat message事件回调函数 57 | $connection->on('chat message', function($msg)use($io){ 58 | // 触发所有客户端定义的chat message from server事件 59 | $io->emit('chat message from server', $msg); 60 | }); 61 | }); 62 | ``` 63 | 64 | 客户端通过下面的方法触发服务端的chat message事件。 65 | ```javascript 66 | 67 | 77 | ``` 78 | 79 | ## 分组 80 | socket.io提供分组功能,允许向某个分组发送事件,例如向某个房间广播数据。 81 | 82 | 1、加入分组(一个连接可以加入多个分组) 83 | ```php 84 | $connection->join('group name'); 85 | ``` 86 | 2、离开分组(连接断开时会自动从分组中离开) 87 | ```php 88 | $connection->leave('group name'); 89 | ``` 90 | 91 | ## 向客户端发送事件的各种方法 92 | $io是SocketIO对象。$connection是客户端连接 93 | 94 | $data可以是数字和字符串,也可以是数组。当$data是数组时,客户端会自动转换为javascript对象。 95 | 96 | 同理如果客户端向服务端emit某个事件传递的是一个javascript对象,在服务端接收时会自动转换为php数组。 97 | 98 | 1、向当前客户端发送事件 99 | ```php 100 | $connection->emit('event name', $data); 101 | ``` 102 | 2、向所有客户端发送事件 103 | ```php 104 | $io->emit('event name', $data); 105 | ``` 106 | 3、向所有客户端发送事件,但不包括当前连接。 107 | ```php 108 | $connection->broadcast->emit('event name', $data); 109 | ``` 110 | 4、向某个分组的所有客户端发送事件 111 | ```php 112 | $io->to('group name')->emit('event name', $data); 113 | ``` 114 | 115 | ## 获取客户端ip 116 | ```php 117 | $io->on('connection', function($socket)use($io){ 118 | var_dump($socket->conn->remoteAddress); 119 | }); 120 | ``` 121 | 122 | 123 | -------------------------------------------------------------------------------- /examples/chat/README.md: -------------------------------------------------------------------------------- 1 | # For chat demo 2 | ## start 3 | 4 | double-click start_for_win.bat 5 | 6 | visit http://127.0.0.1:2022 7 | -------------------------------------------------------------------------------- /examples/chat/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Socket.IO Chat Example 6 | 7 | 8 | 9 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/chat/public/main.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var FADE_TIME = 150; // ms 3 | var TYPING_TIMER_LENGTH = 400; // ms 4 | var COLORS = [ 5 | '#e21400', '#91580f', '#f8a700', '#f78b00', 6 | '#58dc00', '#287b00', '#a8f07a', '#4ae8c4', 7 | '#3b88eb', '#3824aa', '#a700ff', '#d300e7' 8 | ]; 9 | 10 | // Initialize varibles 11 | var $window = $(window); 12 | var $usernameInput = $('.usernameInput'); // Input for username 13 | var $messages = $('.messages'); // Messages area 14 | var $inputMessage = $('.inputMessage'); // Input message input box 15 | 16 | var $loginPage = $('.login.page'); // The login page 17 | var $chatPage = $('.chat.page'); // The chatroom page 18 | 19 | // Prompt for setting a username 20 | var username; 21 | var connected = false; 22 | var typing = false; 23 | var lastTypingTime; 24 | var $currentInput = $usernameInput.focus(); 25 | 26 | var socket = io('http://'+document.domain+':2020'); 27 | 28 | function addParticipantsMessage (data) { 29 | var message = ''; 30 | if (data.numUsers === 1) { 31 | message += "there's 1 participant"; 32 | } else { 33 | message += "there are " + data.numUsers + " participants"; 34 | } 35 | log(message); 36 | } 37 | 38 | // Sets the client's username 39 | function setUsername () { 40 | username = cleanInput($usernameInput.val().trim()); 41 | 42 | // If the username is valid 43 | if (username) { 44 | $loginPage.fadeOut(); 45 | $chatPage.show(); 46 | $loginPage.off('click'); 47 | $currentInput = $inputMessage.focus(); 48 | 49 | // Tell the server your username 50 | socket.emit('add user', username); 51 | } 52 | } 53 | 54 | // Sends a chat message 55 | function sendMessage () { 56 | var message = $inputMessage.val(); 57 | // Prevent markup from being injected into the message 58 | message = cleanInput(message); 59 | // if there is a non-empty message and a socket connection 60 | if (message && connected) { 61 | $inputMessage.val(''); 62 | addChatMessage({ 63 | username: username, 64 | message: message 65 | }); 66 | // tell server to execute 'new message' and send along one parameter 67 | socket.emit('new message', message); 68 | } 69 | } 70 | 71 | // Log a message 72 | function log (message, options) { 73 | var $el = $('
  • ').addClass('log').text(message); 74 | addMessageElement($el, options); 75 | } 76 | 77 | // Adds the visual chat message to the message list 78 | function addChatMessage (data, options) { 79 | // Don't fade the message in if there is an 'X was typing' 80 | var $typingMessages = getTypingMessages(data); 81 | options = options || {}; 82 | if ($typingMessages.length !== 0) { 83 | options.fade = false; 84 | $typingMessages.remove(); 85 | } 86 | 87 | var $usernameDiv = $('') 88 | .text(data.username) 89 | .css('color', getUsernameColor(data.username)); 90 | var $messageBodyDiv = $('') 91 | .text(data.message); 92 | 93 | var typingClass = data.typing ? 'typing' : ''; 94 | var $messageDiv = $('
  • ') 95 | .data('username', data.username) 96 | .addClass(typingClass) 97 | .append($usernameDiv, $messageBodyDiv); 98 | 99 | addMessageElement($messageDiv, options); 100 | } 101 | 102 | // Adds the visual chat typing message 103 | function addChatTyping (data) { 104 | data.typing = true; 105 | data.message = 'is typing'; 106 | addChatMessage(data); 107 | } 108 | 109 | // Removes the visual chat typing message 110 | function removeChatTyping (data) { 111 | getTypingMessages(data).fadeOut(function () { 112 | $(this).remove(); 113 | }); 114 | } 115 | 116 | // Adds a message element to the messages and scrolls to the bottom 117 | // el - The element to add as a message 118 | // options.fade - If the element should fade-in (default = true) 119 | // options.prepend - If the element should prepend 120 | // all other messages (default = false) 121 | function addMessageElement (el, options) { 122 | var $el = $(el); 123 | 124 | // Setup default options 125 | if (!options) { 126 | options = {}; 127 | } 128 | if (typeof options.fade === 'undefined') { 129 | options.fade = true; 130 | } 131 | if (typeof options.prepend === 'undefined') { 132 | options.prepend = false; 133 | } 134 | 135 | // Apply options 136 | if (options.fade) { 137 | $el.hide().fadeIn(FADE_TIME); 138 | } 139 | if (options.prepend) { 140 | $messages.prepend($el); 141 | } else { 142 | $messages.append($el); 143 | } 144 | $messages[0].scrollTop = $messages[0].scrollHeight; 145 | } 146 | 147 | // Prevents input from having injected markup 148 | function cleanInput (input) { 149 | return $('
    ').text(input).text(); 150 | } 151 | 152 | // Updates the typing event 153 | function updateTyping () { 154 | if (connected) { 155 | if (!typing) { 156 | typing = true; 157 | socket.emit('typing'); 158 | } 159 | lastTypingTime = (new Date()).getTime(); 160 | 161 | setTimeout(function () { 162 | var typingTimer = (new Date()).getTime(); 163 | var timeDiff = typingTimer - lastTypingTime; 164 | if (timeDiff >= TYPING_TIMER_LENGTH && typing) { 165 | socket.emit('stop typing'); 166 | typing = false; 167 | } 168 | }, TYPING_TIMER_LENGTH); 169 | } 170 | } 171 | 172 | // Gets the 'X is typing' messages of a user 173 | function getTypingMessages (data) { 174 | return $('.typing.message').filter(function (i) { 175 | return $(this).data('username') === data.username; 176 | }); 177 | } 178 | 179 | // Gets the color of a username through our hash function 180 | function getUsernameColor (username) { 181 | // Compute hash code 182 | var hash = 7; 183 | for (var i = 0; i < username.length; i++) { 184 | hash = username.charCodeAt(i) + (hash << 5) - hash; 185 | } 186 | // Calculate color 187 | var index = Math.abs(hash % COLORS.length); 188 | return COLORS[index]; 189 | } 190 | 191 | // Keyboard events 192 | 193 | $window.keydown(function (event) { 194 | // Auto-focus the current input when a key is typed 195 | if (!(event.ctrlKey || event.metaKey || event.altKey)) { 196 | $currentInput.focus(); 197 | } 198 | // When the client hits ENTER on their keyboard 199 | if (event.which === 13) { 200 | if (username) { 201 | sendMessage(); 202 | socket.emit('stop typing'); 203 | typing = false; 204 | } else { 205 | setUsername(); 206 | } 207 | } 208 | }); 209 | 210 | $inputMessage.on('input', function() { 211 | updateTyping(); 212 | }); 213 | 214 | // Click events 215 | 216 | // Focus input when clicking anywhere on login page 217 | $loginPage.click(function () { 218 | $currentInput.focus(); 219 | }); 220 | 221 | // Focus input when clicking on the message input's border 222 | $inputMessage.click(function () { 223 | $inputMessage.focus(); 224 | }); 225 | 226 | // Socket events 227 | 228 | // Whenever the server emits 'login', log the login message 229 | socket.on('login', function (data) { 230 | connected = true; 231 | // Display the welcome message 232 | var message = "Welcome to Socket.IO Chat – "; 233 | log(message, { 234 | prepend: true 235 | }); 236 | addParticipantsMessage(data); 237 | }); 238 | 239 | // Whenever the server emits 'new message', update the chat body 240 | socket.on('new message', function (data) { 241 | addChatMessage(data); 242 | }); 243 | 244 | // Whenever the server emits 'user joined', log it in the chat body 245 | socket.on('user joined', function (data) { 246 | log(data.username + ' joined'); 247 | addParticipantsMessage(data); 248 | }); 249 | 250 | // Whenever the server emits 'user left', log it in the chat body 251 | socket.on('user left', function (data) { 252 | log(data.username + ' left'); 253 | addParticipantsMessage(data); 254 | removeChatTyping(data); 255 | }); 256 | 257 | // Whenever the server emits 'typing', show the typing message 258 | socket.on('typing', function (data) { 259 | addChatTyping(data); 260 | }); 261 | 262 | // Whenever the server emits 'stop typing', kill the typing message 263 | socket.on('stop typing', function (data) { 264 | removeChatTyping(data); 265 | }); 266 | }); 267 | -------------------------------------------------------------------------------- /examples/chat/public/socket.io-client/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Guillermo Rauch 4 | 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /examples/chat/public/socket.io-client/lib/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var url = require('./url'); 7 | var parser = require('socket.io-parser'); 8 | var Manager = require('./manager'); 9 | var debug = require('debug')('socket.io-client'); 10 | 11 | /** 12 | * Module exports. 13 | */ 14 | 15 | module.exports = exports = lookup; 16 | 17 | /** 18 | * Managers cache. 19 | */ 20 | 21 | var cache = exports.managers = {}; 22 | 23 | /** 24 | * Looks up an existing `Manager` for multiplexing. 25 | * If the user summons: 26 | * 27 | * `io('http://localhost/a');` 28 | * `io('http://localhost/b');` 29 | * 30 | * We reuse the existing instance based on same scheme/port/host, 31 | * and we initialize sockets for each namespace. 32 | * 33 | * @api public 34 | */ 35 | 36 | function lookup(uri, opts) { 37 | if (typeof uri == 'object') { 38 | opts = uri; 39 | uri = undefined; 40 | } 41 | 42 | opts = opts || {}; 43 | 44 | var parsed = url(uri); 45 | var source = parsed.source; 46 | var id = parsed.id; 47 | var path = parsed.path; 48 | var sameNamespace = cache[id] && path in cache[id].nsps; 49 | var newConnection = opts.forceNew || opts['force new connection'] || 50 | false === opts.multiplex || sameNamespace; 51 | 52 | var io; 53 | 54 | if (newConnection) { 55 | debug('ignoring socket cache for %s', source); 56 | io = Manager(source, opts); 57 | } else { 58 | if (!cache[id]) { 59 | debug('new io instance for %s', source); 60 | cache[id] = Manager(source, opts); 61 | } 62 | io = cache[id]; 63 | } 64 | 65 | return io.socket(parsed.path); 66 | } 67 | 68 | /** 69 | * Protocol version. 70 | * 71 | * @api public 72 | */ 73 | 74 | exports.protocol = parser.protocol; 75 | 76 | /** 77 | * `connect`. 78 | * 79 | * @param {String} uri 80 | * @api public 81 | */ 82 | 83 | exports.connect = lookup; 84 | 85 | /** 86 | * Expose constructors for standalone build. 87 | * 88 | * @api public 89 | */ 90 | 91 | exports.Manager = require('./manager'); 92 | exports.Socket = require('./socket'); 93 | -------------------------------------------------------------------------------- /examples/chat/public/socket.io-client/lib/manager.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var url = require('./url'); 7 | var eio = require('engine.io-client'); 8 | var Socket = require('./socket'); 9 | var Emitter = require('component-emitter'); 10 | var parser = require('socket.io-parser'); 11 | var on = require('./on'); 12 | var bind = require('component-bind'); 13 | var object = require('object-component'); 14 | var debug = require('debug')('socket.io-client:manager'); 15 | var indexOf = require('indexof'); 16 | var Backoff = require('backo2'); 17 | 18 | /** 19 | * Module exports 20 | */ 21 | 22 | module.exports = Manager; 23 | 24 | /** 25 | * `Manager` constructor. 26 | * 27 | * @param {String} engine instance or engine uri/opts 28 | * @param {Object} options 29 | * @api public 30 | */ 31 | 32 | function Manager(uri, opts){ 33 | if (!(this instanceof Manager)) return new Manager(uri, opts); 34 | if (uri && ('object' == typeof uri)) { 35 | opts = uri; 36 | uri = undefined; 37 | } 38 | opts = opts || {}; 39 | 40 | opts.path = opts.path || '/socket.io'; 41 | this.nsps = {}; 42 | this.subs = []; 43 | this.opts = opts; 44 | this.reconnection(opts.reconnection !== false); 45 | this.reconnectionAttempts(opts.reconnectionAttempts || Infinity); 46 | this.reconnectionDelay(opts.reconnectionDelay || 1000); 47 | this.reconnectionDelayMax(opts.reconnectionDelayMax || 5000); 48 | this.randomizationFactor(opts.randomizationFactor || 0.5); 49 | this.backoff = new Backoff({ 50 | min: this.reconnectionDelay(), 51 | max: this.reconnectionDelayMax(), 52 | jitter: this.randomizationFactor() 53 | }); 54 | this.timeout(null == opts.timeout ? 20000 : opts.timeout); 55 | this.readyState = 'closed'; 56 | this.uri = uri; 57 | this.connected = []; 58 | this.lastPing = null; 59 | this.encoding = false; 60 | this.packetBuffer = []; 61 | this.encoder = new parser.Encoder(); 62 | this.decoder = new parser.Decoder(); 63 | this.autoConnect = opts.autoConnect !== false; 64 | if (this.autoConnect) this.open(); 65 | } 66 | 67 | /** 68 | * Propagate given event to sockets and emit on `this` 69 | * 70 | * @api private 71 | */ 72 | 73 | Manager.prototype.emitAll = function() { 74 | this.emit.apply(this, arguments); 75 | for (var nsp in this.nsps) { 76 | this.nsps[nsp].emit.apply(this.nsps[nsp], arguments); 77 | } 78 | }; 79 | 80 | /** 81 | * Update `socket.id` of all sockets 82 | * 83 | * @api private 84 | */ 85 | 86 | Manager.prototype.updateSocketIds = function(){ 87 | for (var nsp in this.nsps) { 88 | this.nsps[nsp].id = this.engine.id; 89 | } 90 | }; 91 | 92 | /** 93 | * Mix in `Emitter`. 94 | */ 95 | 96 | Emitter(Manager.prototype); 97 | 98 | /** 99 | * Sets the `reconnection` config. 100 | * 101 | * @param {Boolean} true/false if it should automatically reconnect 102 | * @return {Manager} self or value 103 | * @api public 104 | */ 105 | 106 | Manager.prototype.reconnection = function(v){ 107 | if (!arguments.length) return this._reconnection; 108 | this._reconnection = !!v; 109 | return this; 110 | }; 111 | 112 | /** 113 | * Sets the reconnection attempts config. 114 | * 115 | * @param {Number} max reconnection attempts before giving up 116 | * @return {Manager} self or value 117 | * @api public 118 | */ 119 | 120 | Manager.prototype.reconnectionAttempts = function(v){ 121 | if (!arguments.length) return this._reconnectionAttempts; 122 | this._reconnectionAttempts = v; 123 | return this; 124 | }; 125 | 126 | /** 127 | * Sets the delay between reconnections. 128 | * 129 | * @param {Number} delay 130 | * @return {Manager} self or value 131 | * @api public 132 | */ 133 | 134 | Manager.prototype.reconnectionDelay = function(v){ 135 | if (!arguments.length) return this._reconnectionDelay; 136 | this._reconnectionDelay = v; 137 | this.backoff && this.backoff.setMin(v); 138 | return this; 139 | }; 140 | 141 | Manager.prototype.randomizationFactor = function(v){ 142 | if (!arguments.length) return this._randomizationFactor; 143 | this._randomizationFactor = v; 144 | this.backoff && this.backoff.setJitter(v); 145 | return this; 146 | }; 147 | 148 | /** 149 | * Sets the maximum delay between reconnections. 150 | * 151 | * @param {Number} delay 152 | * @return {Manager} self or value 153 | * @api public 154 | */ 155 | 156 | Manager.prototype.reconnectionDelayMax = function(v){ 157 | if (!arguments.length) return this._reconnectionDelayMax; 158 | this._reconnectionDelayMax = v; 159 | this.backoff && this.backoff.setMax(v); 160 | return this; 161 | }; 162 | 163 | /** 164 | * Sets the connection timeout. `false` to disable 165 | * 166 | * @return {Manager} self or value 167 | * @api public 168 | */ 169 | 170 | Manager.prototype.timeout = function(v){ 171 | if (!arguments.length) return this._timeout; 172 | this._timeout = v; 173 | return this; 174 | }; 175 | 176 | /** 177 | * Starts trying to reconnect if reconnection is enabled and we have not 178 | * started reconnecting yet 179 | * 180 | * @api private 181 | */ 182 | 183 | Manager.prototype.maybeReconnectOnOpen = function() { 184 | // Only try to reconnect if it's the first time we're connecting 185 | if (!this.reconnecting && this._reconnection && this.backoff.attempts === 0) { 186 | // keeps reconnection from firing twice for the same reconnection loop 187 | this.reconnect(); 188 | } 189 | }; 190 | 191 | 192 | /** 193 | * Sets the current transport `socket`. 194 | * 195 | * @param {Function} optional, callback 196 | * @return {Manager} self 197 | * @api public 198 | */ 199 | 200 | Manager.prototype.open = 201 | Manager.prototype.connect = function(fn){ 202 | debug('readyState %s', this.readyState); 203 | if (~this.readyState.indexOf('open')) return this; 204 | 205 | debug('opening %s', this.uri); 206 | this.engine = eio(this.uri, this.opts); 207 | var socket = this.engine; 208 | var self = this; 209 | this.readyState = 'opening'; 210 | this.skipReconnect = false; 211 | 212 | // emit `open` 213 | var openSub = on(socket, 'open', function() { 214 | self.onopen(); 215 | fn && fn(); 216 | }); 217 | 218 | // emit `connect_error` 219 | var errorSub = on(socket, 'error', function(data){ 220 | debug('connect_error'); 221 | self.cleanup(); 222 | self.readyState = 'closed'; 223 | self.emitAll('connect_error', data); 224 | if (fn) { 225 | var err = new Error('Connection error'); 226 | err.data = data; 227 | fn(err); 228 | } else { 229 | // Only do this if there is no fn to handle the error 230 | self.maybeReconnectOnOpen(); 231 | } 232 | }); 233 | 234 | // emit `connect_timeout` 235 | if (false !== this._timeout) { 236 | var timeout = this._timeout; 237 | debug('connect attempt will timeout after %d', timeout); 238 | 239 | // set timer 240 | var timer = setTimeout(function(){ 241 | debug('connect attempt timed out after %d', timeout); 242 | openSub.destroy(); 243 | socket.close(); 244 | socket.emit('error', 'timeout'); 245 | self.emitAll('connect_timeout', timeout); 246 | }, timeout); 247 | 248 | this.subs.push({ 249 | destroy: function(){ 250 | clearTimeout(timer); 251 | } 252 | }); 253 | } 254 | 255 | this.subs.push(openSub); 256 | this.subs.push(errorSub); 257 | 258 | return this; 259 | }; 260 | 261 | /** 262 | * Called upon transport open. 263 | * 264 | * @api private 265 | */ 266 | 267 | Manager.prototype.onopen = function(){ 268 | debug('open'); 269 | 270 | // clear old subs 271 | this.cleanup(); 272 | 273 | // mark as open 274 | this.readyState = 'open'; 275 | this.emit('open'); 276 | 277 | // add new subs 278 | var socket = this.engine; 279 | this.subs.push(on(socket, 'data', bind(this, 'ondata'))); 280 | this.subs.push(on(socket, 'ping', bind(this, 'onping'))); 281 | this.subs.push(on(socket, 'pong', bind(this, 'onpong'))); 282 | this.subs.push(on(socket, 'error', bind(this, 'onerror'))); 283 | this.subs.push(on(socket, 'close', bind(this, 'onclose'))); 284 | this.subs.push(on(this.decoder, 'decoded', bind(this, 'ondecoded'))); 285 | }; 286 | 287 | /** 288 | * Called upon a ping. 289 | * 290 | * @api private 291 | */ 292 | 293 | Manager.prototype.onping = function(){ 294 | this.lastPing = new Date; 295 | this.emitAll('ping'); 296 | }; 297 | 298 | /** 299 | * Called upon a packet. 300 | * 301 | * @api private 302 | */ 303 | 304 | Manager.prototype.onpong = function(){ 305 | this.emitAll('pong', new Date - this.lastPing); 306 | }; 307 | 308 | /** 309 | * Called with data. 310 | * 311 | * @api private 312 | */ 313 | 314 | Manager.prototype.ondata = function(data){ 315 | this.decoder.add(data); 316 | }; 317 | 318 | /** 319 | * Called when parser fully decodes a packet. 320 | * 321 | * @api private 322 | */ 323 | 324 | Manager.prototype.ondecoded = function(packet) { 325 | this.emit('packet', packet); 326 | }; 327 | 328 | /** 329 | * Called upon socket error. 330 | * 331 | * @api private 332 | */ 333 | 334 | Manager.prototype.onerror = function(err){ 335 | debug('error', err); 336 | this.emitAll('error', err); 337 | }; 338 | 339 | /** 340 | * Creates a new socket for the given `nsp`. 341 | * 342 | * @return {Socket} 343 | * @api public 344 | */ 345 | 346 | Manager.prototype.socket = function(nsp){ 347 | var socket = this.nsps[nsp]; 348 | if (!socket) { 349 | socket = new Socket(this, nsp); 350 | this.nsps[nsp] = socket; 351 | var self = this; 352 | socket.on('connect', function(){ 353 | socket.id = self.engine.id; 354 | if (!~indexOf(self.connected, socket)) { 355 | self.connected.push(socket); 356 | } 357 | }); 358 | } 359 | return socket; 360 | }; 361 | 362 | /** 363 | * Called upon a socket close. 364 | * 365 | * @param {Socket} socket 366 | */ 367 | 368 | Manager.prototype.destroy = function(socket){ 369 | var index = indexOf(this.connected, socket); 370 | if (~index) this.connected.splice(index, 1); 371 | if (this.connected.length) return; 372 | 373 | this.close(); 374 | }; 375 | 376 | /** 377 | * Writes a packet. 378 | * 379 | * @param {Object} packet 380 | * @api private 381 | */ 382 | 383 | Manager.prototype.packet = function(packet){ 384 | debug('writing packet %j', packet); 385 | var self = this; 386 | 387 | if (!self.encoding) { 388 | // encode, then write to engine with result 389 | self.encoding = true; 390 | this.encoder.encode(packet, function(encodedPackets) { 391 | for (var i = 0; i < encodedPackets.length; i++) { 392 | self.engine.write(encodedPackets[i], packet.options); 393 | } 394 | self.encoding = false; 395 | self.processPacketQueue(); 396 | }); 397 | } else { // add packet to the queue 398 | self.packetBuffer.push(packet); 399 | } 400 | }; 401 | 402 | /** 403 | * If packet buffer is non-empty, begins encoding the 404 | * next packet in line. 405 | * 406 | * @api private 407 | */ 408 | 409 | Manager.prototype.processPacketQueue = function() { 410 | if (this.packetBuffer.length > 0 && !this.encoding) { 411 | var pack = this.packetBuffer.shift(); 412 | this.packet(pack); 413 | } 414 | }; 415 | 416 | /** 417 | * Clean up transport subscriptions and packet buffer. 418 | * 419 | * @api private 420 | */ 421 | 422 | Manager.prototype.cleanup = function(){ 423 | debug('cleanup'); 424 | 425 | var sub; 426 | while (sub = this.subs.shift()) sub.destroy(); 427 | 428 | this.packetBuffer = []; 429 | this.encoding = false; 430 | this.lastPing = null; 431 | 432 | this.decoder.destroy(); 433 | }; 434 | 435 | /** 436 | * Close the current socket. 437 | * 438 | * @api private 439 | */ 440 | 441 | Manager.prototype.close = 442 | Manager.prototype.disconnect = function(){ 443 | debug('disconnect'); 444 | this.skipReconnect = true; 445 | this.reconnecting = false; 446 | if ('opening' == this.readyState) { 447 | // `onclose` will not fire because 448 | // an open event never happened 449 | this.cleanup(); 450 | } 451 | this.backoff.reset(); 452 | this.readyState = 'closed'; 453 | if (this.engine) this.engine.close(); 454 | }; 455 | 456 | /** 457 | * Called upon engine close. 458 | * 459 | * @api private 460 | */ 461 | 462 | Manager.prototype.onclose = function(reason){ 463 | debug('onclose'); 464 | 465 | this.cleanup(); 466 | this.backoff.reset(); 467 | this.readyState = 'closed'; 468 | this.emit('close', reason); 469 | 470 | if (this._reconnection && !this.skipReconnect) { 471 | this.reconnect(); 472 | } 473 | }; 474 | 475 | /** 476 | * Attempt a reconnection. 477 | * 478 | * @api private 479 | */ 480 | 481 | Manager.prototype.reconnect = function(){ 482 | if (this.reconnecting || this.skipReconnect) return this; 483 | 484 | var self = this; 485 | 486 | if (this.backoff.attempts >= this._reconnectionAttempts) { 487 | debug('reconnect failed'); 488 | this.backoff.reset(); 489 | this.emitAll('reconnect_failed'); 490 | this.reconnecting = false; 491 | } else { 492 | var delay = this.backoff.duration(); 493 | debug('will wait %dms before reconnect attempt', delay); 494 | 495 | this.reconnecting = true; 496 | var timer = setTimeout(function(){ 497 | if (self.skipReconnect) return; 498 | 499 | debug('attempting reconnect'); 500 | self.emitAll('reconnect_attempt', self.backoff.attempts); 501 | self.emitAll('reconnecting', self.backoff.attempts); 502 | 503 | // check again for the case socket closed in above events 504 | if (self.skipReconnect) return; 505 | 506 | self.open(function(err){ 507 | if (err) { 508 | debug('reconnect attempt error'); 509 | self.reconnecting = false; 510 | self.reconnect(); 511 | self.emitAll('reconnect_error', err.data); 512 | } else { 513 | debug('reconnect success'); 514 | self.onreconnect(); 515 | } 516 | }); 517 | }, delay); 518 | 519 | this.subs.push({ 520 | destroy: function(){ 521 | clearTimeout(timer); 522 | } 523 | }); 524 | } 525 | }; 526 | 527 | /** 528 | * Called upon successful reconnect. 529 | * 530 | * @api private 531 | */ 532 | 533 | Manager.prototype.onreconnect = function(){ 534 | var attempt = this.backoff.attempts; 535 | this.reconnecting = false; 536 | this.backoff.reset(); 537 | this.updateSocketIds(); 538 | this.emitAll('reconnect', attempt); 539 | }; 540 | -------------------------------------------------------------------------------- /examples/chat/public/socket.io-client/lib/on.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module exports. 4 | */ 5 | 6 | module.exports = on; 7 | 8 | /** 9 | * Helper for subscriptions. 10 | * 11 | * @param {Object|EventEmitter} obj with `Emitter` mixin or `EventEmitter` 12 | * @param {String} event name 13 | * @param {Function} callback 14 | * @api public 15 | */ 16 | 17 | function on(obj, ev, fn) { 18 | obj.on(ev, fn); 19 | return { 20 | destroy: function(){ 21 | obj.removeListener(ev, fn); 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /examples/chat/public/socket.io-client/lib/socket.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var parser = require('socket.io-parser'); 7 | var Emitter = require('component-emitter'); 8 | var toArray = require('to-array'); 9 | var on = require('./on'); 10 | var bind = require('component-bind'); 11 | var debug = require('debug')('socket.io-client:socket'); 12 | var hasBin = require('has-binary'); 13 | 14 | /** 15 | * Module exports. 16 | */ 17 | 18 | module.exports = exports = Socket; 19 | 20 | /** 21 | * Internal events (blacklisted). 22 | * These events can't be emitted by the user. 23 | * 24 | * @api private 25 | */ 26 | 27 | var events = { 28 | connect: 1, 29 | connect_error: 1, 30 | connect_timeout: 1, 31 | disconnect: 1, 32 | error: 1, 33 | reconnect: 1, 34 | reconnect_attempt: 1, 35 | reconnect_failed: 1, 36 | reconnect_error: 1, 37 | reconnecting: 1, 38 | ping: 1, 39 | pong: 1 40 | }; 41 | 42 | /** 43 | * Shortcut to `Emitter#emit`. 44 | */ 45 | 46 | var emit = Emitter.prototype.emit; 47 | 48 | /** 49 | * `Socket` constructor. 50 | * 51 | * @api public 52 | */ 53 | 54 | function Socket(io, nsp){ 55 | this.io = io; 56 | this.nsp = nsp; 57 | this.json = this; // compat 58 | this.ids = 0; 59 | this.acks = {}; 60 | if (this.io.autoConnect) this.open(); 61 | this.receiveBuffer = []; 62 | this.sendBuffer = []; 63 | this.connected = false; 64 | this.disconnected = true; 65 | } 66 | 67 | /** 68 | * Mix in `Emitter`. 69 | */ 70 | 71 | Emitter(Socket.prototype); 72 | 73 | /** 74 | * Subscribe to open, close and packet events 75 | * 76 | * @api private 77 | */ 78 | 79 | Socket.prototype.subEvents = function() { 80 | if (this.subs) return; 81 | 82 | var io = this.io; 83 | this.subs = [ 84 | on(io, 'open', bind(this, 'onopen')), 85 | on(io, 'packet', bind(this, 'onpacket')), 86 | on(io, 'close', bind(this, 'onclose')) 87 | ]; 88 | }; 89 | 90 | /** 91 | * "Opens" the socket. 92 | * 93 | * @api public 94 | */ 95 | 96 | Socket.prototype.open = 97 | Socket.prototype.connect = function(){ 98 | if (this.connected) return this; 99 | 100 | this.subEvents(); 101 | this.io.open(); // ensure open 102 | if ('open' == this.io.readyState) this.onopen(); 103 | return this; 104 | }; 105 | 106 | /** 107 | * Sends a `message` event. 108 | * 109 | * @return {Socket} self 110 | * @api public 111 | */ 112 | 113 | Socket.prototype.send = function(){ 114 | var args = toArray(arguments); 115 | args.unshift('message'); 116 | this.emit.apply(this, args); 117 | return this; 118 | }; 119 | 120 | /** 121 | * Override `emit`. 122 | * If the event is in `events`, it's emitted normally. 123 | * 124 | * @param {String} event name 125 | * @return {Socket} self 126 | * @api public 127 | */ 128 | 129 | Socket.prototype.emit = function(ev){ 130 | if (events.hasOwnProperty(ev)) { 131 | emit.apply(this, arguments); 132 | return this; 133 | } 134 | 135 | var args = toArray(arguments); 136 | var parserType = parser.EVENT; // default 137 | if (hasBin(args)) { parserType = parser.BINARY_EVENT; } // binary 138 | var packet = { type: parserType, data: args }; 139 | 140 | packet.options = {}; 141 | packet.options.compress = !this.flags || false !== this.flags.compress; 142 | 143 | // event ack callback 144 | if ('function' == typeof args[args.length - 1]) { 145 | debug('emitting packet with ack id %d', this.ids); 146 | this.acks[this.ids] = args.pop(); 147 | packet.id = this.ids++; 148 | } 149 | 150 | if (this.connected) { 151 | this.packet(packet); 152 | } else { 153 | this.sendBuffer.push(packet); 154 | } 155 | 156 | delete this.flags; 157 | 158 | return this; 159 | }; 160 | 161 | /** 162 | * Sends a packet. 163 | * 164 | * @param {Object} packet 165 | * @api private 166 | */ 167 | 168 | Socket.prototype.packet = function(packet){ 169 | packet.nsp = this.nsp; 170 | this.io.packet(packet); 171 | }; 172 | 173 | /** 174 | * Called upon engine `open`. 175 | * 176 | * @api private 177 | */ 178 | 179 | Socket.prototype.onopen = function(){ 180 | debug('transport is open - connecting'); 181 | 182 | // write connect packet if necessary 183 | if ('/' != this.nsp) { 184 | this.packet({ type: parser.CONNECT, options: { compress: true } }); 185 | } 186 | }; 187 | 188 | /** 189 | * Called upon engine `close`. 190 | * 191 | * @param {String} reason 192 | * @api private 193 | */ 194 | 195 | Socket.prototype.onclose = function(reason){ 196 | debug('close (%s)', reason); 197 | this.connected = false; 198 | this.disconnected = true; 199 | delete this.id; 200 | this.emit('disconnect', reason); 201 | }; 202 | 203 | /** 204 | * Called with socket packet. 205 | * 206 | * @param {Object} packet 207 | * @api private 208 | */ 209 | 210 | Socket.prototype.onpacket = function(packet){ 211 | if (packet.nsp != this.nsp) return; 212 | 213 | switch (packet.type) { 214 | case parser.CONNECT: 215 | this.onconnect(); 216 | break; 217 | 218 | case parser.EVENT: 219 | this.onevent(packet); 220 | break; 221 | 222 | case parser.BINARY_EVENT: 223 | this.onevent(packet); 224 | break; 225 | 226 | case parser.ACK: 227 | this.onack(packet); 228 | break; 229 | 230 | case parser.BINARY_ACK: 231 | this.onack(packet); 232 | break; 233 | 234 | case parser.DISCONNECT: 235 | this.ondisconnect(); 236 | break; 237 | 238 | case parser.ERROR: 239 | this.emit('error', packet.data); 240 | break; 241 | } 242 | }; 243 | 244 | /** 245 | * Called upon a server event. 246 | * 247 | * @param {Object} packet 248 | * @api private 249 | */ 250 | 251 | Socket.prototype.onevent = function(packet){ 252 | var args = packet.data || []; 253 | debug('emitting event %j', args); 254 | 255 | if (null != packet.id) { 256 | debug('attaching ack callback to event'); 257 | args.push(this.ack(packet.id)); 258 | } 259 | 260 | if (this.connected) { 261 | emit.apply(this, args); 262 | } else { 263 | this.receiveBuffer.push(args); 264 | } 265 | }; 266 | 267 | /** 268 | * Produces an ack callback to emit with an event. 269 | * 270 | * @api private 271 | */ 272 | 273 | Socket.prototype.ack = function(id){ 274 | var self = this; 275 | var sent = false; 276 | return function(){ 277 | // prevent double callbacks 278 | if (sent) return; 279 | sent = true; 280 | var args = toArray(arguments); 281 | debug('sending ack %j', args); 282 | 283 | var type = hasBin(args) ? parser.BINARY_ACK : parser.ACK; 284 | self.packet({ 285 | type: type, 286 | id: id, 287 | data: args, 288 | options: { compress: true } 289 | }); 290 | }; 291 | }; 292 | 293 | /** 294 | * Called upon a server acknowlegement. 295 | * 296 | * @param {Object} packet 297 | * @api private 298 | */ 299 | 300 | Socket.prototype.onack = function(packet){ 301 | var ack = this.acks[packet.id]; 302 | if ('function' == typeof ack) { 303 | debug('calling ack %s with %j', packet.id, packet.data); 304 | ack.apply(this, packet.data); 305 | delete this.acks[packet.id]; 306 | } else { 307 | debug('bad ack %s', packet.id); 308 | } 309 | }; 310 | 311 | /** 312 | * Called upon server connect. 313 | * 314 | * @api private 315 | */ 316 | 317 | Socket.prototype.onconnect = function(){ 318 | this.connected = true; 319 | this.disconnected = false; 320 | this.emit('connect'); 321 | this.emitBuffered(); 322 | }; 323 | 324 | /** 325 | * Emit buffered events (received and emitted). 326 | * 327 | * @api private 328 | */ 329 | 330 | Socket.prototype.emitBuffered = function(){ 331 | var i; 332 | for (i = 0; i < this.receiveBuffer.length; i++) { 333 | emit.apply(this, this.receiveBuffer[i]); 334 | } 335 | this.receiveBuffer = []; 336 | 337 | for (i = 0; i < this.sendBuffer.length; i++) { 338 | this.packet(this.sendBuffer[i]); 339 | } 340 | this.sendBuffer = []; 341 | }; 342 | 343 | /** 344 | * Called upon server disconnect. 345 | * 346 | * @api private 347 | */ 348 | 349 | Socket.prototype.ondisconnect = function(){ 350 | debug('server disconnect (%s)', this.nsp); 351 | this.destroy(); 352 | this.onclose('io server disconnect'); 353 | }; 354 | 355 | /** 356 | * Called upon forced client/server side disconnections, 357 | * this method ensures the manager stops tracking us and 358 | * that reconnections don't get triggered for this. 359 | * 360 | * @api private. 361 | */ 362 | 363 | Socket.prototype.destroy = function(){ 364 | if (this.subs) { 365 | // clean subscriptions to avoid reconnections 366 | for (var i = 0; i < this.subs.length; i++) { 367 | this.subs[i].destroy(); 368 | } 369 | this.subs = null; 370 | } 371 | 372 | this.io.destroy(this); 373 | }; 374 | 375 | /** 376 | * Disconnects the socket manually. 377 | * 378 | * @return {Socket} self 379 | * @api public 380 | */ 381 | 382 | Socket.prototype.close = 383 | Socket.prototype.disconnect = function(){ 384 | if (this.connected) { 385 | debug('performing disconnect (%s)', this.nsp); 386 | this.packet({ type: parser.DISCONNECT, options: { compress: true } }); 387 | } 388 | 389 | // remove socket from pool 390 | this.destroy(); 391 | 392 | if (this.connected) { 393 | // fire events 394 | this.onclose('io client disconnect'); 395 | } 396 | return this; 397 | }; 398 | 399 | /** 400 | * Sets the compress flag. 401 | * 402 | * @param {Boolean} if `true`, compresses the sending data 403 | * @return {Socket} self 404 | * @api public 405 | */ 406 | 407 | Socket.prototype.compress = function(compress){ 408 | this.flags = this.flags || {}; 409 | this.flags.compress = compress; 410 | return this; 411 | }; 412 | -------------------------------------------------------------------------------- /examples/chat/public/socket.io-client/lib/url.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var parseuri = require('parseuri'); 7 | var debug = require('debug')('socket.io-client:url'); 8 | 9 | /** 10 | * Module exports. 11 | */ 12 | 13 | module.exports = url; 14 | 15 | /** 16 | * URL parser. 17 | * 18 | * @param {String} url 19 | * @param {Object} An object meant to mimic window.location. 20 | * Defaults to window.location. 21 | * @api public 22 | */ 23 | 24 | function url(uri, loc){ 25 | var obj = uri; 26 | 27 | // default to window.location 28 | var loc = loc || global.location; 29 | if (null == uri) uri = loc.protocol + '//' + loc.host; 30 | 31 | // relative path support 32 | if ('string' == typeof uri) { 33 | if ('/' == uri.charAt(0)) { 34 | if ('/' == uri.charAt(1)) { 35 | uri = loc.protocol + uri; 36 | } else { 37 | uri = loc.host + uri; 38 | } 39 | } 40 | 41 | if (!/^(https?|wss?):\/\//.test(uri)) { 42 | debug('protocol-less url %s', uri); 43 | if ('undefined' != typeof loc) { 44 | uri = loc.protocol + '//' + uri; 45 | } else { 46 | uri = 'https://' + uri; 47 | } 48 | } 49 | 50 | // parse 51 | debug('parse %s', uri); 52 | obj = parseuri(uri); 53 | } 54 | 55 | // make sure we treat `localhost:80` and `localhost` equally 56 | if (!obj.port) { 57 | if (/^(http|ws)$/.test(obj.protocol)) { 58 | obj.port = '80'; 59 | } 60 | else if (/^(http|ws)s$/.test(obj.protocol)) { 61 | obj.port = '443'; 62 | } 63 | } 64 | 65 | obj.path = obj.path || '/'; 66 | 67 | // define unique id 68 | obj.id = obj.protocol + '://' + obj.host + ':' + obj.port; 69 | // define href 70 | obj.href = obj.protocol + '://' + obj.host + (loc && loc.port == obj.port ? '' : (':' + obj.port)); 71 | 72 | return obj; 73 | } 74 | -------------------------------------------------------------------------------- /examples/chat/public/style.css: -------------------------------------------------------------------------------- 1 | /* Fix user-agent */ 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | font-weight: 300; 9 | -webkit-font-smoothing: antialiased; 10 | } 11 | 12 | html, input { 13 | font-family: 14 | "HelveticaNeue-Light", 15 | "Helvetica Neue Light", 16 | "Helvetica Neue", 17 | Helvetica, 18 | Arial, 19 | "Lucida Grande", 20 | sans-serif; 21 | } 22 | 23 | html, body { 24 | height: 100%; 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | ul { 30 | list-style: none; 31 | word-wrap: break-word; 32 | } 33 | 34 | /* Pages */ 35 | 36 | .pages { 37 | height: 100%; 38 | margin: 0; 39 | padding: 0; 40 | width: 100%; 41 | } 42 | 43 | .page { 44 | height: 100%; 45 | position: absolute; 46 | width: 100%; 47 | } 48 | 49 | /* Login Page */ 50 | 51 | .login.page { 52 | background-color: #000; 53 | } 54 | 55 | .login.page .form { 56 | height: 100px; 57 | margin-top: -100px; 58 | position: absolute; 59 | 60 | text-align: center; 61 | top: 50%; 62 | width: 100%; 63 | } 64 | 65 | .login.page .form .usernameInput { 66 | background-color: transparent; 67 | border: none; 68 | border-bottom: 2px solid #fff; 69 | outline: none; 70 | padding-bottom: 15px; 71 | text-align: center; 72 | width: 400px; 73 | } 74 | 75 | .login.page .title { 76 | font-size: 200%; 77 | } 78 | 79 | .login.page .usernameInput { 80 | font-size: 200%; 81 | letter-spacing: 3px; 82 | } 83 | 84 | .login.page .title, .login.page .usernameInput { 85 | color: #fff; 86 | font-weight: 100; 87 | } 88 | 89 | /* Chat page */ 90 | 91 | .chat.page { 92 | display: none; 93 | } 94 | 95 | /* Font */ 96 | 97 | .messages { 98 | font-size: 150%; 99 | } 100 | 101 | .inputMessage { 102 | font-size: 100%; 103 | } 104 | 105 | .log { 106 | color: gray; 107 | font-size: 70%; 108 | margin: 5px; 109 | text-align: center; 110 | } 111 | 112 | /* Messages */ 113 | 114 | .chatArea { 115 | height: 100%; 116 | padding-bottom: 60px; 117 | } 118 | 119 | .messages { 120 | height: 100%; 121 | margin: 0; 122 | overflow-y: scroll; 123 | padding: 10px 20px 10px 20px; 124 | } 125 | 126 | .message.typing .messageBody { 127 | color: gray; 128 | } 129 | 130 | .username { 131 | float: left; 132 | font-weight: 700; 133 | overflow: hidden; 134 | padding-right: 15px; 135 | text-align: right; 136 | } 137 | 138 | /* Input */ 139 | 140 | .inputMessage { 141 | border: 10px solid #000; 142 | bottom: 0; 143 | height: 60px; 144 | left: 0; 145 | outline: none; 146 | padding-left: 10px; 147 | position: absolute; 148 | right: 0; 149 | width: 100%; 150 | } 151 | -------------------------------------------------------------------------------- /examples/chat/start_for_win.bat: -------------------------------------------------------------------------------- 1 | php start_web.php start_io.php 2 | pause 3 | -------------------------------------------------------------------------------- /examples/chat/start_io.php: -------------------------------------------------------------------------------- 1 | on('connection', function($socket){ 12 | $socket->addedUser = false; 13 | 14 | // when the client emits 'new message', this listens and executes 15 | $socket->on('new message', function ($data)use($socket){ 16 | // we tell the client to execute 'new message' 17 | $socket->broadcast->emit('new message', array( 18 | 'username'=> $socket->username, 19 | 'message'=> $data 20 | )); 21 | }); 22 | 23 | // when the client emits 'add user', this listens and executes 24 | $socket->on('add user', function ($username) use($socket){ 25 | global $usernames, $numUsers; 26 | // we store the username in the socket session for this client 27 | $socket->username = $username; 28 | // add the client's username to the global list 29 | $usernames[$username] = $username; 30 | ++$numUsers; 31 | $socket->addedUser = true; 32 | $socket->emit('login', array( 33 | 'numUsers' => $numUsers 34 | )); 35 | // echo globally (all clients) that a person has connected 36 | $socket->broadcast->emit('user joined', array( 37 | 'username' => $socket->username, 38 | 'numUsers' => $numUsers 39 | )); 40 | }); 41 | 42 | // when the client emits 'typing', we broadcast it to others 43 | $socket->on('typing', function () use($socket) { 44 | $socket->broadcast->emit('typing', array( 45 | 'username' => $socket->username 46 | )); 47 | }); 48 | 49 | // when the client emits 'stop typing', we broadcast it to others 50 | $socket->on('stop typing', function () use($socket) { 51 | $socket->broadcast->emit('stop typing', array( 52 | 'username' => $socket->username 53 | )); 54 | }); 55 | 56 | // when the user disconnects.. perform this 57 | $socket->on('disconnect', function () use($socket) { 58 | global $usernames, $numUsers; 59 | // remove the username from global usernames list 60 | if($socket->addedUser) { 61 | unset($usernames[$socket->username]); 62 | --$numUsers; 63 | 64 | // echo globally that this client has left 65 | $socket->broadcast->emit('user left', array( 66 | 'username' => $socket->username, 67 | 'numUsers' => $numUsers 68 | )); 69 | } 70 | }); 71 | 72 | }); 73 | 74 | if (!defined('GLOBAL_START')) { 75 | Worker::runAll(); 76 | } 77 | -------------------------------------------------------------------------------- /examples/chat/start_web.php: -------------------------------------------------------------------------------- 1 | addRoot('localhost', __DIR__ . '/public'); 12 | 13 | if (!defined('GLOBAL_START')) { 14 | Worker::runAll(); 15 | } 16 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/Debug.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 | -------------------------------------------------------------------------------- /src/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 | if (!$success) 73 | { 74 | self::sendErrorMessage($req, $req->res, $err); 75 | return; 76 | } 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 | $worker->onConnect = array($this, 'onConnect'); 231 | } 232 | 233 | public function onConnect($connection) 234 | { 235 | $connection->onRequest = array($this, 'handleRequest'); 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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/Engine/Protocols/Http/Request.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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | TcpConnection::$statistics['total_request']++; 34 | $connection->onClose = '\PHPSocketIO\Engine\Protocols\SocketIO::emitClose'; 35 | if(isset($req->headers['upgrade']) && strtolower($req->headers['upgrade']) === 'websocket') 36 | { 37 | $connection->consumeRecvBuffer(strlen($http_buffer)); 38 | WebSocket::dealHandshake($connection, $req, $res); 39 | self::cleanup($connection); 40 | return 0; 41 | } 42 | if(!empty($connection->onRequest)) 43 | { 44 | $connection->consumeRecvBuffer(strlen($http_buffer)); 45 | self::emitRequest($connection, $req, $res); 46 | if($req->method === 'GET' || $req->method === 'OPTIONS') 47 | { 48 | self::emitEnd($connection, $req); 49 | return 0; 50 | } 51 | 52 | // POST 53 | if('\PHPSocketIO\Engine\Protocols\SocketIO::onData' !== $connection->onMessage) 54 | { 55 | $connection->onMessage = '\PHPSocketIO\Engine\Protocols\SocketIO::onData'; 56 | } 57 | if(!$raw_body) 58 | { 59 | return 0; 60 | } 61 | self::onData($connection, $raw_body); 62 | return 0; 63 | } 64 | else 65 | { 66 | if($req->method === 'GET') 67 | { 68 | return $pos + 4; 69 | } 70 | elseif(isset($req->headers['content-length'])) 71 | { 72 | return $req->headers['content-length']; 73 | } 74 | else 75 | { 76 | $connection->close("HTTP/1.1 400 bad request\r\n\r\ntrunk not support"); 77 | return 0; 78 | } 79 | } 80 | } 81 | 82 | public static function onData($connection, $data) 83 | { 84 | $req = $connection->httpRequest; 85 | self::emitData($connection, $req, $data); 86 | if((isset($req->headers['content-length']) && $req->headers['content-length'] <= strlen($data)) 87 | || substr($data, -5) === "0\r\n\r\n") 88 | { 89 | self::emitEnd($connection, $req); 90 | } 91 | } 92 | 93 | protected static function emitRequest($connection, $req, $res) 94 | { 95 | try 96 | { 97 | call_user_func($connection->onRequest, $req, $res); 98 | } 99 | catch(\Exception $e) 100 | { 101 | echo $e; 102 | } 103 | } 104 | 105 | public static function emitClose($connection) 106 | { 107 | $req = $connection->httpRequest; 108 | if(isset($req->onClose)) 109 | { 110 | try 111 | { 112 | call_user_func($req->onClose, $req); 113 | } 114 | catch(\Exception $e) 115 | { 116 | echo $e; 117 | } 118 | } 119 | $res = $connection->httpResponse; 120 | if(isset($res->onClose)) 121 | { 122 | try 123 | { 124 | call_user_func($res->onClose, $res); 125 | } 126 | catch(\Exception $e) 127 | { 128 | echo $e; 129 | } 130 | } 131 | self::cleanup($connection); 132 | } 133 | 134 | public static function cleanup($connection) 135 | { 136 | if(!empty($connection->onRequest)) 137 | { 138 | $connection->onRequest = null; 139 | } 140 | if(!empty($connection->onWebSocketConnect)) 141 | { 142 | $connection->onWebSocketConnect = null; 143 | } 144 | if(!empty($connection->httpRequest)) 145 | { 146 | $connection->httpRequest->destroy(); 147 | $connection->httpRequest = null; 148 | } 149 | if(!empty($connection->httpResponse)) 150 | { 151 | $connection->httpResponse->destroy(); 152 | $connection->httpResponse = null; 153 | } 154 | } 155 | 156 | public static function emitData($connection, $req, $data) 157 | { 158 | if(isset($req->onData)) 159 | { 160 | try 161 | { 162 | call_user_func($req->onData, $req, $data); 163 | } 164 | catch(\Exception $e) 165 | { 166 | echo $e; 167 | } 168 | } 169 | } 170 | 171 | public static function emitEnd($connection, $req) 172 | { 173 | if(isset($req->onEnd)) 174 | { 175 | try 176 | { 177 | call_user_func($req->onEnd, $req); 178 | } 179 | catch(\Exception $e) 180 | { 181 | echo $e; 182 | } 183 | } 184 | $connection->hasReadedHead = false; 185 | } 186 | 187 | public static function encode($buffer, $connection) 188 | { 189 | if(!isset($connection->onRequest)) 190 | { 191 | $connection->httpResponse->setHeader('Content-Length', strlen($buffer)); 192 | return $connection->httpResponse->getHeadBuffer() . $buffer; 193 | } 194 | return $buffer; 195 | } 196 | 197 | public static function decode($http_buffer, $connection) 198 | { 199 | if(isset($connection->onRequest)) 200 | { 201 | return $http_buffer; 202 | } 203 | else 204 | { 205 | list($head, $body) = explode("\r\n\r\n", $http_buffer, 2); 206 | return $body; 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | use Workerman\Connection\ConnectionInterface; 17 | 18 | /** 19 | * WebSocket 协议服务端解包和打包 20 | */ 21 | class RFC6455 implements \Workerman\Protocols\ProtocolInterface 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, ConnectionInterface $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, ConnectionInterface $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, ConnectionInterface $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 | -------------------------------------------------------------------------------- /src/Engine/Socket.php: -------------------------------------------------------------------------------- 1 | id = $id; 25 | $this->server = $server; 26 | $this->request = $req; 27 | $this->remoteAddress = $req->connection->getRemoteIp().':'.$req->connection->getRemotePort(); 28 | $this->setTransport($transport); 29 | $this->onOpen(); 30 | Debug::debug('Engine/Socket __construct'); 31 | } 32 | 33 | public function __destruct() 34 | { 35 | Debug::debug('Engine/Socket __destruct'); 36 | } 37 | 38 | public function maybeUpgrade($transport) 39 | { 40 | $this->upgrading = true; 41 | $this->upgradeTimeoutTimer = Timer::add( 42 | $this->server->upgradeTimeout, 43 | array($this, 'upgradeTimeoutCallback'), 44 | array($transport), false 45 | ); 46 | $this->upgradeTransport = $transport; 47 | $transport->on('packet', array($this, 'onUpgradePacket')); 48 | $transport->once('close', array($this, 'onUpgradeTransportClose')); 49 | $transport->once('error', array($this, 'onUpgradeTransportError')); 50 | $this->once('close', array($this, 'onUpgradeTransportClose')); 51 | } 52 | 53 | public function onUpgradePacket($packet) 54 | { 55 | if(empty($this->upgradeTransport)) 56 | { 57 | $this->onError('upgradeTransport empty'); 58 | return; 59 | } 60 | if('ping' === $packet['type'] && (isset($packet['data']) && 'probe' === $packet['data'])) 61 | { 62 | $this->upgradeTransport->send(array(array('type'=> 'pong', 'data'=> 'probe'))); 63 | //$this->transport->shouldClose = function(){}; 64 | if ($this->checkIntervalTimer) { 65 | Timer::del($this->checkIntervalTimer); 66 | } 67 | $this->checkIntervalTimer = Timer::add(0.5, array($this, 'check')); 68 | } 69 | else if('upgrade' === $packet['type'] && $this->readyState !== 'closed') 70 | { 71 | $this->upgradeCleanup(); 72 | $this->upgraded = true; 73 | $this->clearTransport(); 74 | $this->transport->destroy(); 75 | $this->setTransport($this->upgradeTransport); 76 | $this->emit('upgrade', $this->upgradeTransport); 77 | $this->upgradeTransport = null; 78 | $this->setPingTimeout(); 79 | $this->flush(); 80 | if($this->readyState === 'closing') 81 | { 82 | $this->transport->close(array($this, 'onClose')); 83 | } 84 | } 85 | else 86 | { 87 | if(!empty($this->upgradeTransport)) 88 | { 89 | $this->upgradeCleanup(); 90 | $this->upgradeTransport->close(); 91 | $this->upgradeTransport = null; 92 | } 93 | } 94 | 95 | } 96 | 97 | 98 | public function upgradeCleanup() 99 | { 100 | $this->upgrading = false; 101 | Timer::del($this->checkIntervalTimer); 102 | Timer::del($this->upgradeTimeoutTimer); 103 | if(!empty($this->upgradeTransport)) 104 | { 105 | $this->upgradeTransport->removeListener('packet', array($this, 'onUpgradePacket')); 106 | $this->upgradeTransport->removeListener('close', array($this, 'onUpgradeTransportClose')); 107 | $this->upgradeTransport->removeListener('error', array($this, 'onUpgradeTransportError')); 108 | } 109 | $this->removeListener('close', array($this, 'onUpgradeTransportClose')); 110 | } 111 | 112 | public function onUpgradeTransportClose() 113 | { 114 | $this->onUpgradeTransportError('transport closed'); 115 | } 116 | 117 | public function onUpgradeTransportError($err) 118 | { 119 | //echo $err; 120 | $this->upgradeCleanup(); 121 | if($this->upgradeTransport) 122 | { 123 | $this->upgradeTransport->close(); 124 | $this->upgradeTransport = null; 125 | } 126 | } 127 | 128 | public function upgradeTimeoutCallback($transport) 129 | { 130 | //echo("client did not complete upgrade - closing transport\n"); 131 | $this->upgradeCleanup(); 132 | if('open' === $transport->readyState) 133 | { 134 | $transport->close(); 135 | } 136 | } 137 | 138 | public function setTransport($transport) 139 | { 140 | $this->transport = $transport; 141 | $this->transport->once('error', array($this, 'onError')); 142 | $this->transport->on('packet', array($this, 'onPacket')); 143 | $this->transport->on('drain', array($this, 'flush')); 144 | $this->transport->once('close', array($this, 'onClose')); 145 | //this function will manage packet events (also message callbacks) 146 | $this->setupSendCallback(); 147 | } 148 | 149 | public function onOpen() 150 | { 151 | $this->readyState = 'open'; 152 | 153 | // sends an `open` packet 154 | $this->transport->sid = $this->id; 155 | $this->sendPacket('open', json_encode(array( 156 | 'sid'=> $this->id 157 | , 'upgrades' => $this->getAvailableUpgrades() 158 | , 'pingInterval'=> $this->server->pingInterval*1000 159 | , 'pingTimeout'=> $this->server->pingTimeout*1000 160 | ))); 161 | 162 | $this->emit('open'); 163 | $this->setPingTimeout(); 164 | } 165 | 166 | public function onPacket($packet) 167 | { 168 | if ('open' === $this->readyState) { 169 | // export packet event 170 | $this->emit('packet', $packet); 171 | 172 | // Reset ping timeout on any packet, incoming data is a good sign of 173 | // other side's liveness 174 | $this->setPingTimeout(); 175 | switch ($packet['type']) { 176 | 177 | case 'ping': 178 | $this->sendPacket('pong'); 179 | $this->emit('heartbeat'); 180 | break; 181 | 182 | case 'error': 183 | $this->onClose('parse error'); 184 | break; 185 | 186 | case 'message': 187 | $this->emit('data', $packet['data']); 188 | $this->emit('message', $packet['data']); 189 | break; 190 | } 191 | } 192 | else 193 | { 194 | echo('packet received with closed socket'); 195 | } 196 | } 197 | 198 | public function check() 199 | { 200 | if('polling' == $this->transport->name && $this->transport->writable) 201 | { 202 | $this->transport->send(array(array('type' => 'noop'))); 203 | } 204 | } 205 | 206 | public function onError($err) 207 | { 208 | $this->onClose('transport error', $err); 209 | } 210 | 211 | public function setPingTimeout() 212 | { 213 | if ($this->pingTimeoutTimer) { 214 | Timer::del($this->pingTimeoutTimer); 215 | } 216 | $this->pingTimeoutTimer = Timer::add( 217 | $this->server->pingInterval + $this->server->pingTimeout , 218 | array($this, 'pingTimeoutCallback'), null, false); 219 | } 220 | 221 | public function pingTimeoutCallback() 222 | { 223 | $this->transport->close(); 224 | $this->onClose('ping timeout'); 225 | } 226 | 227 | 228 | public function clearTransport() 229 | { 230 | $this->transport->close(); 231 | Timer::del($this->pingTimeoutTimer); 232 | } 233 | 234 | public function onClose($reason = '', $description = null) 235 | { 236 | if ('closed' !== $this->readyState) 237 | { 238 | Timer::del($this->pingTimeoutTimer); 239 | Timer::del($this->checkIntervalTimer); 240 | $this->checkIntervalTimer = null; 241 | Timer::del($this->upgradeTimeoutTimer); 242 | // clean writeBuffer in next tick, so developers can still 243 | // grab the writeBuffer on 'close' event 244 | $this->writeBuffer = array(); 245 | $this->packetsFn = array(); 246 | $this->sentCallbackFn = array(); 247 | $this->clearTransport(); 248 | $this->readyState = 'closed'; 249 | $this->emit('close', $this->id, $reason, $description); 250 | $this->server = null; 251 | $this->request = null; 252 | $this->upgradeTransport = null; 253 | $this->removeAllListeners(); 254 | if(!empty($this->transport)) 255 | { 256 | $this->transport->removeAllListeners(); 257 | $this->transport = null; 258 | } 259 | } 260 | } 261 | 262 | public function send($data, $options, $callback) 263 | { 264 | $this->sendPacket('message', $data, $options, $callback); 265 | return $this; 266 | } 267 | 268 | public function write($data, $options = array(), $callback = null) 269 | { 270 | return $this->send($data, $options, $callback); 271 | } 272 | 273 | public function sendPacket($type, $data = null, $callback = null) 274 | { 275 | if('closing' !== $this->readyState) 276 | { 277 | $packet = array( 278 | 'type'=> $type 279 | ); 280 | if($data !== null) 281 | { 282 | $packet['data'] = $data; 283 | } 284 | // exports packetCreate event 285 | $this->emit('packetCreate', $packet); 286 | $this->writeBuffer[] = $packet; 287 | //add send callback to object 288 | if($callback) 289 | { 290 | $this->packetsFn[] = $callback; 291 | } 292 | $this->flush(); 293 | } 294 | } 295 | 296 | public function flush() 297 | { 298 | if ('closed' !== $this->readyState && $this->transport->writable 299 | && $this->writeBuffer) 300 | { 301 | $this->emit('flush', $this->writeBuffer); 302 | $this->server->emit('flush', $this, $this->writeBuffer); 303 | $wbuf = $this->writeBuffer; 304 | $this->writeBuffer = array(); 305 | if($this->packetsFn) 306 | { 307 | if(!empty($this->transport->supportsFraming)) 308 | { 309 | $this->sentCallbackFn[] = $this->packetsFn; 310 | } 311 | else 312 | { 313 | // @todo check 314 | $this->sentCallbackFn[]=$this->packetsFn; 315 | } 316 | } 317 | $this->packetsFn = array(); 318 | $this->transport->send($wbuf); 319 | $this->emit('drain'); 320 | if($this->server) 321 | { 322 | $this->server->emit('drain', $this); 323 | } 324 | } 325 | } 326 | 327 | public function getAvailableUpgrades() 328 | { 329 | return array('websocket'); 330 | } 331 | 332 | public function close() 333 | { 334 | if ('open' !== $this->readyState) 335 | { 336 | return; 337 | } 338 | 339 | $this->readyState = 'closing'; 340 | 341 | if ($this->writeBuffer) { 342 | $this->once('drain', array($this, 'closeTransport')); 343 | return; 344 | } 345 | 346 | $this->closeTransport(); 347 | } 348 | 349 | public function closeTransport() 350 | { 351 | //todo onClose.bind(this, 'forced close')); 352 | $this->transport->close(array($this, 'onClose')); 353 | } 354 | 355 | public function setupSendCallback() 356 | { 357 | $self = $this; 358 | //the message was sent successfully, execute the callback 359 | $this->transport->on('drain', array($this, 'onDrainCallback')); 360 | } 361 | 362 | public function onDrainCallback() 363 | { 364 | if ($this->sentCallbackFn) 365 | { 366 | $seqFn = array_shift($this->sentCallbackFn); 367 | if(is_callable($seqFn)) 368 | { 369 | echo('executing send callback'); 370 | call_user_func($seqFn, $this->transport); 371 | }else if (is_array($seqFn)) { 372 | echo('executing batch send callback'); 373 | foreach($seqFn as $fn) 374 | { 375 | call_user_func($fn, $this->transport); 376 | } 377 | } 378 | } 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/Engine/Transports/Polling.php: -------------------------------------------------------------------------------- 1 | res; 15 | 16 | if ('GET' === $req->method) 17 | { 18 | $this->onPollRequest($req, $res); 19 | } 20 | else if('POST' === $req->method) 21 | { 22 | $this->onDataRequest($req, $res); 23 | } 24 | else 25 | { 26 | $res->writeHead(500); 27 | $res->end(); 28 | } 29 | } 30 | 31 | public function onPollRequest($req, $res) 32 | { 33 | if($this->req) 34 | { 35 | echo ('request overlap'); 36 | // assert: this.res, '.req and .res should be (un)set together' 37 | $this->onError('overlap from client'); 38 | $res->writeHead(500); 39 | return; 40 | } 41 | 42 | $this->req = $req; 43 | $this->res = $res; 44 | 45 | 46 | $req->onClose = array($this, 'pollRequestOnClose'); 47 | $req->cleanup = array($this, 'pollRequestClean'); 48 | 49 | $this->writable = true; 50 | $this->emit('drain'); 51 | 52 | // if we're still writable but had a pending close, trigger an empty send 53 | if ($this->writable && $this->shouldClose) 54 | { 55 | echo('triggering empty send to append close packet'); 56 | $this->send(array(array('type'=>'noop'))); 57 | } 58 | } 59 | 60 | public function pollRequestOnClose() 61 | { 62 | $this->onError('poll connection closed prematurely'); 63 | $this->pollRequestClean(); 64 | } 65 | 66 | public function pollRequestClean() 67 | { 68 | if(isset($this->req)) 69 | { 70 | $this->req->res = null; 71 | $this->req->onClose = $this->req->cleanup = null; 72 | $this->req = $this->res = null; 73 | } 74 | } 75 | 76 | public function onDataRequest($req, $res) 77 | { 78 | if(isset($this->dataReq)) 79 | { 80 | // assert: this.dataRes, '.dataReq and .dataRes should be (un)set together' 81 | $this->onError('data request overlap from client'); 82 | $res->writeHead(500); 83 | return; 84 | } 85 | 86 | $this->dataReq = $req; 87 | $this->dataRes = $res; 88 | $req->onClose = array($this, 'dataRequestOnClose'); 89 | $req->onData = array($this, 'dataRequestOnData'); 90 | $req->onEnd = array($this, 'dataRequestOnEnd'); 91 | } 92 | 93 | public function dataRequestCleanup() 94 | { 95 | $this->chunks = ''; 96 | $this->dataReq->res = null; 97 | $this->dataReq->onClose = $this->dataReq->onData = $this->dataReq->onEnd = null; 98 | $this->dataReq = $this->dataRes = null; 99 | } 100 | 101 | public function dataRequestOnClose() 102 | { 103 | $this->dataRequestCleanup(); 104 | $this->onError('data request connection closed prematurely'); 105 | } 106 | 107 | public function dataRequestOnData($req, $data) 108 | { 109 | $this->chunks .= $data; 110 | // todo maxHttpBufferSize 111 | /*if(strlen($this->chunks) > $this->maxHttpBufferSize) 112 | { 113 | $this->chunks = ''; 114 | $req->connection->destroy(); 115 | }*/ 116 | } 117 | 118 | public function dataRequestOnEnd () 119 | { 120 | $this->onData($this->chunks); 121 | 122 | $headers = array( 123 | 'Content-Type'=> 'text/html', 124 | 'Content-Length'=> 2, 125 | 'X-XSS-Protection' => '0', 126 | ); 127 | 128 | $this->dataRes->writeHead(200, '', $this->headers($this->dataReq, $headers)); 129 | $this->dataRes->end('ok'); 130 | $this->dataRequestCleanup(); 131 | } 132 | 133 | public function onData($data) 134 | { 135 | $packets = Parser::decodePayload($data); 136 | if(isset($packets['type'])) 137 | { 138 | if('close' === $packets['type']) 139 | { 140 | $this->onClose(); 141 | return false; 142 | } 143 | else 144 | { 145 | $packets = array($packets); 146 | } 147 | } 148 | 149 | foreach($packets as $packet) 150 | { 151 | $this->onPacket($packet); 152 | } 153 | } 154 | 155 | public function onClose() 156 | { 157 | if($this->writable) 158 | { 159 | // close pending poll request 160 | $this->send(array(array('type'=> 'noop'))); 161 | } 162 | parent::onClose(); 163 | } 164 | 165 | public function send($packets) 166 | { 167 | $this->writable = false; 168 | if($this->shouldClose) 169 | { 170 | echo('appending close packet to payload'); 171 | $packets[] = array('type'=>'close'); 172 | call_user_func($this->shouldClose); 173 | $this->shouldClose = null; 174 | } 175 | $data = Parser::encodePayload($packets, $this->supportsBinary); 176 | $this->write($data); 177 | } 178 | 179 | public function write($data) 180 | { 181 | $this->doWrite($data); 182 | if(!empty($this->req->cleanup)) 183 | { 184 | call_user_func($this->req->cleanup); 185 | } 186 | } 187 | 188 | public function doClose($fn) 189 | { 190 | if(!empty($this->dataReq)) 191 | { 192 | //echo('aborting ongoing data request'); 193 | $this->dataReq->destroy(); 194 | } 195 | 196 | if($this->writable) 197 | { 198 | //echo('transport writable - closing right away'); 199 | $this->send(array(array('type'=> 'close'))); 200 | call_user_func($fn); 201 | } 202 | else 203 | { 204 | //echo("transport not writable - buffering orderly close\n"); 205 | $this->shouldClose = $fn; 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Engine/Transports/PollingJsonp.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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | if(empty($event_name) || empty($this->_eventListenerMap[$event_name])) 84 | { 85 | return false; 86 | } 87 | foreach($this->_eventListenerMap[$event_name] as $key=>$item) 88 | { 89 | $args = func_get_args(); 90 | unset($args[0]); 91 | call_user_func_array($item[0], $args); 92 | // once ? 93 | if($item[1]) 94 | { 95 | unset($this->_eventListenerMap[$event_name][$key]); 96 | if(empty($this->_eventListenerMap[$event_name])) 97 | { 98 | unset($this->_eventListenerMap[$event_name]); 99 | } 100 | } 101 | } 102 | return true; 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/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 = new Socket($this, $client); 61 | if('open' === $client->conn->readyState) 62 | { 63 | $this->sockets[$socket->id]=$socket; 64 | $socket->onconnect(); 65 | if(!empty($fn)) call_user_func($fn, $socket, $nsp); 66 | $this->emit('connect', $socket); 67 | $this->emit('connection', $socket); 68 | } 69 | else 70 | { 71 | echo('next called after client was closed - ignoring socket'); 72 | } 73 | } 74 | 75 | 76 | /** 77 | * Removes a client. Called by each `Socket`. 78 | * 79 | * @api private 80 | */ 81 | 82 | public function remove($socket) 83 | { 84 | // todo $socket->id 85 | unset($this->sockets[$socket->id]); 86 | } 87 | 88 | 89 | /** 90 | * Emits to all clients. 91 | * 92 | * @return {Namespace} self 93 | * @api public 94 | */ 95 | 96 | public function emit($ev = null) 97 | { 98 | $args = func_get_args(); 99 | if (isset(self::$events[$ev])) 100 | { 101 | call_user_func_array(array($this, 'parent::emit'), $args); 102 | } 103 | else 104 | { 105 | // set up packet object 106 | 107 | $parserType = Parser::EVENT; // default 108 | //if (self::hasBin($args)) { $parserType = Parser::BINARY_EVENT; } // binary 109 | 110 | $packet = array('type'=> $parserType, 'data'=> $args ); 111 | 112 | if (is_callable(end($args))) 113 | { 114 | echo('Callbacks are not supported when broadcasting'); 115 | return; 116 | } 117 | 118 | $this->adapter->broadcast($packet, array( 119 | 'rooms'=> $this->rooms, 120 | 'flags'=> $this->flags 121 | )); 122 | 123 | $this->rooms = array(); 124 | $this->flags = array();; 125 | } 126 | return $this; 127 | } 128 | 129 | public function send() 130 | { 131 | $args = func_get_args(); 132 | array_unshift($args, 'message'); 133 | $this->emit($args); 134 | return $this; 135 | } 136 | 137 | public function write() 138 | { 139 | $args = func_get_args(); 140 | return call_user_func_array(array($this, 'send'), $args); 141 | } 142 | 143 | public function clients($fn) 144 | { 145 | $this->adapter->clients($this->rooms, $fn); 146 | return $this; 147 | } 148 | 149 | /** 150 | * Sets the compress flag. 151 | * 152 | * @param {Boolean} if `true`, compresses the sending data 153 | * @return {Socket} self 154 | * @api public 155 | */ 156 | 157 | public function compress($compress) 158 | { 159 | $this->flags['compress'] = $compress; 160 | return $this; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/Parser/Encoder.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 = $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($this, '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($this, '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 | -------------------------------------------------------------------------------- /src/SocketIO.php: -------------------------------------------------------------------------------- 1 | adapter($adapter); 18 | if(isset($opts['origins'])) 19 | { 20 | $this->origins($opts['origins']); 21 | } 22 | 23 | $this->sockets = $this->of('/'); 24 | 25 | if(!class_exists('Protocols\SocketIO')) 26 | { 27 | class_alias('PHPSocketIO\Engine\Protocols\SocketIO', 'Protocols\SocketIO'); 28 | } 29 | if($port) 30 | { 31 | $worker = new Worker('SocketIO://0.0.0.0:'.$port, $opts); 32 | $worker->name = 'PHPSocketIO'; 33 | 34 | if(isset($opts['ssl'])) { 35 | $worker->transport = 'ssl'; 36 | } 37 | 38 | $this->attach($worker); 39 | } 40 | } 41 | 42 | public function adapter($v = null) 43 | { 44 | if (empty($v)) return $this->_adapter; 45 | $this->_adapter = $v; 46 | foreach($this->nsps as $nsp) 47 | { 48 | $nsp->initAdapter(); 49 | } 50 | return $this; 51 | } 52 | 53 | public function origins($v = null) 54 | { 55 | if ($v === null) return $this->_origins; 56 | $this->_origins = $v; 57 | if(isset($this->engine)) { 58 | $this->engine->origins = $this->_origins; 59 | } 60 | return $this; 61 | } 62 | 63 | public function attach($srv, $opts = array()) 64 | { 65 | $engine = new Engine(); 66 | $this->eio = $engine->attach($srv, $opts); 67 | 68 | // Export http server 69 | $this->worker = $srv; 70 | 71 | // bind to engine events 72 | $this->bind($engine); 73 | 74 | return $this; 75 | } 76 | 77 | public function bind($engine) 78 | { 79 | $this->engine = $engine; 80 | $this->engine->on('connection', array($this, 'onConnection')); 81 | $this->engine->origins = $this->_origins; 82 | return $this; 83 | } 84 | 85 | public function of($name, $fn = null) 86 | { 87 | if($name[0] !== '/') 88 | { 89 | $name = "/$name"; 90 | } 91 | if(empty($this->nsps[$name])) 92 | { 93 | $this->nsps[$name] = new Nsp($this, $name); 94 | } 95 | if ($fn) 96 | { 97 | $this->nsps[$name]->on('connect', $fn); 98 | } 99 | return $this->nsps[$name]; 100 | } 101 | 102 | public function onConnection($engine_socket) 103 | { 104 | $client = new Client($this, $engine_socket); 105 | $client->connect('/'); 106 | return $this; 107 | } 108 | 109 | public function on() 110 | { 111 | if(func_get_arg(0) === 'workerStart') 112 | { 113 | $this->worker->onWorkerStart = func_get_arg(1); 114 | return; 115 | } 116 | return call_user_func_array(array($this->sockets, 'on'), func_get_args()); 117 | } 118 | 119 | public function in() 120 | { 121 | return call_user_func_array(array($this->sockets, 'in'), func_get_args()); 122 | } 123 | 124 | public function to() 125 | { 126 | return call_user_func_array(array($this->sockets, 'to'), func_get_args()); 127 | } 128 | 129 | public function emit() 130 | { 131 | return call_user_func_array(array($this->sockets, 'emit'), func_get_args()); 132 | } 133 | 134 | public function send() 135 | { 136 | return call_user_func_array(array($this->sockets, 'send'), func_get_args()); 137 | } 138 | 139 | public function write() 140 | { 141 | return call_user_func_array(array($this->sockets, 'write'), func_get_args()); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/autoload.php: -------------------------------------------------------------------------------- 1 | on('removeListener', function($event_name, $func){echo $event_name,':',var_export($func, true),"removed\n";}); 10 | $emitter->on('newListener', function($event_name, $func){echo $event_name,':',var_export($func, true)," added\n";}); 11 | $emitter->on('test', $func); 12 | $emitter->on('test', $func); 13 | $emitter->emit('test', 1 ,2); 14 | echo "----------------------\n"; 15 | $emitter->once('test', $func); 16 | $emitter->emit('test', 3 ,4); 17 | echo "----------------------\n"; 18 | $emitter->emit('test', 4 ,4); 19 | echo "----------------------\n"; 20 | $emitter->removeListener('test', $func)->emit('test', 5 ,6); 21 | echo "----------------------\n"; 22 | $emitter->on('test2', function(){echo "test2\n";}); 23 | 24 | var_dump($emitter->listeners('test2')); 25 | 26 | --------------------------------------------------------------------------------