123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486 |
- /**
- * Module dependencies.
- */
-
- var EventEmitter = require('events').EventEmitter;
- var util = require('util');
- var debug = require('debug')('engine:socket');
-
- /**
- * Module exports.
- */
-
- module.exports = Socket;
-
- /**
- * Client class (abstract).
- *
- * @api private
- */
-
- function Socket (id, server, transport, req) {
- this.id = id;
- this.server = server;
- this.upgrading = false;
- this.upgraded = false;
- this.readyState = 'opening';
- this.writeBuffer = [];
- this.packetsFn = [];
- this.sentCallbackFn = [];
- this.cleanupFn = [];
- this.request = req;
-
- // Cache IP since it might not be in the req later
- if (req.websocket && req.websocket._socket) {
- this.remoteAddress = req.websocket._socket.remoteAddress;
- } else {
- this.remoteAddress = req.connection.remoteAddress;
- }
-
- this.checkIntervalTimer = null;
- this.upgradeTimeoutTimer = null;
- this.pingTimeoutTimer = null;
-
- this.setTransport(transport);
- this.onOpen();
- }
-
- /**
- * Inherits from EventEmitter.
- */
-
- util.inherits(Socket, EventEmitter);
-
- /**
- * Called upon transport considered open.
- *
- * @api private
- */
-
- Socket.prototype.onOpen = function () {
- this.readyState = 'open';
-
- // sends an `open` packet
- this.transport.sid = this.id;
- this.sendPacket('open', JSON.stringify({
- sid: this.id,
- upgrades: this.getAvailableUpgrades(),
- pingInterval: this.server.pingInterval,
- pingTimeout: this.server.pingTimeout
- }));
-
- if (this.server.initialPacket) {
- this.sendPacket('message', this.server.initialPacket);
- }
-
- this.emit('open');
- this.setPingTimeout();
- };
-
- /**
- * Called upon transport packet.
- *
- * @param {Object} packet
- * @api private
- */
-
- Socket.prototype.onPacket = function (packet) {
- if ('open' === this.readyState) {
- // export packet event
- debug('packet');
- this.emit('packet', packet);
-
- // Reset ping timeout on any packet, incoming data is a good sign of
- // other side's liveness
- this.setPingTimeout();
-
- switch (packet.type) {
- case 'ping':
- debug('got ping');
- this.sendPacket('pong');
- this.emit('heartbeat');
- break;
-
- case 'error':
- this.onClose('parse error');
- break;
-
- case 'message':
- this.emit('data', packet.data);
- this.emit('message', packet.data);
- break;
- }
- } else {
- debug('packet received with closed socket');
- }
- };
-
- /**
- * Called upon transport error.
- *
- * @param {Error} error object
- * @api private
- */
-
- Socket.prototype.onError = function (err) {
- debug('transport error');
- this.onClose('transport error', err);
- };
-
- /**
- * Sets and resets ping timeout timer based on client pings.
- *
- * @api private
- */
-
- Socket.prototype.setPingTimeout = function () {
- var self = this;
- clearTimeout(self.pingTimeoutTimer);
- self.pingTimeoutTimer = setTimeout(function () {
- self.onClose('ping timeout');
- }, self.server.pingInterval + self.server.pingTimeout);
- };
-
- /**
- * Attaches handlers for the given transport.
- *
- * @param {Transport} transport
- * @api private
- */
-
- Socket.prototype.setTransport = function (transport) {
- var onError = this.onError.bind(this);
- var onPacket = this.onPacket.bind(this);
- var flush = this.flush.bind(this);
- var onClose = this.onClose.bind(this, 'transport close');
-
- this.transport = transport;
- this.transport.once('error', onError);
- this.transport.on('packet', onPacket);
- this.transport.on('drain', flush);
- this.transport.once('close', onClose);
- // this function will manage packet events (also message callbacks)
- this.setupSendCallback();
-
- this.cleanupFn.push(function () {
- transport.removeListener('error', onError);
- transport.removeListener('packet', onPacket);
- transport.removeListener('drain', flush);
- transport.removeListener('close', onClose);
- });
- };
-
- /**
- * Upgrades socket to the given transport
- *
- * @param {Transport} transport
- * @api private
- */
-
- Socket.prototype.maybeUpgrade = function (transport) {
- debug('might upgrade socket transport from "%s" to "%s"'
- , this.transport.name, transport.name);
-
- this.upgrading = true;
-
- var self = this;
-
- // set transport upgrade timer
- self.upgradeTimeoutTimer = setTimeout(function () {
- debug('client did not complete upgrade - closing transport');
- cleanup();
- if ('open' === transport.readyState) {
- transport.close();
- }
- }, this.server.upgradeTimeout);
-
- function onPacket (packet) {
- if ('ping' === packet.type && 'probe' === packet.data) {
- transport.send([{ type: 'pong', data: 'probe' }]);
- self.emit('upgrading', transport);
- clearInterval(self.checkIntervalTimer);
- self.checkIntervalTimer = setInterval(check, 100);
- } else if ('upgrade' === packet.type && self.readyState !== 'closed') {
- debug('got upgrade packet - upgrading');
- cleanup();
- self.transport.discard();
- self.upgraded = true;
- self.clearTransport();
- self.setTransport(transport);
- self.emit('upgrade', transport);
- self.setPingTimeout();
- self.flush();
- if (self.readyState === 'closing') {
- transport.close(function () {
- self.onClose('forced close');
- });
- }
- } else {
- cleanup();
- transport.close();
- }
- }
-
- // we force a polling cycle to ensure a fast upgrade
- function check () {
- if ('polling' === self.transport.name && self.transport.writable) {
- debug('writing a noop packet to polling for fast upgrade');
- self.transport.send([{ type: 'noop' }]);
- }
- }
-
- function cleanup () {
- self.upgrading = false;
-
- clearInterval(self.checkIntervalTimer);
- self.checkIntervalTimer = null;
-
- clearTimeout(self.upgradeTimeoutTimer);
- self.upgradeTimeoutTimer = null;
-
- transport.removeListener('packet', onPacket);
- transport.removeListener('close', onTransportClose);
- transport.removeListener('error', onError);
- self.removeListener('close', onClose);
- }
-
- function onError (err) {
- debug('client did not complete upgrade - %s', err);
- cleanup();
- transport.close();
- transport = null;
- }
-
- function onTransportClose () {
- onError('transport closed');
- }
-
- function onClose () {
- onError('socket closed');
- }
-
- transport.on('packet', onPacket);
- transport.once('close', onTransportClose);
- transport.once('error', onError);
-
- self.once('close', onClose);
- };
-
- /**
- * Clears listeners and timers associated with current transport.
- *
- * @api private
- */
-
- Socket.prototype.clearTransport = function () {
- var cleanup;
-
- var toCleanUp = this.cleanupFn.length;
-
- for (var i = 0; i < toCleanUp; i++) {
- cleanup = this.cleanupFn.shift();
- cleanup();
- }
-
- // silence further transport errors and prevent uncaught exceptions
- this.transport.on('error', function () {
- debug('error triggered by discarded transport');
- });
-
- // ensure transport won't stay open
- this.transport.close();
-
- clearTimeout(this.pingTimeoutTimer);
- };
-
- /**
- * Called upon transport considered closed.
- * Possible reasons: `ping timeout`, `client error`, `parse error`,
- * `transport error`, `server close`, `transport close`
- */
-
- Socket.prototype.onClose = function (reason, description) {
- if ('closed' !== this.readyState) {
- this.readyState = 'closed';
- clearTimeout(this.pingTimeoutTimer);
- clearInterval(this.checkIntervalTimer);
- this.checkIntervalTimer = null;
- clearTimeout(this.upgradeTimeoutTimer);
- var self = this;
- // clean writeBuffer in next tick, so developers can still
- // grab the writeBuffer on 'close' event
- process.nextTick(function () {
- self.writeBuffer = [];
- });
- this.packetsFn = [];
- this.sentCallbackFn = [];
- this.clearTransport();
- this.emit('close', reason, description);
- }
- };
-
- /**
- * Setup and manage send callback
- *
- * @api private
- */
-
- Socket.prototype.setupSendCallback = function () {
- var self = this;
- this.transport.on('drain', onDrain);
-
- this.cleanupFn.push(function () {
- self.transport.removeListener('drain', onDrain);
- });
-
- // the message was sent successfully, execute the callback
- function onDrain () {
- if (self.sentCallbackFn.length > 0) {
- var seqFn = self.sentCallbackFn.splice(0, 1)[0];
- if ('function' === typeof seqFn) {
- debug('executing send callback');
- seqFn(self.transport);
- } else if (Array.isArray(seqFn)) {
- debug('executing batch send callback');
- for (var l = seqFn.length, i = 0; i < l; i++) {
- if ('function' === typeof seqFn[i]) {
- seqFn[i](self.transport);
- }
- }
- }
- }
- }
- };
-
- /**
- * Sends a message packet.
- *
- * @param {String} message
- * @param {Object} options
- * @param {Function} callback
- * @return {Socket} for chaining
- * @api public
- */
-
- Socket.prototype.send =
- Socket.prototype.write = function (data, options, callback) {
- this.sendPacket('message', data, options, callback);
- return this;
- };
-
- /**
- * Sends a packet.
- *
- * @param {String} packet type
- * @param {String} optional, data
- * @param {Object} options
- * @api private
- */
-
- Socket.prototype.sendPacket = function (type, data, options, callback) {
- if ('function' === typeof options) {
- callback = options;
- options = null;
- }
-
- options = options || {};
- options.compress = false !== options.compress;
-
- if ('closing' !== this.readyState && 'closed' !== this.readyState) {
- debug('sending packet "%s" (%s)', type, data);
-
- var packet = {
- type: type,
- options: options
- };
- if (data) packet.data = data;
-
- // exports packetCreate event
- this.emit('packetCreate', packet);
-
- this.writeBuffer.push(packet);
-
- // add send callback to object, if defined
- if (callback) this.packetsFn.push(callback);
-
- this.flush();
- }
- };
-
- /**
- * Attempts to flush the packets buffer.
- *
- * @api private
- */
-
- Socket.prototype.flush = function () {
- if ('closed' !== this.readyState &&
- this.transport.writable &&
- this.writeBuffer.length) {
- debug('flushing buffer to transport');
- this.emit('flush', this.writeBuffer);
- this.server.emit('flush', this, this.writeBuffer);
- var wbuf = this.writeBuffer;
- this.writeBuffer = [];
- if (!this.transport.supportsFraming) {
- this.sentCallbackFn.push(this.packetsFn);
- } else {
- this.sentCallbackFn.push.apply(this.sentCallbackFn, this.packetsFn);
- }
- this.packetsFn = [];
- this.transport.send(wbuf);
- this.emit('drain');
- this.server.emit('drain', this);
- }
- };
-
- /**
- * Get available upgrades for this socket.
- *
- * @api private
- */
-
- Socket.prototype.getAvailableUpgrades = function () {
- var availableUpgrades = [];
- var allUpgrades = this.server.upgrades(this.transport.name);
- for (var i = 0, l = allUpgrades.length; i < l; ++i) {
- var upg = allUpgrades[i];
- if (this.server.transports.indexOf(upg) !== -1) {
- availableUpgrades.push(upg);
- }
- }
- return availableUpgrades;
- };
-
- /**
- * Closes the socket and underlying transport.
- *
- * @param {Boolean} optional, discard
- * @return {Socket} for chaining
- * @api public
- */
-
- Socket.prototype.close = function (discard) {
- if ('open' !== this.readyState) return;
-
- this.readyState = 'closing';
-
- if (this.writeBuffer.length) {
- this.once('drain', this.closeTransport.bind(this, discard));
- return;
- }
-
- this.closeTransport(discard);
- };
-
- /**
- * Closes the underlying transport.
- *
- * @param {Boolean} discard
- * @api private
- */
-
- Socket.prototype.closeTransport = function (discard) {
- if (discard) this.transport.discard();
- this.transport.close(this.onClose.bind(this, 'forced close'));
- };
|