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.

770 lines
19 KiB

4 years ago
  1. /**
  2. * Module dependencies.
  3. */
  4. var uid2 = require('uid2');
  5. var redis = require('redis').createClient;
  6. var msgpack = require('notepack.io');
  7. var Adapter = require('socket.io-adapter');
  8. var debug = require('debug')('socket.io-redis');
  9. /**
  10. * Module exports.
  11. */
  12. module.exports = adapter;
  13. /**
  14. * Request types, for messages between nodes
  15. */
  16. var requestTypes = {
  17. clients: 0,
  18. clientRooms: 1,
  19. allRooms: 2,
  20. remoteJoin: 3,
  21. remoteLeave: 4,
  22. customRequest: 5,
  23. remoteDisconnect: 6
  24. };
  25. /**
  26. * Returns a redis Adapter class.
  27. *
  28. * @param {String} optional, redis uri
  29. * @return {RedisAdapter} adapter
  30. * @api public
  31. */
  32. function adapter(uri, opts) {
  33. opts = opts || {};
  34. // handle options only
  35. if ('object' == typeof uri) {
  36. opts = uri;
  37. uri = null;
  38. }
  39. // opts
  40. var pub = opts.pubClient;
  41. var sub = opts.subClient;
  42. var prefix = opts.key || 'socket.io';
  43. var requestsTimeout = opts.requestsTimeout || 5000;
  44. // init clients if needed
  45. function createClient() {
  46. if (uri) {
  47. // handle uri string
  48. return redis(uri, opts);
  49. } else {
  50. return redis(opts);
  51. }
  52. }
  53. if (!pub) pub = createClient();
  54. if (!sub) sub = createClient();
  55. // this server's key
  56. var uid = uid2(6);
  57. /**
  58. * Adapter constructor.
  59. *
  60. * @param {String} namespace name
  61. * @api public
  62. */
  63. function Redis(nsp){
  64. Adapter.call(this, nsp);
  65. this.uid = uid;
  66. this.prefix = prefix;
  67. this.requestsTimeout = requestsTimeout;
  68. this.channel = prefix + '#' + nsp.name + '#';
  69. this.requestChannel = prefix + '-request#' + this.nsp.name + '#';
  70. this.responseChannel = prefix + '-response#' + this.nsp.name + '#';
  71. this.requests = {};
  72. this.customHook = function(data, cb){ cb(null); }
  73. if (String.prototype.startsWith) {
  74. this.channelMatches = function (messageChannel, subscribedChannel) {
  75. return messageChannel.startsWith(subscribedChannel);
  76. }
  77. } else { // Fallback to other impl for older Node.js
  78. this.channelMatches = function (messageChannel, subscribedChannel) {
  79. return messageChannel.substr(0, subscribedChannel.length) === subscribedChannel;
  80. }
  81. }
  82. this.pubClient = pub;
  83. this.subClient = sub;
  84. var self = this;
  85. sub.psubscribe(this.channel + '*', function(err){
  86. if (err) self.emit('error', err);
  87. });
  88. sub.on('pmessageBuffer', this.onmessage.bind(this));
  89. sub.subscribe([this.requestChannel, this.responseChannel], function(err){
  90. if (err) self.emit('error', err);
  91. });
  92. sub.on('messageBuffer', this.onrequest.bind(this));
  93. function onError(err) {
  94. self.emit('error', err);
  95. }
  96. pub.on('error', onError);
  97. sub.on('error', onError);
  98. }
  99. /**
  100. * Inherits from `Adapter`.
  101. */
  102. Redis.prototype.__proto__ = Adapter.prototype;
  103. /**
  104. * Called with a subscription message
  105. *
  106. * @api private
  107. */
  108. Redis.prototype.onmessage = function(pattern, channel, msg){
  109. channel = channel.toString();
  110. if (!this.channelMatches(channel, this.channel)) {
  111. return debug('ignore different channel');
  112. }
  113. var room = channel.slice(this.channel.length, -1);
  114. if (room !== '' && !this.rooms.hasOwnProperty(room)) {
  115. return debug('ignore unknown room %s', room);
  116. }
  117. var args = msgpack.decode(msg);
  118. var packet;
  119. if (uid === args.shift()) return debug('ignore same uid');
  120. packet = args[0];
  121. if (packet && packet.nsp === undefined) {
  122. packet.nsp = '/';
  123. }
  124. if (!packet || packet.nsp != this.nsp.name) {
  125. return debug('ignore different namespace');
  126. }
  127. args.push(true);
  128. this.broadcast.apply(this, args);
  129. };
  130. /**
  131. * Called on request from another node
  132. *
  133. * @api private
  134. */
  135. Redis.prototype.onrequest = function(channel, msg){
  136. channel = channel.toString();
  137. if (this.channelMatches(channel, this.responseChannel)) {
  138. return this.onresponse(channel, msg);
  139. } else if (!this.channelMatches(channel, this.requestChannel)) {
  140. return debug('ignore different channel');
  141. }
  142. var self = this;
  143. var request;
  144. try {
  145. request = JSON.parse(msg);
  146. } catch(err){
  147. self.emit('error', err);
  148. return;
  149. }
  150. debug('received request %j', request);
  151. switch (request.type) {
  152. case requestTypes.clients:
  153. Adapter.prototype.clients.call(self, request.rooms, function(err, clients){
  154. if(err){
  155. self.emit('error', err);
  156. return;
  157. }
  158. var response = JSON.stringify({
  159. requestid: request.requestid,
  160. clients: clients
  161. });
  162. pub.publish(self.responseChannel, response);
  163. });
  164. break;
  165. case requestTypes.clientRooms:
  166. Adapter.prototype.clientRooms.call(self, request.sid, function(err, rooms){
  167. if(err){
  168. self.emit('error', err);
  169. return;
  170. }
  171. if (!rooms) { return; }
  172. var response = JSON.stringify({
  173. requestid: request.requestid,
  174. rooms: rooms
  175. });
  176. pub.publish(self.responseChannel, response);
  177. });
  178. break;
  179. case requestTypes.allRooms:
  180. var response = JSON.stringify({
  181. requestid: request.requestid,
  182. rooms: Object.keys(this.rooms)
  183. });
  184. pub.publish(self.responseChannel, response);
  185. break;
  186. case requestTypes.remoteJoin:
  187. var socket = this.nsp.connected[request.sid];
  188. if (!socket) { return; }
  189. socket.join(request.room, function(){
  190. var response = JSON.stringify({
  191. requestid: request.requestid
  192. });
  193. pub.publish(self.responseChannel, response);
  194. });
  195. break;
  196. case requestTypes.remoteLeave:
  197. var socket = this.nsp.connected[request.sid];
  198. if (!socket) { return; }
  199. socket.leave(request.room, function(){
  200. var response = JSON.stringify({
  201. requestid: request.requestid
  202. });
  203. pub.publish(self.responseChannel, response);
  204. });
  205. break;
  206. case requestTypes.remoteDisconnect:
  207. var socket = this.nsp.connected[request.sid];
  208. if (!socket) { return; }
  209. socket.disconnect(request.close);
  210. var response = JSON.stringify({
  211. requestid: request.requestid
  212. });
  213. pub.publish(self.responseChannel, response);
  214. break;
  215. case requestTypes.customRequest:
  216. this.customHook(request.data, function(data) {
  217. var response = JSON.stringify({
  218. requestid: request.requestid,
  219. data: data
  220. });
  221. pub.publish(self.responseChannel, response);
  222. });
  223. break;
  224. default:
  225. debug('ignoring unknown request type: %s', request.type);
  226. }
  227. };
  228. /**
  229. * Called on response from another node
  230. *
  231. * @api private
  232. */
  233. Redis.prototype.onresponse = function(channel, msg){
  234. var self = this;
  235. var response;
  236. try {
  237. response = JSON.parse(msg);
  238. } catch(err){
  239. self.emit('error', err);
  240. return;
  241. }
  242. var requestid = response.requestid;
  243. if (!requestid || !self.requests[requestid]) {
  244. debug('ignoring unknown request');
  245. return;
  246. }
  247. debug('received response %j', response);
  248. var request = self.requests[requestid];
  249. switch (request.type) {
  250. case requestTypes.clients:
  251. request.msgCount++;
  252. // ignore if response does not contain 'clients' key
  253. if(!response.clients || !Array.isArray(response.clients)) return;
  254. for(var i = 0; i < response.clients.length; i++){
  255. request.clients[response.clients[i]] = true;
  256. }
  257. if (request.msgCount === request.numsub) {
  258. clearTimeout(request.timeout);
  259. if (request.callback) process.nextTick(request.callback.bind(null, null, Object.keys(request.clients)));
  260. delete self.requests[requestid];
  261. }
  262. break;
  263. case requestTypes.clientRooms:
  264. clearTimeout(request.timeout);
  265. if (request.callback) process.nextTick(request.callback.bind(null, null, response.rooms));
  266. delete self.requests[requestid];
  267. break;
  268. case requestTypes.allRooms:
  269. request.msgCount++;
  270. // ignore if response does not contain 'rooms' key
  271. if(!response.rooms || !Array.isArray(response.rooms)) return;
  272. for(var i = 0; i < response.rooms.length; i++){
  273. request.rooms[response.rooms[i]] = true;
  274. }
  275. if (request.msgCount === request.numsub) {
  276. clearTimeout(request.timeout);
  277. if (request.callback) process.nextTick(request.callback.bind(null, null, Object.keys(request.rooms)));
  278. delete self.requests[requestid];
  279. }
  280. break;
  281. case requestTypes.remoteJoin:
  282. case requestTypes.remoteLeave:
  283. case requestTypes.remoteDisconnect:
  284. clearTimeout(request.timeout);
  285. if (request.callback) process.nextTick(request.callback.bind(null, null));
  286. delete self.requests[requestid];
  287. break;
  288. case requestTypes.customRequest:
  289. request.msgCount++;
  290. request.replies.push(response.data);
  291. if (request.msgCount === request.numsub) {
  292. clearTimeout(request.timeout);
  293. if (request.callback) process.nextTick(request.callback.bind(null, null, request.replies));
  294. delete self.requests[requestid];
  295. }
  296. break;
  297. default:
  298. debug('ignoring unknown request type: %s', request.type);
  299. }
  300. };
  301. /**
  302. * Broadcasts a packet.
  303. *
  304. * @param {Object} packet to emit
  305. * @param {Object} options
  306. * @param {Boolean} whether the packet came from another node
  307. * @api public
  308. */
  309. Redis.prototype.broadcast = function(packet, opts, remote){
  310. packet.nsp = this.nsp.name;
  311. if (!(remote || (opts && opts.flags && opts.flags.local))) {
  312. var msg = msgpack.encode([uid, packet, opts]);
  313. var channel = this.channel;
  314. if (opts.rooms && opts.rooms.length === 1) {
  315. channel += opts.rooms[0] + '#';
  316. }
  317. debug('publishing message to channel %s', channel);
  318. pub.publish(channel, msg);
  319. }
  320. Adapter.prototype.broadcast.call(this, packet, opts);
  321. };
  322. /**
  323. * Get the number of subscribers of a channel
  324. *
  325. * @param {String} channel
  326. */
  327. function getNumSub(channel){
  328. if(pub.constructor.name != 'Cluster'){
  329. // RedisClient or Redis
  330. return new Promise(function(resolve,reject) {
  331. pub.send_command('pubsub', ['numsub', channel], function(err, numsub){
  332. if (err) return reject(err);
  333. resolve(parseInt(numsub[1], 10));
  334. });
  335. })
  336. }else{
  337. // Cluster
  338. var nodes = pub.nodes();
  339. return Promise.all(
  340. nodes.map(function(node) {
  341. return node.send_command('pubsub', ['numsub', channel]);
  342. })
  343. ).then(function(values) {
  344. var numsub = 0;
  345. values.map(function(value){
  346. numsub += parseInt(value[1], 10);
  347. })
  348. return numsub;
  349. }).catch(function(err){
  350. throw err;
  351. });
  352. }
  353. }
  354. /**
  355. * Gets a list of clients by sid.
  356. *
  357. * @param {Array} explicit set of rooms to check.
  358. * @param {Function} callback
  359. * @api public
  360. */
  361. Redis.prototype.clients = function(rooms, fn){
  362. if ('function' == typeof rooms){
  363. fn = rooms;
  364. rooms = null;
  365. }
  366. rooms = rooms || [];
  367. var self = this;
  368. var requestid = uid2(6);
  369. getNumSub(self.requestChannel).then(numsub => {
  370. debug('waiting for %d responses to "clients" request', numsub);
  371. var request = JSON.stringify({
  372. requestid : requestid,
  373. type: requestTypes.clients,
  374. rooms : rooms
  375. });
  376. // if there is no response for x second, return result
  377. var timeout = setTimeout(function() {
  378. var request = self.requests[requestid];
  379. if (fn) process.nextTick(fn.bind(null, new Error('timeout reached while waiting for clients response'), Object.keys(request.clients)));
  380. delete self.requests[requestid];
  381. }, self.requestsTimeout);
  382. self.requests[requestid] = {
  383. type: requestTypes.clients,
  384. numsub: numsub,
  385. msgCount: 0,
  386. clients: {},
  387. callback: fn,
  388. timeout: timeout
  389. };
  390. pub.publish(self.requestChannel, request);
  391. }).catch(err => {
  392. self.emit('error', err);
  393. if (fn) fn(err);
  394. });
  395. };
  396. /**
  397. * Gets the list of rooms a given client has joined.
  398. *
  399. * @param {String} client id
  400. * @param {Function} callback
  401. * @api public
  402. */
  403. Redis.prototype.clientRooms = function(id, fn){
  404. var self = this;
  405. var requestid = uid2(6);
  406. var rooms = this.sids[id];
  407. if (rooms) {
  408. if (fn) process.nextTick(fn.bind(null, null, Object.keys(rooms)));
  409. return;
  410. }
  411. var request = JSON.stringify({
  412. requestid : requestid,
  413. type: requestTypes.clientRooms,
  414. sid : id
  415. });
  416. // if there is no response for x second, return result
  417. var timeout = setTimeout(function() {
  418. if (fn) process.nextTick(fn.bind(null, new Error('timeout reached while waiting for rooms response')));
  419. delete self.requests[requestid];
  420. }, self.requestsTimeout);
  421. self.requests[requestid] = {
  422. type: requestTypes.clientRooms,
  423. callback: fn,
  424. timeout: timeout
  425. };
  426. pub.publish(self.requestChannel, request);
  427. };
  428. /**
  429. * Gets the list of all rooms (accross every node)
  430. *
  431. * @param {Function} callback
  432. * @api public
  433. */
  434. Redis.prototype.allRooms = function(fn){
  435. var self = this;
  436. var requestid = uid2(6);
  437. getNumSub(self.requestChannel).then(numsub => {
  438. debug('waiting for %d responses to "allRooms" request', numsub);
  439. var request = JSON.stringify({
  440. requestid : requestid,
  441. type: requestTypes.allRooms
  442. });
  443. // if there is no response for x second, return result
  444. var timeout = setTimeout(function() {
  445. var request = self.requests[requestid];
  446. if (fn) process.nextTick(fn.bind(null, new Error('timeout reached while waiting for allRooms response'), Object.keys(request.rooms)));
  447. delete self.requests[requestid];
  448. }, self.requestsTimeout);
  449. self.requests[requestid] = {
  450. type: requestTypes.allRooms,
  451. numsub: numsub,
  452. msgCount: 0,
  453. rooms: {},
  454. callback: fn,
  455. timeout: timeout
  456. };
  457. pub.publish(self.requestChannel, request);
  458. }).catch(err => {
  459. self.emit('error', err);
  460. if (fn) fn(err);
  461. });
  462. };
  463. /**
  464. * Makes the socket with the given id join the room
  465. *
  466. * @param {String} socket id
  467. * @param {String} room name
  468. * @param {Function} callback
  469. * @api public
  470. */
  471. Redis.prototype.remoteJoin = function(id, room, fn){
  472. var self = this;
  473. var requestid = uid2(6);
  474. var socket = this.nsp.connected[id];
  475. if (socket) {
  476. socket.join(room, fn);
  477. return;
  478. }
  479. var request = JSON.stringify({
  480. requestid : requestid,
  481. type: requestTypes.remoteJoin,
  482. sid: id,
  483. room: room
  484. });
  485. // if there is no response for x second, return result
  486. var timeout = setTimeout(function() {
  487. if (fn) process.nextTick(fn.bind(null, new Error('timeout reached while waiting for remoteJoin response')));
  488. delete self.requests[requestid];
  489. }, self.requestsTimeout);
  490. self.requests[requestid] = {
  491. type: requestTypes.remoteJoin,
  492. callback: fn,
  493. timeout: timeout
  494. };
  495. pub.publish(self.requestChannel, request);
  496. };
  497. /**
  498. * Makes the socket with the given id leave the room
  499. *
  500. * @param {String} socket id
  501. * @param {String} room name
  502. * @param {Function} callback
  503. * @api public
  504. */
  505. Redis.prototype.remoteLeave = function(id, room, fn){
  506. var self = this;
  507. var requestid = uid2(6);
  508. var socket = this.nsp.connected[id];
  509. if (socket) {
  510. socket.leave(room, fn);
  511. return;
  512. }
  513. var request = JSON.stringify({
  514. requestid : requestid,
  515. type: requestTypes.remoteLeave,
  516. sid: id,
  517. room: room
  518. });
  519. // if there is no response for x second, return result
  520. var timeout = setTimeout(function() {
  521. if (fn) process.nextTick(fn.bind(null, new Error('timeout reached while waiting for remoteLeave response')));
  522. delete self.requests[requestid];
  523. }, self.requestsTimeout);
  524. self.requests[requestid] = {
  525. type: requestTypes.remoteLeave,
  526. callback: fn,
  527. timeout: timeout
  528. };
  529. pub.publish(self.requestChannel, request);
  530. };
  531. /**
  532. * Makes the socket with the given id to be disconnected forcefully
  533. * @param {String} socket id
  534. * @param {Boolean} close if `true`, closes the underlying connection
  535. * @param {Function} callback
  536. */
  537. Redis.prototype.remoteDisconnect = function(id, close, fn) {
  538. var self = this;
  539. var requestid = uid2(6);
  540. var socket = this.nsp.connected[id];
  541. if(socket) {
  542. socket.disconnect(close);
  543. if (fn) process.nextTick(fn.bind(null, null));
  544. return;
  545. }
  546. var request = JSON.stringify({
  547. requestid : requestid,
  548. type: requestTypes.remoteDisconnect,
  549. sid: id,
  550. close: close
  551. });
  552. // if there is no response for x second, return result
  553. var timeout = setTimeout(function() {
  554. if (fn) process.nextTick(fn.bind(null, new Error('timeout reached while waiting for remoteDisconnect response')));
  555. delete self.requests[requestid];
  556. }, self.requestsTimeout);
  557. self.requests[requestid] = {
  558. type: requestTypes.remoteDisconnect,
  559. callback: fn,
  560. timeout: timeout
  561. };
  562. pub.publish(self.requestChannel, request);
  563. };
  564. /**
  565. * Sends a new custom request to other nodes
  566. *
  567. * @param {Object} data (no binary)
  568. * @param {Function} callback
  569. * @api public
  570. */
  571. Redis.prototype.customRequest = function(data, fn){
  572. if (typeof data === 'function'){
  573. fn = data;
  574. data = null;
  575. }
  576. var self = this;
  577. var requestid = uid2(6);
  578. getNumSub(self.requestChannel).then(numsub => {
  579. debug('waiting for %d responses to "customRequest" request', numsub);
  580. var request = JSON.stringify({
  581. requestid : requestid,
  582. type: requestTypes.customRequest,
  583. data: data
  584. });
  585. // if there is no response for x second, return result
  586. var timeout = setTimeout(function() {
  587. var request = self.requests[requestid];
  588. if (fn) process.nextTick(fn.bind(null, new Error('timeout reached while waiting for customRequest response'), request.replies));
  589. delete self.requests[requestid];
  590. }, self.requestsTimeout);
  591. self.requests[requestid] = {
  592. type: requestTypes.customRequest,
  593. numsub: numsub,
  594. msgCount: 0,
  595. replies: [],
  596. callback: fn,
  597. timeout: timeout
  598. };
  599. pub.publish(self.requestChannel, request);
  600. }).catch(err => {
  601. self.emit('error', err);
  602. if (fn) fn(err);
  603. });
  604. };
  605. Redis.uid = uid;
  606. Redis.pubClient = pub;
  607. Redis.subClient = sub;
  608. Redis.prefix = prefix;
  609. Redis.requestsTimeout = requestsTimeout;
  610. return Redis;
  611. }