sessionService.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. var EventEmitter = require('events').EventEmitter;
  2. var util = require('util');
  3. var logger = require('pomelo-logger').getLogger('pomelo', __filename);
  4. var utils = require('../../util/utils');
  5. var FRONTEND_SESSION_FIELDS = ['id', 'frontendId', 'uid', '__sessionService__'];
  6. var EXPORTED_SESSION_FIELDS = ['id', 'frontendId', 'uid', 'settings'];
  7. var ST_INITED = 0;
  8. var ST_CLOSED = 1;
  9. /**
  10. * Session service maintains the internal session for each client connection.
  11. *
  12. * Session service is created by session component and is only
  13. * <b>available</b> in frontend servers. You can access the service by
  14. * `app.get('sessionService')` or `app.sessionService` in frontend servers.
  15. *
  16. * @param {Object} opts constructor parameters
  17. * @class
  18. * @constructor
  19. */
  20. var SessionService = function(opts) {
  21. opts = opts || {};
  22. this.singleSession = opts.singleSession;
  23. this.sessions = {}; // sid -> session
  24. this.uidMap = {}; // uid -> sessions
  25. };
  26. module.exports = SessionService;
  27. /**
  28. * Create and return internal session.
  29. *
  30. * @param {Integer} sid uniqe id for the internal session
  31. * @param {String} frontendId frontend server in which the internal session is created
  32. * @param {Object} socket the underlying socket would be held by the internal session
  33. *
  34. * @return {Session}
  35. *
  36. * @memberOf SessionService
  37. * @api private
  38. */
  39. SessionService.prototype.create = function(sid, frontendId, socket) {
  40. var session = new Session(sid, frontendId, socket, this);
  41. this.sessions[session.id] = session;
  42. return session;
  43. };
  44. /**
  45. * Bind the session with a user id.
  46. *
  47. * @memberOf SessionService
  48. * @api private
  49. */
  50. SessionService.prototype.bind = function(sid, uid, cb) {
  51. var session = this.sessions[sid];
  52. if(!session) {
  53. process.nextTick(function() {
  54. cb(new Error('session does not exist, sid: ' + sid));
  55. });
  56. return;
  57. }
  58. if(session.uid) {
  59. if(session.uid === uid) {
  60. // already bound with the same uid
  61. cb();
  62. return;
  63. }
  64. // already bound with other uid
  65. process.nextTick(function() {
  66. cb(new Error('session has already bound with ' + session.uid));
  67. });
  68. return;
  69. }
  70. var sessions = this.uidMap[uid];
  71. if(!!this.singleSession && !!sessions) {
  72. process.nextTick(function() {
  73. cb(new Error('singleSession is enabled, and session has already bound with uid: ' + uid));
  74. });
  75. return;
  76. }
  77. if(!sessions) {
  78. sessions = this.uidMap[uid] = [];
  79. }
  80. for(var i=0, l=sessions.length; i<l; i++) {
  81. // session has binded with the uid
  82. if(sessions[i].id === session.id) {
  83. process.nextTick(cb);
  84. return;
  85. }
  86. }
  87. sessions.push(session);
  88. session.bind(uid);
  89. if(cb) {
  90. process.nextTick(cb);
  91. }
  92. };
  93. /**
  94. * Unbind a session with the user id.
  95. *
  96. * @memberOf SessionService
  97. * @api private
  98. */
  99. SessionService.prototype.unbind = function(sid, uid, cb) {
  100. var session = this.sessions[sid];
  101. if(!session) {
  102. process.nextTick(function() {
  103. cb(new Error('session does not exist, sid: ' + sid));
  104. });
  105. return;
  106. }
  107. if(!session.uid || session.uid !== uid) {
  108. process.nextTick(function() {
  109. cb(new Error('session has not bind with ' + session.uid));
  110. });
  111. return;
  112. }
  113. var sessions = this.uidMap[uid], sess;
  114. if(sessions) {
  115. for(var i=0, l=sessions.length; i<l; i++) {
  116. sess = sessions[i];
  117. if(sess.id === sid) {
  118. sessions.splice(i, 1);
  119. break;
  120. }
  121. }
  122. if(sessions.length === 0) {
  123. delete this.uidMap[uid];
  124. }
  125. }
  126. session.unbind(uid);
  127. if(cb) {
  128. process.nextTick(cb);
  129. }
  130. };
  131. /**
  132. * Get session by id.
  133. *
  134. * @param {Number} id The session id
  135. * @return {Session}
  136. *
  137. * @memberOf SessionService
  138. * @api private
  139. */
  140. SessionService.prototype.get = function(sid) {
  141. return this.sessions[sid];
  142. };
  143. /**
  144. * Get sessions by userId.
  145. *
  146. * @param {Number} uid User id associated with the session
  147. * @return {Array} list of session binded with the uid
  148. *
  149. * @memberOf SessionService
  150. * @api private
  151. */
  152. SessionService.prototype.getByUid = function(uid) {
  153. return this.uidMap[uid];
  154. };
  155. /**
  156. * Remove session by key.
  157. *
  158. * @param {Number} sid The session id
  159. *
  160. * @memberOf SessionService
  161. * @api private
  162. */
  163. SessionService.prototype.remove = function(sid) {
  164. var session = this.sessions[sid];
  165. if(session) {
  166. var uid = session.uid;
  167. delete this.sessions[session.id];
  168. var sessions = this.uidMap[uid];
  169. if(!sessions) {
  170. return;
  171. }
  172. for(var i=0, l=sessions.length; i<l; i++) {
  173. if(sessions[i].id === sid) {
  174. sessions.splice(i, 1);
  175. if(sessions.length === 0) {
  176. delete this.uidMap[uid];
  177. }
  178. break;
  179. }
  180. }
  181. }
  182. };
  183. /**
  184. * Import the key/value into session.
  185. *
  186. * @api private
  187. */
  188. SessionService.prototype.import = function(sid, key, value, cb) {
  189. var session = this.sessions[sid];
  190. if(!session) {
  191. utils.invokeCallback(cb, new Error('session does not exist, sid: ' + sid));
  192. return;
  193. }
  194. session.set(key, value);
  195. utils.invokeCallback(cb);
  196. };
  197. /**
  198. * Import new value for the existed session.
  199. *
  200. * @memberOf SessionService
  201. * @api private
  202. */
  203. SessionService.prototype.importAll = function(sid, settings, cb) {
  204. var session = this.sessions[sid];
  205. if(!session) {
  206. utils.invokeCallback(cb, new Error('session does not exist, sid: ' + sid));
  207. return;
  208. }
  209. for(var f in settings) {
  210. session.set(f, settings[f]);
  211. }
  212. utils.invokeCallback(cb);
  213. };
  214. /**
  215. * Kick all the session offline under the user id.
  216. *
  217. * @param {Number} uid user id asscociated with the session
  218. * @param {Function} cb callback function
  219. *
  220. * @memberOf SessionService
  221. */
  222. SessionService.prototype.kick = function(uid, reason, cb) {
  223. // compatible for old kick(uid, cb);
  224. if(typeof reason === 'function') {
  225. cb = reason;
  226. reason = 'kick';
  227. }
  228. var sessions = this.getByUid(uid);
  229. if(sessions) {
  230. // notify client
  231. var sids = [];
  232. var self = this;
  233. sessions.forEach(function(session) {
  234. sids.push(session.id);
  235. });
  236. sids.forEach(function(sid) {
  237. self.sessions[sid].closed(reason);
  238. });
  239. process.nextTick(function() {
  240. utils.invokeCallback(cb);
  241. });
  242. } else {
  243. process.nextTick(function() {
  244. utils.invokeCallback(cb);
  245. });
  246. }
  247. };
  248. /**
  249. * Kick a user offline by session id.
  250. *
  251. * @param {Number} sid session id
  252. * @param {Function} cb callback function
  253. *
  254. * @memberOf SessionService
  255. */
  256. SessionService.prototype.kickBySessionId = function(sid, reason, cb) {
  257. if(typeof reason === 'function') {
  258. cb = reason;
  259. reason = 'kick';
  260. }
  261. var session = this.get(sid);
  262. if(session) {
  263. // notify client
  264. session.closed(reason);
  265. process.nextTick(function() {
  266. utils.invokeCallback(cb);
  267. });
  268. } else {
  269. process.nextTick(function() {
  270. utils.invokeCallback(cb);
  271. });
  272. }
  273. };
  274. /**
  275. * Get client remote address by session id.
  276. *
  277. * @param {Number} sid session id
  278. * @return {Object} remote address of client
  279. *
  280. * @memberOf SessionService
  281. */
  282. SessionService.prototype.getClientAddressBySessionId = function(sid) {
  283. var session = this.get(sid);
  284. if(session) {
  285. var socket = session.__socket__;
  286. return socket.remoteAddress;
  287. } else {
  288. return null;
  289. }
  290. };
  291. /**
  292. * Send message to the client by session id.
  293. *
  294. * @param {String} sid session id
  295. * @param {Object} msg message to send
  296. *
  297. * @memberOf SessionService
  298. * @api private
  299. */
  300. SessionService.prototype.sendMessage = function(sid, msg) {
  301. var session = this.sessions[sid];
  302. if(!session) {
  303. logger.debug('Fail to send message for non-existing session, sid: ' + sid + ' msg: ' + msg);
  304. return false;
  305. }
  306. return send(this, session, msg);
  307. };
  308. /**
  309. * Send message to the client by user id.
  310. *
  311. * @param {String} uid userId
  312. * @param {Object} msg message to send
  313. *
  314. * @memberOf SessionService
  315. * @api private
  316. */
  317. SessionService.prototype.sendMessageByUid = function(uid, msg) {
  318. var sessions = this.uidMap[uid];
  319. if(!sessions) {
  320. logger.debug('fail to send message by uid for non-existing session. uid: %j',
  321. uid);
  322. return false;
  323. }
  324. for(var i=0, l=sessions.length; i<l; i++) {
  325. send(this, sessions[i], msg);
  326. }
  327. };
  328. /**
  329. * Iterate all the session in the session service.
  330. *
  331. * @param {Function} cb callback function to fetch session
  332. * @api private
  333. */
  334. SessionService.prototype.forEachSession = function(cb) {
  335. for(var sid in this.sessions) {
  336. cb(this.sessions[sid]);
  337. }
  338. };
  339. /**
  340. * Iterate all the binded session in the session service.
  341. *
  342. * @param {Function} cb callback function to fetch session
  343. * @api private
  344. */
  345. SessionService.prototype.forEachBindedSession = function(cb) {
  346. var i, l, sessions;
  347. for(var uid in this.uidMap) {
  348. sessions = this.uidMap[uid];
  349. for(i=0, l=sessions.length; i<l; i++) {
  350. cb(sessions[i]);
  351. }
  352. }
  353. };
  354. /**
  355. * Get sessions' quantity in specified server.
  356. *
  357. */
  358. SessionService.prototype.getSessionsCount = function() {
  359. return utils.size(this.sessions);
  360. };
  361. /**
  362. * Send message to the client that associated with the session.
  363. *
  364. * @api private
  365. */
  366. var send = function(service, session, msg) {
  367. session.send(msg);
  368. return true;
  369. };
  370. /**
  371. * Session maintains the relationship between client connection and user information.
  372. * There is a session associated with each client connection. And it should bind to a
  373. * user id after the client passes the identification.
  374. *
  375. * Session is created in frontend server and should not be accessed in handler.
  376. * There is a proxy class called BackendSession in backend servers and FrontendSession
  377. * in frontend servers.
  378. */
  379. var Session = function(sid, frontendId, socket, service) {
  380. EventEmitter.call(this);
  381. this.id = sid; // r
  382. this.frontendId = frontendId; // r
  383. this.uid = null; // r
  384. this.settings = {};
  385. // private
  386. this.__socket__ = socket;
  387. this.__sessionService__ = service;
  388. this.__state__ = ST_INITED;
  389. };
  390. util.inherits(Session, EventEmitter);
  391. /*
  392. * Export current session as frontend session.
  393. */
  394. Session.prototype.toFrontendSession = function() {
  395. return new FrontendSession(this);
  396. };
  397. /**
  398. * Bind the session with the the uid.
  399. *
  400. * @param {Number} uid User id
  401. * @api public
  402. */
  403. Session.prototype.bind = function(uid) {
  404. this.uid = uid;
  405. this.emit('bind', uid);
  406. };
  407. /**
  408. * Unbind the session with the the uid.
  409. *
  410. * @param {Number} uid User id
  411. * @api private
  412. */
  413. Session.prototype.unbind = function(uid) {
  414. this.uid = null;
  415. this.emit('unbind', uid);
  416. };
  417. /**
  418. * Set values (one or many) for the session.
  419. *
  420. * @param {String|Object} key session key
  421. * @param {Object} value session value
  422. * @api public
  423. */
  424. Session.prototype.set = function(key, value) {
  425. if (utils.isObject(key)) {
  426. for (var i in key) {
  427. this.settings[i] = key[i];
  428. }
  429. } else {
  430. this.settings[key] = value;
  431. }
  432. };
  433. /**
  434. * Remove value from the session.
  435. *
  436. * @param {String} key session key
  437. * @api public
  438. */
  439. Session.prototype.remove = function(key) {
  440. delete this[key];
  441. };
  442. /**
  443. * Get value from the session.
  444. *
  445. * @param {String} key session key
  446. * @return {Object} value associated with session key
  447. * @api public
  448. */
  449. Session.prototype.get = function(key) {
  450. return this.settings[key];
  451. };
  452. /**
  453. * Send message to the session.
  454. *
  455. * @param {Object} msg final message sent to client
  456. */
  457. Session.prototype.send = function(msg) {
  458. this.__socket__.send(msg);
  459. };
  460. /**
  461. * Send message to the session in batch.
  462. *
  463. * @param {Array} msgs list of message
  464. */
  465. Session.prototype.sendBatch = function(msgs) {
  466. this.__socket__.sendBatch(msgs);
  467. };
  468. /**
  469. * Closed callback for the session which would disconnect client in next tick.
  470. *
  471. * @api public
  472. */
  473. Session.prototype.closed = function(reason) {
  474. logger.debug('session on [%s] is closed with session id: %s', this.frontendId, this.id);
  475. if(this.__state__ === ST_CLOSED) {
  476. return;
  477. }
  478. this.__state__ = ST_CLOSED;
  479. this.__sessionService__.remove(this.id);
  480. this.emit('closed', this.toFrontendSession(), reason);
  481. this.__socket__.emit('closing', reason);
  482. var self = this;
  483. // give a chance to send disconnect message to client
  484. process.nextTick(function() {
  485. self.__socket__.disconnect();
  486. });
  487. };
  488. /**
  489. * Frontend session for frontend server.
  490. */
  491. var FrontendSession = function(session) {
  492. EventEmitter.call(this);
  493. clone(session, this, FRONTEND_SESSION_FIELDS);
  494. // deep copy for settings
  495. this.settings = dclone(session.settings);
  496. this.__session__ = session;
  497. };
  498. util.inherits(FrontendSession, EventEmitter);
  499. FrontendSession.prototype.bind = function(uid, cb) {
  500. var self = this;
  501. this.__sessionService__.bind(this.id, uid, function(err) {
  502. if(!err) {
  503. self.uid = uid;
  504. }
  505. utils.invokeCallback(cb, err);
  506. });
  507. };
  508. FrontendSession.prototype.unbind = function(uid, cb) {
  509. var self = this;
  510. this.__sessionService__.unbind(this.id, uid, function(err) {
  511. if(!err) {
  512. self.uid = null;
  513. }
  514. utils.invokeCallback(cb, err);
  515. });
  516. };
  517. FrontendSession.prototype.set = function(key, value) {
  518. this.settings[key] = value;
  519. };
  520. FrontendSession.prototype.get = function(key) {
  521. return this.settings[key];
  522. };
  523. FrontendSession.prototype.push = function(key, cb) {
  524. this.__sessionService__.import(this.id, key, this.get(key), cb);
  525. };
  526. FrontendSession.prototype.pushAll = function(cb) {
  527. this.__sessionService__.importAll(this.id, this.settings, cb);
  528. };
  529. FrontendSession.prototype.on = function(event, listener) {
  530. EventEmitter.prototype.on.call(this, event, listener);
  531. this.__session__.on(event, listener);
  532. };
  533. /**
  534. * Export the key/values for serialization.
  535. *
  536. * @api private
  537. */
  538. FrontendSession.prototype.export = function() {
  539. var res = {};
  540. clone(this, res, EXPORTED_SESSION_FIELDS);
  541. return res;
  542. };
  543. var clone = function(src, dest, includes) {
  544. var f;
  545. for(var i=0, l=includes.length; i<l; i++) {
  546. f = includes[i];
  547. dest[f] = src[f];
  548. }
  549. };
  550. var dclone = function(src) {
  551. var res = {};
  552. for(var f in src) {
  553. res[f] = src[f];
  554. }
  555. return res;
  556. };