You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

479 lines
11 KiB

7 years ago
  1. /**
  2. * Module dependencies.
  3. */
  4. var EventEmitter = require('events').EventEmitter;
  5. var util = require('util');
  6. var debug = require('debug')('engine:socket');
  7. /**
  8. * Module exports.
  9. */
  10. module.exports = Socket;
  11. /**
  12. * Client class (abstract).
  13. *
  14. * @api private
  15. */
  16. function Socket (id, server, transport, req) {
  17. this.id = id;
  18. this.server = server;
  19. this.upgrading = false;
  20. this.upgraded = false;
  21. this.readyState = 'opening';
  22. this.writeBuffer = [];
  23. this.packetsFn = [];
  24. this.sentCallbackFn = [];
  25. this.cleanupFn = [];
  26. this.request = req;
  27. // Cache IP since it might not be in the req later
  28. this.remoteAddress = req.connection.remoteAddress;
  29. this.checkIntervalTimer = null;
  30. this.upgradeTimeoutTimer = null;
  31. this.pingTimeoutTimer = null;
  32. this.setTransport(transport);
  33. this.onOpen();
  34. }
  35. /**
  36. * Inherits from EventEmitter.
  37. */
  38. util.inherits(Socket, EventEmitter);
  39. /**
  40. * Called upon transport considered open.
  41. *
  42. * @api private
  43. */
  44. Socket.prototype.onOpen = function () {
  45. this.readyState = 'open';
  46. // sends an `open` packet
  47. this.transport.sid = this.id;
  48. this.sendPacket('open', JSON.stringify({
  49. sid: this.id,
  50. upgrades: this.getAvailableUpgrades(),
  51. pingInterval: this.server.pingInterval,
  52. pingTimeout: this.server.pingTimeout
  53. }));
  54. this.emit('open');
  55. this.setPingTimeout();
  56. };
  57. /**
  58. * Called upon transport packet.
  59. *
  60. * @param {Object} packet
  61. * @api private
  62. */
  63. Socket.prototype.onPacket = function (packet) {
  64. if ('open' === this.readyState) {
  65. // export packet event
  66. debug('packet');
  67. this.emit('packet', packet);
  68. // Reset ping timeout on any packet, incoming data is a good sign of
  69. // other side's liveness
  70. this.setPingTimeout();
  71. switch (packet.type) {
  72. case 'ping':
  73. debug('got ping');
  74. this.sendPacket('pong');
  75. this.emit('heartbeat');
  76. break;
  77. case 'error':
  78. this.onClose('parse error');
  79. break;
  80. case 'message':
  81. this.emit('data', packet.data);
  82. this.emit('message', packet.data);
  83. break;
  84. }
  85. } else {
  86. debug('packet received with closed socket');
  87. }
  88. };
  89. /**
  90. * Called upon transport error.
  91. *
  92. * @param {Error} error object
  93. * @api private
  94. */
  95. Socket.prototype.onError = function (err) {
  96. debug('transport error');
  97. this.onClose('transport error', err);
  98. };
  99. /**
  100. * Sets and resets ping timeout timer based on client pings.
  101. *
  102. * @api private
  103. */
  104. Socket.prototype.setPingTimeout = function () {
  105. var self = this;
  106. clearTimeout(self.pingTimeoutTimer);
  107. self.pingTimeoutTimer = setTimeout(function () {
  108. self.onClose('ping timeout');
  109. }, self.server.pingInterval + self.server.pingTimeout);
  110. };
  111. /**
  112. * Attaches handlers for the given transport.
  113. *
  114. * @param {Transport} transport
  115. * @api private
  116. */
  117. Socket.prototype.setTransport = function (transport) {
  118. var onError = this.onError.bind(this);
  119. var onPacket = this.onPacket.bind(this);
  120. var flush = this.flush.bind(this);
  121. var onClose = this.onClose.bind(this, 'transport close');
  122. this.transport = transport;
  123. this.transport.once('error', onError);
  124. this.transport.on('packet', onPacket);
  125. this.transport.on('drain', flush);
  126. this.transport.once('close', onClose);
  127. // this function will manage packet events (also message callbacks)
  128. this.setupSendCallback();
  129. this.cleanupFn.push(function () {
  130. transport.removeListener('error', onError);
  131. transport.removeListener('packet', onPacket);
  132. transport.removeListener('drain', flush);
  133. transport.removeListener('close', onClose);
  134. });
  135. };
  136. /**
  137. * Upgrades socket to the given transport
  138. *
  139. * @param {Transport} transport
  140. * @api private
  141. */
  142. Socket.prototype.maybeUpgrade = function (transport) {
  143. debug('might upgrade socket transport from "%s" to "%s"'
  144. , this.transport.name, transport.name);
  145. this.upgrading = true;
  146. var self = this;
  147. // set transport upgrade timer
  148. self.upgradeTimeoutTimer = setTimeout(function () {
  149. debug('client did not complete upgrade - closing transport');
  150. cleanup();
  151. if ('open' === transport.readyState) {
  152. transport.close();
  153. }
  154. }, this.server.upgradeTimeout);
  155. function onPacket (packet) {
  156. if ('ping' === packet.type && 'probe' === packet.data) {
  157. transport.send([{ type: 'pong', data: 'probe' }]);
  158. self.emit('upgrading', transport);
  159. clearInterval(self.checkIntervalTimer);
  160. self.checkIntervalTimer = setInterval(check, 100);
  161. } else if ('upgrade' === packet.type && self.readyState !== 'closed') {
  162. debug('got upgrade packet - upgrading');
  163. cleanup();
  164. self.transport.discard();
  165. self.upgraded = true;
  166. self.clearTransport();
  167. self.setTransport(transport);
  168. self.emit('upgrade', transport);
  169. self.setPingTimeout();
  170. self.flush();
  171. if (self.readyState === 'closing') {
  172. transport.close(function () {
  173. self.onClose('forced close');
  174. });
  175. }
  176. } else {
  177. cleanup();
  178. transport.close();
  179. }
  180. }
  181. // we force a polling cycle to ensure a fast upgrade
  182. function check () {
  183. if ('polling' === self.transport.name && self.transport.writable) {
  184. debug('writing a noop packet to polling for fast upgrade');
  185. self.transport.send([{ type: 'noop' }]);
  186. }
  187. }
  188. function cleanup () {
  189. self.upgrading = false;
  190. clearInterval(self.checkIntervalTimer);
  191. self.checkIntervalTimer = null;
  192. clearTimeout(self.upgradeTimeoutTimer);
  193. self.upgradeTimeoutTimer = null;
  194. transport.removeListener('packet', onPacket);
  195. transport.removeListener('close', onTransportClose);
  196. transport.removeListener('error', onError);
  197. self.removeListener('close', onClose);
  198. }
  199. function onError (err) {
  200. debug('client did not complete upgrade - %s', err);
  201. cleanup();
  202. transport.close();
  203. transport = null;
  204. }
  205. function onTransportClose () {
  206. onError('transport closed');
  207. }
  208. function onClose () {
  209. onError('socket closed');
  210. }
  211. transport.on('packet', onPacket);
  212. transport.once('close', onTransportClose);
  213. transport.once('error', onError);
  214. self.once('close', onClose);
  215. };
  216. /**
  217. * Clears listeners and timers associated with current transport.
  218. *
  219. * @api private
  220. */
  221. Socket.prototype.clearTransport = function () {
  222. var cleanup;
  223. var toCleanUp = this.cleanupFn.length;
  224. for (var i = 0; i < toCleanUp; i++) {
  225. cleanup = this.cleanupFn.shift();
  226. cleanup();
  227. }
  228. // silence further transport errors and prevent uncaught exceptions
  229. this.transport.on('error', function () {
  230. debug('error triggered by discarded transport');
  231. });
  232. // ensure transport won't stay open
  233. this.transport.close();
  234. clearTimeout(this.pingTimeoutTimer);
  235. };
  236. /**
  237. * Called upon transport considered closed.
  238. * Possible reasons: `ping timeout`, `client error`, `parse error`,
  239. * `transport error`, `server close`, `transport close`
  240. */
  241. Socket.prototype.onClose = function (reason, description) {
  242. if ('closed' !== this.readyState) {
  243. this.readyState = 'closed';
  244. clearTimeout(this.pingTimeoutTimer);
  245. clearInterval(this.checkIntervalTimer);
  246. this.checkIntervalTimer = null;
  247. clearTimeout(this.upgradeTimeoutTimer);
  248. var self = this;
  249. // clean writeBuffer in next tick, so developers can still
  250. // grab the writeBuffer on 'close' event
  251. process.nextTick(function () {
  252. self.writeBuffer = [];
  253. });
  254. this.packetsFn = [];
  255. this.sentCallbackFn = [];
  256. this.clearTransport();
  257. this.emit('close', reason, description);
  258. }
  259. };
  260. /**
  261. * Setup and manage send callback
  262. *
  263. * @api private
  264. */
  265. Socket.prototype.setupSendCallback = function () {
  266. var self = this;
  267. this.transport.on('drain', onDrain);
  268. this.cleanupFn.push(function () {
  269. self.transport.removeListener('drain', onDrain);
  270. });
  271. // the message was sent successfully, execute the callback
  272. function onDrain () {
  273. if (self.sentCallbackFn.length > 0) {
  274. var seqFn = self.sentCallbackFn.splice(0, 1)[0];
  275. if ('function' === typeof seqFn) {
  276. debug('executing send callback');
  277. seqFn(self.transport);
  278. } else if (Array.isArray(seqFn)) {
  279. debug('executing batch send callback');
  280. for (var l = seqFn.length, i = 0; i < l; i++) {
  281. if ('function' === typeof seqFn[i]) {
  282. seqFn[i](self.transport);
  283. }
  284. }
  285. }
  286. }
  287. }
  288. };
  289. /**
  290. * Sends a message packet.
  291. *
  292. * @param {String} message
  293. * @param {Object} options
  294. * @param {Function} callback
  295. * @return {Socket} for chaining
  296. * @api public
  297. */
  298. Socket.prototype.send =
  299. Socket.prototype.write = function (data, options, callback) {
  300. this.sendPacket('message', data, options, callback);
  301. return this;
  302. };
  303. /**
  304. * Sends a packet.
  305. *
  306. * @param {String} packet type
  307. * @param {String} optional, data
  308. * @param {Object} options
  309. * @api private
  310. */
  311. Socket.prototype.sendPacket = function (type, data, options, callback) {
  312. if ('function' === typeof options) {
  313. callback = options;
  314. options = null;
  315. }
  316. options = options || {};
  317. options.compress = false !== options.compress;
  318. if ('closing' !== this.readyState) {
  319. debug('sending packet "%s" (%s)', type, data);
  320. var packet = {
  321. type: type,
  322. options: options
  323. };
  324. if (data) packet.data = data;
  325. // exports packetCreate event
  326. this.emit('packetCreate', packet);
  327. this.writeBuffer.push(packet);
  328. // add send callback to object, if defined
  329. if (callback) this.packetsFn.push(callback);
  330. this.flush();
  331. }
  332. };
  333. /**
  334. * Attempts to flush the packets buffer.
  335. *
  336. * @api private
  337. */
  338. Socket.prototype.flush = function () {
  339. if ('closed' !== this.readyState &&
  340. this.transport.writable &&
  341. this.writeBuffer.length) {
  342. debug('flushing buffer to transport');
  343. this.emit('flush', this.writeBuffer);
  344. this.server.emit('flush', this, this.writeBuffer);
  345. var wbuf = this.writeBuffer;
  346. this.writeBuffer = [];
  347. if (!this.transport.supportsFraming) {
  348. this.sentCallbackFn.push(this.packetsFn);
  349. } else {
  350. this.sentCallbackFn.push.apply(this.sentCallbackFn, this.packetsFn);
  351. }
  352. this.packetsFn = [];
  353. this.transport.send(wbuf);
  354. this.emit('drain');
  355. this.server.emit('drain', this);
  356. }
  357. };
  358. /**
  359. * Get available upgrades for this socket.
  360. *
  361. * @api private
  362. */
  363. Socket.prototype.getAvailableUpgrades = function () {
  364. var availableUpgrades = [];
  365. var allUpgrades = this.server.upgrades(this.transport.name);
  366. for (var i = 0, l = allUpgrades.length; i < l; ++i) {
  367. var upg = allUpgrades[i];
  368. if (this.server.transports.indexOf(upg) !== -1) {
  369. availableUpgrades.push(upg);
  370. }
  371. }
  372. return availableUpgrades;
  373. };
  374. /**
  375. * Closes the socket and underlying transport.
  376. *
  377. * @param {Boolean} optional, discard
  378. * @return {Socket} for chaining
  379. * @api public
  380. */
  381. Socket.prototype.close = function (discard) {
  382. if ('open' !== this.readyState) return;
  383. this.readyState = 'closing';
  384. if (this.writeBuffer.length) {
  385. this.once('drain', this.closeTransport.bind(this, discard));
  386. return;
  387. }
  388. this.closeTransport(discard);
  389. };
  390. /**
  391. * Closes the underlying transport.
  392. *
  393. * @param {Boolean} discard
  394. * @api private
  395. */
  396. Socket.prototype.closeTransport = function (discard) {
  397. if (discard) this.transport.discard();
  398. this.transport.close(this.onClose.bind(this, 'forced close'));
  399. };