├── 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,''."\0";
46 | $connection->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 |
--------------------------------------------------------------------------------