├── .gitignore ├── config.json ├── package.json ├── public ├── lib │ ├── buffer.js │ └── engine.io.js ├── index.html ├── js │ ├── game.js │ └── app.js └── css │ └── main.css ├── server.js ├── clients.js ├── README.md └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lit-js", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node main.js" 9 | }, 10 | "author": "Huy", 11 | "license": "MIT", 12 | "dependencies": { 13 | "engine.io": "^1.6.9", 14 | "express": "^4.13.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /public/lib/buffer.js: -------------------------------------------------------------------------------- 1 | var BufferUtil = function() { 2 | return { 3 | from: function(str) { 4 | var buf = new ArrayBuffer(str.length*2); 5 | var bufView = new Uint8Array(buf); 6 | for (var i=0, strLen=str.length; i < strLen; i++) { 7 | bufView[i] = str.charCodeAt(i); 8 | } 9 | return buf; 10 | }, 11 | toString: function(binary) { 12 | return String.fromCharCode.apply(null, new Uint8Array(binary)); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var Client = require('./clients'); 2 | 3 | /* 4 | Main server logic - handle everything game-related here 5 | */ 6 | 7 | var Server = function() { 8 | 9 | return { 10 | 11 | connection: function(id) { 12 | console.log('Client', id, 'connected!'); 13 | console.log('Total:', Client().count()); 14 | 15 | // Send welcome message 16 | Client().get(id).emit('welcome', id); 17 | }, 18 | on: function(event, msg) { 19 | console.log('Server received'); 20 | console.log('Event:', event, 'Message:', msg); 21 | }, 22 | disconnect: function(id) { 23 | console.log('Client', id, 'disconnected!'); 24 | console.log('Total:', Client().count()); 25 | } 26 | 27 | } 28 | 29 | } 30 | 31 | module.exports = Server; 32 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Your Game 6 | 7 | 8 | 9 | 10 | 13 |
14 |
15 |

Your Online Game

16 | 17 | Nick name must be alphanumeric characters only! 18 |
19 | 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /clients.js: -------------------------------------------------------------------------------- 1 | global.SOCKET_CLIENTS = {}; 2 | var Client = function() { 3 | return { 4 | add: function(socket) { 5 | global.SOCKET_CLIENTS[socket.id] = socket; 6 | }, 7 | remove: function(socketId) { 8 | delete global.SOCKET_CLIENTS[socketId]; 9 | }, 10 | count: function() { 11 | return Object.keys(global.SOCKET_CLIENTS).length; 12 | }, 13 | get: function(id) { 14 | if (!global.SOCKET_CLIENTS[id]) return null; 15 | return { 16 | emit: function(eventName, data) { 17 | if (global.SOCKET_CLIENTS[id]) { 18 | global.SOCKET_CLIENTS[id].send(Buffer.from(JSON.stringify({ event: eventName, message: data }))); 19 | } 20 | }, 21 | broadcast: function(eventName, data) { 22 | Object.keys(global.SOCKET_CLIENTS).each(function(cid) { 23 | if (cid != id) { 24 | global.SOCKET_CLIENTS[cid].send(Buffer.from(JSON.stringify({ event: eventName, message: data }))); 25 | } 26 | }); 27 | } 28 | } 29 | } 30 | } 31 | } 32 | 33 | module.exports = Client; 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lit: a better WebSocket game server 2 | 3 | 4 | 5 | **lit** is under development! There are still many things to do, it is not ready for production. 6 | 7 | **Thank you so much [@Ulydev](https://github.com/ulydev) for the awesome logo!** 8 | 9 | ## Demo 10 | 11 | I made a demo game using this library, [check it out](https://github.com/huytd/lit-demo) 12 | 13 | ## Why this? 14 | Few months ago, I built a [cloned version of AgarIO](https://github.com/huytd/agar.io-clone). I'm still not happy with the networking part of the game yet. It still has some bugs, it's laggy and unable to handle the large amount of players. So I decided to quit using Socket.IO and moved to a lower level library called Engine.IO. It focuses on binary data instead of string data, thus improving performance. 15 | 16 | ## Getting started: 17 | 18 | #### Server 19 | 20 | ``` 21 | npm install 22 | ``` 23 | ``` 24 | npm start 25 | ``` 26 | 27 | Backend logic is located in ```server.js``` 28 | 29 | #### Client 30 | 31 | Navigate to ```http://localhost:3000/``` 32 | 33 | Game logic is located in ```public/js/game.js``` 34 | 35 | ## License 36 | Published under MIT License. 37 | -------------------------------------------------------------------------------- /public/js/game.js: -------------------------------------------------------------------------------- 1 | function Game () { }; 2 | 3 | Game.prototype.handleNetwork = function(socket) { 4 | //Network callback 5 | 6 | socket.on('open', function() { 7 | socket.on('message', function(data) { 8 | console.log('Received:', BufferUtil().toString(data)); 9 | socket.send(BufferUtil().from(JSON.stringify({ 10 | event: 'reply', 11 | message: { 12 | text: 'Hey there, server! My name is ' + playerName 13 | } 14 | }))); 15 | }); 16 | socket.on('close', function(){}); 17 | }); 18 | } 19 | 20 | Game.prototype.handleLogic = function() { 21 | //Update loop 22 | 23 | console.log('Game updated'); 24 | } 25 | 26 | Game.prototype.handleGraphics = function(graphics) { 27 | //Draw loop 28 | 29 | graphics.fillStyle = '#fbfcfc'; 30 | graphics.fillRect(0, 0, screenWidth, screenHeight); 31 | 32 | graphics.fillStyle = '#2ecc71'; 33 | graphics.strokeStyle = '#27ae60'; 34 | graphics.font = 'bold 50px Verdana'; 35 | graphics.textAlign = 'center'; 36 | graphics.lineWidth = 2; 37 | graphics.fillText('Connected as ' + playerName, screenWidth / 2, screenHeight / 2); 38 | graphics.strokeText('Connected as ' + playerName, screenWidth / 2, screenHeight / 2); 39 | } 40 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var engine = require('engine.io'); 2 | var engineServer = engine.listen(5000); 3 | var http = require('express')(); 4 | var Server = require('./server')(); 5 | var Client = require('./clients'); 6 | 7 | var config = require('./config.json'); 8 | 9 | /* 10 | Socket backend - This part should not be edited 11 | */ 12 | 13 | engineServer.on('connection', function(socket) { 14 | 15 | if (!Client().get(socket.id)) { 16 | Client().add(socket); 17 | } 18 | Server.connection(socket.id); 19 | 20 | socket.on('message', function(data){ 21 | var stringified = data.toString('utf-8').replace(/\0/g, ''); //Take \0 ending character into account 22 | var req = JSON.parse(stringified); 23 | Server.on(req.event, req.message); 24 | }); 25 | 26 | socket.on('close', function(){ 27 | if (Client().get(socket.id)) { 28 | Client().remove(socket.id); 29 | Server.disconnect(socket.id); 30 | } 31 | }); 32 | 33 | }); 34 | 35 | // CLIENT 36 | 37 | http.use(require('express').static('public')); 38 | 39 | http.get('/', function(req, res) { 40 | res.sendFile('public/index.html'); 41 | }); 42 | 43 | var port = process.env.PORT || config.port; 44 | http.listen(3000, function(){ 45 | console.log('Server is listening on port 3000'); 46 | }); 47 | -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: sans-serif; 3 | font-size: 14px; 4 | } 5 | 6 | html, body { 7 | margin: 0; padding: 0; 8 | width: 100%; height: 100%; 9 | background-color: #222; 10 | overflow: hidden; 11 | } 12 | 13 | canvas { 14 | width: 100%; height: 100%; 15 | margin: 0; padding: 0; 16 | } 17 | 18 | #startMenu { 19 | margin:auto; 20 | margin-top:100px; 21 | width: 350px; 22 | padding: 20px; 23 | border-radius: 5px 5px 5px 5px; 24 | -moz-border-radius: 5px 5px 5px 5px; 25 | -webkit-border-radius: 5px 5px 5px 5px; 26 | background-color: white; 27 | box-sizing: border-box; 28 | } 29 | 30 | #startMenu p { 31 | padding: 0; 32 | text-align:center; 33 | font-size: x-large; 34 | font-weight: bold; 35 | } 36 | 37 | #playerNameInput { 38 | width: 100%; 39 | text-align: center; 40 | padding: 10px; 41 | border: solid 1px #dcdcdc; 42 | transition: box-shadow 0.3s, border 0.3s; 43 | box-sizing: border-box; 44 | border-radius: 5px 5px 5px 5px; 45 | -moz-border-radius: 5px 5px 5px 5px; 46 | -webkit-border-radius: 5px 5px 5px 5px; 47 | margin-bottom: 10px; 48 | outline: none; 49 | } 50 | 51 | #playerNameInput:focus, #playerNameInput.focus { 52 | border: solid 1px #CCCCCC; 53 | box-shadow: 0 0 3px 1px #DDDDDD; 54 | } 55 | 56 | #startButton { 57 | position: relative; 58 | margin: auto; 59 | margin-top: 10px; 60 | width: 100%; 61 | height: 40px; 62 | box-sizing: border-box; 63 | font-size: large; 64 | color: white; 65 | text-align: center; 66 | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); 67 | background: #2ecc71; 68 | border: 0; 69 | border-bottom: 2px solid #28be68; 70 | cursor: pointer; 71 | -webkit-box-shadow: inset 0 -2px #28be68; 72 | box-shadow: inset 0 -2px #28be68; 73 | border-radius: 5px 5px 5px 5px; 74 | -moz-border-radius: 5px 5px 5px 5px; 75 | -webkit-border-radius: 5px 5px 5px 5px; 76 | margin-bottom: 10px; 77 | } 78 | 79 | #startButton:active, #startButton:hover { 80 | top: 1px; 81 | background: #55D88B; 82 | outline: none; 83 | -webkit-box-shadow: none; 84 | box-shadow: none; 85 | } 86 | 87 | #startMenu h3 { 88 | padding-bottom: 0; 89 | margin-bottom: 0; 90 | } 91 | 92 | #startMenu ul { 93 | margin: 10px; padding: 10px; 94 | margin-top: 0; 95 | } 96 | 97 | #startMenu .input-error { 98 | color : red; 99 | display: none; 100 | font-size : 12px; 101 | } 102 | -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | var socket; 4 | 5 | // 6 | 7 | var playerName; 8 | 9 | var btn; 10 | var nickErrorText; 11 | var playerNameInput; 12 | 13 | // 14 | 15 | var screenWidth = window.innerWidth; 16 | var screenHeight = window.innerHeight; 17 | 18 | var c = document.getElementById('cvs'); 19 | var canvas = c.getContext('2d'); 20 | c.width = screenWidth; c.height = screenHeight; 21 | 22 | var KEY_ENTER = 13; 23 | 24 | // 25 | 26 | var game = new Game(); 27 | 28 | // 29 | 30 | function startGame() { 31 | document.getElementById('gameAreaWrapper').style.display = 'block'; 32 | document.getElementById('startMenuWrapper').style.display = 'none'; 33 | 34 | playerName = playerNameInput.value.replace(/(<([^>]+)>)/ig, ''); 35 | 36 | //Set up socket 37 | socket = new eio.Socket('ws://localhost:5000/'); 38 | game.handleNetwork(socket); 39 | 40 | //Start loop 41 | windowLoop(); 42 | } 43 | 44 | function windowLoop () { 45 | requestAnimFrame(windowLoop); 46 | gameLoop(); 47 | } 48 | 49 | function gameLoop () { 50 | game.handleLogic(); 51 | game.handleGraphics(canvas); 52 | } 53 | 54 | //Check nick and start game 55 | function checkNick() { 56 | if (validNick()) { 57 | startGame(); 58 | } else { 59 | nickErrorText.style.display = 'inline'; 60 | } 61 | } 62 | 63 | //Check if nick is alphanumeric 64 | function validNick() { 65 | var regex = /^\w*$/; 66 | console.log('Regex Test', regex.exec(playerNameInput.value)); 67 | return regex.exec(playerNameInput.value) !== null; 68 | } 69 | 70 | //Set up form 71 | window.onload = function() { 72 | 'use strict'; 73 | 74 | btn = document.getElementById('startButton'); 75 | nickErrorText = document.querySelector('#startMenu .input-error'); 76 | playerNameInput = document.getElementById('playerNameInput') 77 | 78 | btn.onclick = checkNick; //Check nick on click 79 | 80 | playerNameInput.addEventListener('keypress', function (e) { 81 | var key = e.which || e.keyCode; 82 | 83 | if (key === KEY_ENTER) { 84 | checkNick(); 85 | } 86 | }); 87 | }; 88 | 89 | //Define animation frame 90 | window.requestAnimFrame = (function () { 91 | return window.requestAnimationFrame || 92 | window.webkitRequestAnimationFrame || 93 | window.mozRequestAnimationFrame || 94 | function( callback ){ 95 | window.setTimeout(callback, 1000 / 60); 96 | }; 97 | })(); 98 | 99 | //Resize event 100 | window.addEventListener('resize', function() { 101 | screenWidth = window.innerWidth; 102 | screenHeight = window.innerHeight; 103 | c.width = screenWidth; 104 | c.height = screenHeight; 105 | }, true); 106 | -------------------------------------------------------------------------------- /public/lib/engine.io.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.eio=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) { 125 | this.extraHeaders = opts.extraHeaders; 126 | } 127 | } 128 | 129 | this.open(); 130 | } 131 | 132 | Socket.priorWebsocketSuccess = false; 133 | 134 | /** 135 | * Mix in `Emitter`. 136 | */ 137 | 138 | Emitter(Socket.prototype); 139 | 140 | /** 141 | * Protocol version. 142 | * 143 | * @api public 144 | */ 145 | 146 | Socket.protocol = parser.protocol; // this is an int 147 | 148 | /** 149 | * Expose deps for legacy compatibility 150 | * and standalone browser access. 151 | */ 152 | 153 | Socket.Socket = Socket; 154 | Socket.Transport = _dereq_('./transport'); 155 | Socket.transports = _dereq_('./transports'); 156 | Socket.parser = _dereq_('engine.io-parser'); 157 | 158 | /** 159 | * Creates transport of the given type. 160 | * 161 | * @param {String} transport name 162 | * @return {Transport} 163 | * @api private 164 | */ 165 | 166 | Socket.prototype.createTransport = function (name) { 167 | debug('creating transport "%s"', name); 168 | var query = clone(this.query); 169 | 170 | // append engine.io protocol identifier 171 | query.EIO = parser.protocol; 172 | 173 | // transport name 174 | query.transport = name; 175 | 176 | // session id if we already have one 177 | if (this.id) query.sid = this.id; 178 | 179 | var transport = new transports[name]({ 180 | agent: this.agent, 181 | hostname: this.hostname, 182 | port: this.port, 183 | secure: this.secure, 184 | path: this.path, 185 | query: query, 186 | forceJSONP: this.forceJSONP, 187 | jsonp: this.jsonp, 188 | forceBase64: this.forceBase64, 189 | enablesXDR: this.enablesXDR, 190 | timestampRequests: this.timestampRequests, 191 | timestampParam: this.timestampParam, 192 | policyPort: this.policyPort, 193 | socket: this, 194 | pfx: this.pfx, 195 | key: this.key, 196 | passphrase: this.passphrase, 197 | cert: this.cert, 198 | ca: this.ca, 199 | ciphers: this.ciphers, 200 | rejectUnauthorized: this.rejectUnauthorized, 201 | perMessageDeflate: this.perMessageDeflate, 202 | extraHeaders: this.extraHeaders 203 | }); 204 | 205 | return transport; 206 | }; 207 | 208 | function clone (obj) { 209 | var o = {}; 210 | for (var i in obj) { 211 | if (obj.hasOwnProperty(i)) { 212 | o[i] = obj[i]; 213 | } 214 | } 215 | return o; 216 | } 217 | 218 | /** 219 | * Initializes transport to use and starts probe. 220 | * 221 | * @api private 222 | */ 223 | Socket.prototype.open = function () { 224 | var transport; 225 | if (this.rememberUpgrade && Socket.priorWebsocketSuccess && this.transports.indexOf('websocket') != -1) { 226 | transport = 'websocket'; 227 | } else if (0 === this.transports.length) { 228 | // Emit error on next tick so it can be listened to 229 | var self = this; 230 | setTimeout(function() { 231 | self.emit('error', 'No transports available'); 232 | }, 0); 233 | return; 234 | } else { 235 | transport = this.transports[0]; 236 | } 237 | this.readyState = 'opening'; 238 | 239 | // Retry with the next transport if the transport is disabled (jsonp: false) 240 | try { 241 | transport = this.createTransport(transport); 242 | } catch (e) { 243 | this.transports.shift(); 244 | this.open(); 245 | return; 246 | } 247 | 248 | transport.open(); 249 | this.setTransport(transport); 250 | }; 251 | 252 | /** 253 | * Sets the current transport. Disables the existing one (if any). 254 | * 255 | * @api private 256 | */ 257 | 258 | Socket.prototype.setTransport = function(transport){ 259 | debug('setting transport %s', transport.name); 260 | var self = this; 261 | 262 | if (this.transport) { 263 | debug('clearing existing transport %s', this.transport.name); 264 | this.transport.removeAllListeners(); 265 | } 266 | 267 | // set up transport 268 | this.transport = transport; 269 | 270 | // set up transport listeners 271 | transport 272 | .on('drain', function(){ 273 | self.onDrain(); 274 | }) 275 | .on('packet', function(packet){ 276 | self.onPacket(packet); 277 | }) 278 | .on('error', function(e){ 279 | self.onError(e); 280 | }) 281 | .on('close', function(){ 282 | self.onClose('transport close'); 283 | }); 284 | }; 285 | 286 | /** 287 | * Probes a transport. 288 | * 289 | * @param {String} transport name 290 | * @api private 291 | */ 292 | 293 | Socket.prototype.probe = function (name) { 294 | debug('probing transport "%s"', name); 295 | var transport = this.createTransport(name, { probe: 1 }) 296 | , failed = false 297 | , self = this; 298 | 299 | Socket.priorWebsocketSuccess = false; 300 | 301 | function onTransportOpen(){ 302 | if (self.onlyBinaryUpgrades) { 303 | var upgradeLosesBinary = !this.supportsBinary && self.transport.supportsBinary; 304 | failed = failed || upgradeLosesBinary; 305 | } 306 | if (failed) return; 307 | 308 | debug('probe transport "%s" opened', name); 309 | transport.send([{ type: 'ping', data: 'probe' }]); 310 | transport.once('packet', function (msg) { 311 | if (failed) return; 312 | if ('pong' == msg.type && 'probe' == msg.data) { 313 | debug('probe transport "%s" pong', name); 314 | self.upgrading = true; 315 | self.emit('upgrading', transport); 316 | if (!transport) return; 317 | Socket.priorWebsocketSuccess = 'websocket' == transport.name; 318 | 319 | debug('pausing current transport "%s"', self.transport.name); 320 | self.transport.pause(function () { 321 | if (failed) return; 322 | if ('closed' == self.readyState) return; 323 | debug('changing transport and sending upgrade packet'); 324 | 325 | cleanup(); 326 | 327 | self.setTransport(transport); 328 | transport.send([{ type: 'upgrade' }]); 329 | self.emit('upgrade', transport); 330 | transport = null; 331 | self.upgrading = false; 332 | self.flush(); 333 | }); 334 | } else { 335 | debug('probe transport "%s" failed', name); 336 | var err = new Error('probe error'); 337 | err.transport = transport.name; 338 | self.emit('upgradeError', err); 339 | } 340 | }); 341 | } 342 | 343 | function freezeTransport() { 344 | if (failed) return; 345 | 346 | // Any callback called by transport should be ignored since now 347 | failed = true; 348 | 349 | cleanup(); 350 | 351 | transport.close(); 352 | transport = null; 353 | } 354 | 355 | //Handle any error that happens while probing 356 | function onerror(err) { 357 | var error = new Error('probe error: ' + err); 358 | error.transport = transport.name; 359 | 360 | freezeTransport(); 361 | 362 | debug('probe transport "%s" failed because of error: %s', name, err); 363 | 364 | self.emit('upgradeError', error); 365 | } 366 | 367 | function onTransportClose(){ 368 | onerror("transport closed"); 369 | } 370 | 371 | //When the socket is closed while we're probing 372 | function onclose(){ 373 | onerror("socket closed"); 374 | } 375 | 376 | //When the socket is upgraded while we're probing 377 | function onupgrade(to){ 378 | if (transport && to.name != transport.name) { 379 | debug('"%s" works - aborting "%s"', to.name, transport.name); 380 | freezeTransport(); 381 | } 382 | } 383 | 384 | //Remove all listeners on the transport and on self 385 | function cleanup(){ 386 | transport.removeListener('open', onTransportOpen); 387 | transport.removeListener('error', onerror); 388 | transport.removeListener('close', onTransportClose); 389 | self.removeListener('close', onclose); 390 | self.removeListener('upgrading', onupgrade); 391 | } 392 | 393 | transport.once('open', onTransportOpen); 394 | transport.once('error', onerror); 395 | transport.once('close', onTransportClose); 396 | 397 | this.once('close', onclose); 398 | this.once('upgrading', onupgrade); 399 | 400 | transport.open(); 401 | 402 | }; 403 | 404 | /** 405 | * Called when connection is deemed open. 406 | * 407 | * @api public 408 | */ 409 | 410 | Socket.prototype.onOpen = function () { 411 | debug('socket open'); 412 | this.readyState = 'open'; 413 | Socket.priorWebsocketSuccess = 'websocket' == this.transport.name; 414 | this.emit('open'); 415 | this.flush(); 416 | 417 | // we check for `readyState` in case an `open` 418 | // listener already closed the socket 419 | if ('open' == this.readyState && this.upgrade && this.transport.pause) { 420 | debug('starting upgrade probes'); 421 | for (var i = 0, l = this.upgrades.length; i < l; i++) { 422 | this.probe(this.upgrades[i]); 423 | } 424 | } 425 | }; 426 | 427 | /** 428 | * Handles a packet. 429 | * 430 | * @api private 431 | */ 432 | 433 | Socket.prototype.onPacket = function (packet) { 434 | if ('opening' == this.readyState || 'open' == this.readyState) { 435 | debug('socket receive: type "%s", data "%s"', packet.type, packet.data); 436 | 437 | this.emit('packet', packet); 438 | 439 | // Socket is live - any packet counts 440 | this.emit('heartbeat'); 441 | 442 | switch (packet.type) { 443 | case 'open': 444 | this.onHandshake(parsejson(packet.data)); 445 | break; 446 | 447 | case 'pong': 448 | this.setPing(); 449 | this.emit('pong'); 450 | break; 451 | 452 | case 'error': 453 | var err = new Error('server error'); 454 | err.code = packet.data; 455 | this.onError(err); 456 | break; 457 | 458 | case 'message': 459 | this.emit('data', packet.data); 460 | this.emit('message', packet.data); 461 | break; 462 | } 463 | } else { 464 | debug('packet received with socket readyState "%s"', this.readyState); 465 | } 466 | }; 467 | 468 | /** 469 | * Called upon handshake completion. 470 | * 471 | * @param {Object} handshake obj 472 | * @api private 473 | */ 474 | 475 | Socket.prototype.onHandshake = function (data) { 476 | this.emit('handshake', data); 477 | this.id = data.sid; 478 | this.transport.query.sid = data.sid; 479 | this.upgrades = this.filterUpgrades(data.upgrades); 480 | this.pingInterval = data.pingInterval; 481 | this.pingTimeout = data.pingTimeout; 482 | this.onOpen(); 483 | // In case open handler closes socket 484 | if ('closed' == this.readyState) return; 485 | this.setPing(); 486 | 487 | // Prolong liveness of socket on heartbeat 488 | this.removeListener('heartbeat', this.onHeartbeat); 489 | this.on('heartbeat', this.onHeartbeat); 490 | }; 491 | 492 | /** 493 | * Resets ping timeout. 494 | * 495 | * @api private 496 | */ 497 | 498 | Socket.prototype.onHeartbeat = function (timeout) { 499 | clearTimeout(this.pingTimeoutTimer); 500 | var self = this; 501 | self.pingTimeoutTimer = setTimeout(function () { 502 | if ('closed' == self.readyState) return; 503 | self.onClose('ping timeout'); 504 | }, timeout || (self.pingInterval + self.pingTimeout)); 505 | }; 506 | 507 | /** 508 | * Pings server every `this.pingInterval` and expects response 509 | * within `this.pingTimeout` or closes connection. 510 | * 511 | * @api private 512 | */ 513 | 514 | Socket.prototype.setPing = function () { 515 | var self = this; 516 | clearTimeout(self.pingIntervalTimer); 517 | self.pingIntervalTimer = setTimeout(function () { 518 | debug('writing ping packet - expecting pong within %sms', self.pingTimeout); 519 | self.ping(); 520 | self.onHeartbeat(self.pingTimeout); 521 | }, self.pingInterval); 522 | }; 523 | 524 | /** 525 | * Sends a ping packet. 526 | * 527 | * @api private 528 | */ 529 | 530 | Socket.prototype.ping = function () { 531 | var self = this; 532 | this.sendPacket('ping', function(){ 533 | self.emit('ping'); 534 | }); 535 | }; 536 | 537 | /** 538 | * Called on `drain` event 539 | * 540 | * @api private 541 | */ 542 | 543 | Socket.prototype.onDrain = function() { 544 | this.writeBuffer.splice(0, this.prevBufferLen); 545 | 546 | // setting prevBufferLen = 0 is very important 547 | // for example, when upgrading, upgrade packet is sent over, 548 | // and a nonzero prevBufferLen could cause problems on `drain` 549 | this.prevBufferLen = 0; 550 | 551 | if (0 === this.writeBuffer.length) { 552 | this.emit('drain'); 553 | } else { 554 | this.flush(); 555 | } 556 | }; 557 | 558 | /** 559 | * Flush write buffers. 560 | * 561 | * @api private 562 | */ 563 | 564 | Socket.prototype.flush = function () { 565 | if ('closed' != this.readyState && this.transport.writable && 566 | !this.upgrading && this.writeBuffer.length) { 567 | debug('flushing %d packets in socket', this.writeBuffer.length); 568 | this.transport.send(this.writeBuffer); 569 | // keep track of current length of writeBuffer 570 | // splice writeBuffer and callbackBuffer on `drain` 571 | this.prevBufferLen = this.writeBuffer.length; 572 | this.emit('flush'); 573 | } 574 | }; 575 | 576 | /** 577 | * Sends a message. 578 | * 579 | * @param {String} message. 580 | * @param {Function} callback function. 581 | * @param {Object} options. 582 | * @return {Socket} for chaining. 583 | * @api public 584 | */ 585 | 586 | Socket.prototype.write = 587 | Socket.prototype.send = function (msg, options, fn) { 588 | this.sendPacket('message', msg, options, fn); 589 | return this; 590 | }; 591 | 592 | /** 593 | * Sends a packet. 594 | * 595 | * @param {String} packet type. 596 | * @param {String} data. 597 | * @param {Object} options. 598 | * @param {Function} callback function. 599 | * @api private 600 | */ 601 | 602 | Socket.prototype.sendPacket = function (type, data, options, fn) { 603 | if('function' == typeof data) { 604 | fn = data; 605 | data = undefined; 606 | } 607 | 608 | if ('function' == typeof options) { 609 | fn = options; 610 | options = null; 611 | } 612 | 613 | if ('closing' == this.readyState || 'closed' == this.readyState) { 614 | return; 615 | } 616 | 617 | options = options || {}; 618 | options.compress = false !== options.compress; 619 | 620 | var packet = { 621 | type: type, 622 | data: data, 623 | options: options 624 | }; 625 | this.emit('packetCreate', packet); 626 | this.writeBuffer.push(packet); 627 | if (fn) this.once('flush', fn); 628 | this.flush(); 629 | }; 630 | 631 | /** 632 | * Closes the connection. 633 | * 634 | * @api private 635 | */ 636 | 637 | Socket.prototype.close = function () { 638 | if ('opening' == this.readyState || 'open' == this.readyState) { 639 | this.readyState = 'closing'; 640 | 641 | var self = this; 642 | 643 | if (this.writeBuffer.length) { 644 | this.once('drain', function() { 645 | if (this.upgrading) { 646 | waitForUpgrade(); 647 | } else { 648 | close(); 649 | } 650 | }); 651 | } else if (this.upgrading) { 652 | waitForUpgrade(); 653 | } else { 654 | close(); 655 | } 656 | } 657 | 658 | function close() { 659 | self.onClose('forced close'); 660 | debug('socket closing - telling transport to close'); 661 | self.transport.close(); 662 | } 663 | 664 | function cleanupAndClose() { 665 | self.removeListener('upgrade', cleanupAndClose); 666 | self.removeListener('upgradeError', cleanupAndClose); 667 | close(); 668 | } 669 | 670 | function waitForUpgrade() { 671 | // wait for upgrade to finish since we can't send packets while pausing a transport 672 | self.once('upgrade', cleanupAndClose); 673 | self.once('upgradeError', cleanupAndClose); 674 | } 675 | 676 | return this; 677 | }; 678 | 679 | /** 680 | * Called upon transport error 681 | * 682 | * @api private 683 | */ 684 | 685 | Socket.prototype.onError = function (err) { 686 | debug('socket error %j', err); 687 | Socket.priorWebsocketSuccess = false; 688 | this.emit('error', err); 689 | this.onClose('transport error', err); 690 | }; 691 | 692 | /** 693 | * Called upon transport close. 694 | * 695 | * @api private 696 | */ 697 | 698 | Socket.prototype.onClose = function (reason, desc) { 699 | if ('opening' == this.readyState || 'open' == this.readyState || 'closing' == this.readyState) { 700 | debug('socket close with reason: "%s"', reason); 701 | var self = this; 702 | 703 | // clear timers 704 | clearTimeout(this.pingIntervalTimer); 705 | clearTimeout(this.pingTimeoutTimer); 706 | 707 | // stop event from firing again for transport 708 | this.transport.removeAllListeners('close'); 709 | 710 | // ensure transport won't stay open 711 | this.transport.close(); 712 | 713 | // ignore further transport communication 714 | this.transport.removeAllListeners(); 715 | 716 | // set ready state 717 | this.readyState = 'closed'; 718 | 719 | // clear session id 720 | this.id = null; 721 | 722 | // emit close event 723 | this.emit('close', reason, desc); 724 | 725 | // clean buffers after, so users can still 726 | // grab the buffers on `close` event 727 | self.writeBuffer = []; 728 | self.prevBufferLen = 0; 729 | } 730 | }; 731 | 732 | /** 733 | * Filters upgrades, returning only those matching client transports. 734 | * 735 | * @param {Array} server upgrades 736 | * @api private 737 | * 738 | */ 739 | 740 | Socket.prototype.filterUpgrades = function (upgrades) { 741 | var filteredUpgrades = []; 742 | for (var i = 0, j = upgrades.length; i