sessionService.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  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 userstate 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 userstate 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 userstate id.
  216. *
  217. * @param {Number} uid userstate 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 userstate 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, cb) {
  257. var session = this.get(sid);
  258. if(session) {
  259. // notify client
  260. session.closed('kick');
  261. process.nextTick(function() {
  262. utils.invokeCallback(cb);
  263. });
  264. } else {
  265. process.nextTick(function() {
  266. utils.invokeCallback(cb);
  267. });
  268. }
  269. };
  270. /**
  271. * Get client remote address by session id.
  272. *
  273. * @param {Number} sid session id
  274. * @return {Object} remote address of client
  275. *
  276. * @memberOf SessionService
  277. */
  278. SessionService.prototype.getClientAddressBySessionId = function(sid) {
  279. var session = this.get(sid);
  280. if(session) {
  281. var socket = session.__socket__;
  282. return socket.remoteAddress;
  283. } else {
  284. return null;
  285. }
  286. };
  287. /**
  288. * Send message to the client by session id.
  289. *
  290. * @param {String} sid session id
  291. * @param {Object} msg message to send
  292. *
  293. * @memberOf SessionService
  294. * @api private
  295. */
  296. SessionService.prototype.sendMessage = function(sid, msg) {
  297. var session = this.sessions[sid];
  298. if(!session) {
  299. logger.debug('Fail to send message for non-existing session, sid: ' + sid + ' msg: ' + msg);
  300. return false;
  301. }
  302. return send(this, session, msg);
  303. };
  304. /**
  305. * Send message to the client by userstate id.
  306. *
  307. * @param {String} uid userId
  308. * @param {Object} msg message to send
  309. *
  310. * @memberOf SessionService
  311. * @api private
  312. */
  313. SessionService.prototype.sendMessageByUid = function(uid, msg) {
  314. var sessions = this.uidMap[uid];
  315. if(!sessions) {
  316. logger.debug('fail to send message by uid for non-existing session. uid: %j',
  317. uid);
  318. return false;
  319. }
  320. for(var i=0, l=sessions.length; i<l; i++) {
  321. send(this, sessions[i], msg);
  322. }
  323. };
  324. /**
  325. * Iterate all the session in the session service.
  326. *
  327. * @param {Function} cb callback function to fetch session
  328. * @api private
  329. */
  330. SessionService.prototype.forEachSession = function(cb) {
  331. for(var sid in this.sessions) {
  332. cb(this.sessions[sid]);
  333. }
  334. };
  335. /**
  336. * Iterate all the binded session in the session service.
  337. *
  338. * @param {Function} cb callback function to fetch session
  339. * @api private
  340. */
  341. SessionService.prototype.forEachBindedSession = function(cb) {
  342. var i, l, sessions;
  343. for(var uid in this.uidMap) {
  344. sessions = this.uidMap[uid];
  345. for(i=0, l=sessions.length; i<l; i++) {
  346. cb(sessions[i]);
  347. }
  348. }
  349. };
  350. /**
  351. * Get sessions' quantity in specified server.
  352. *
  353. */
  354. SessionService.prototype.getSessionsCount = function() {
  355. return utils.size(this.sessions);
  356. };
  357. /**
  358. * Send message to the client that associated with the session.
  359. *
  360. * @api private
  361. */
  362. var send = function(service, session, msg) {
  363. session.send(msg);
  364. return true;
  365. };
  366. /**
  367. * Session maintains the relationship between client connection and userstate information.
  368. * There is a session associated with each client connection. And it should bind to a
  369. * userstate id after the client passes the identification.
  370. *
  371. * Session is created in frontend server and should not be accessed in handler.
  372. * There is a proxy class called BackendSession in backend servers and FrontendSession
  373. * in frontend servers.
  374. */
  375. var Session = function(sid, frontendId, socket, service) {
  376. EventEmitter.call(this);
  377. this.id = sid; // r
  378. this.frontendId = frontendId; // r
  379. this.uid = null; // r
  380. this.settings = {};
  381. // private
  382. this.__socket__ = socket;
  383. this.__sessionService__ = service;
  384. this.__state__ = ST_INITED;
  385. };
  386. util.inherits(Session, EventEmitter);
  387. /*
  388. * Export current session as frontend session.
  389. */
  390. Session.prototype.toFrontendSession = function() {
  391. return new FrontendSession(this);
  392. };
  393. /**
  394. * Bind the session with the the uid.
  395. *
  396. * @param {Number} uid User id
  397. * @api public
  398. */
  399. Session.prototype.bind = function(uid) {
  400. this.uid = uid;
  401. this.emit('bind', uid);
  402. };
  403. /**
  404. * Unbind the session with the the uid.
  405. *
  406. * @param {Number} uid User id
  407. * @api private
  408. */
  409. Session.prototype.unbind = function(uid) {
  410. this.uid = null;
  411. this.emit('unbind', uid);
  412. };
  413. /**
  414. * Set value for the session.
  415. *
  416. * @param {String} key session key
  417. * @param {Object} value session value
  418. * @api public
  419. */
  420. Session.prototype.set = function(key, value) {
  421. this.settings[key] = value;
  422. };
  423. /**
  424. * Get value from the session.
  425. *
  426. * @param {String} key session key
  427. * @return {Object} value associated with session key
  428. * @api public
  429. */
  430. Session.prototype.get = function(key) {
  431. return this.settings[key];
  432. };
  433. /**
  434. * Send message to the session.
  435. *
  436. * @param {Object} msg final message sent to client
  437. */
  438. Session.prototype.send = function(msg) {
  439. this.__socket__.send(msg);
  440. };
  441. /**
  442. * Send message to the session in batch.
  443. *
  444. * @param {Array} msgs list of message
  445. */
  446. Session.prototype.sendBatch = function(msgs) {
  447. this.__socket__.sendBatch(msgs);
  448. };
  449. /**
  450. * Closed callback for the session which would disconnect client in next tick.
  451. *
  452. * @api public
  453. */
  454. Session.prototype.closed = function(reason) {
  455. logger.debug('session on [%s] is closed with session id: %s', this.frontendId, this.id);
  456. if(this.__state__ === ST_CLOSED) {
  457. return;
  458. }
  459. this.__state__ = ST_CLOSED;
  460. this.__sessionService__.remove(this.id);
  461. this.emit('closed', this.toFrontendSession(), reason);
  462. this.__socket__.emit('closing', reason);
  463. var self = this;
  464. // give a chance to send disconnect message to client
  465. process.nextTick(function() {
  466. self.__socket__.disconnect();
  467. });
  468. };
  469. /**
  470. * Frontend session for frontend server.
  471. */
  472. var FrontendSession = function(session) {
  473. EventEmitter.call(this);
  474. clone(session, this, FRONTEND_SESSION_FIELDS);
  475. // deep copy for settings
  476. this.settings = dclone(session.settings);
  477. this.__session__ = session;
  478. };
  479. util.inherits(FrontendSession, EventEmitter);
  480. FrontendSession.prototype.bind = function(uid, cb) {
  481. var self = this;
  482. this.__sessionService__.bind(this.id, uid, function(err) {
  483. if(!err) {
  484. self.uid = uid;
  485. }
  486. utils.invokeCallback(cb, err);
  487. });
  488. };
  489. FrontendSession.prototype.unbind = function(uid, cb) {
  490. var self = this;
  491. this.__sessionService__.unbind(this.id, uid, function(err) {
  492. if(!err) {
  493. self.uid = null;
  494. }
  495. utils.invokeCallback(cb, err);
  496. });
  497. };
  498. FrontendSession.prototype.set = function(key, value) {
  499. this.settings[key] = value;
  500. };
  501. FrontendSession.prototype.get = function(key) {
  502. return this.settings[key];
  503. };
  504. FrontendSession.prototype.push = function(key, cb) {
  505. this.__sessionService__.import(this.id, key, this.get(key), cb);
  506. };
  507. FrontendSession.prototype.pushAll = function(cb) {
  508. this.__sessionService__.importAll(this.id, this.settings, cb);
  509. };
  510. FrontendSession.prototype.on = function(event, listener) {
  511. EventEmitter.prototype.on.call(this, event, listener);
  512. this.__session__.on(event, listener);
  513. };
  514. /**
  515. * Export the key/values for serialization.
  516. *
  517. * @api private
  518. */
  519. FrontendSession.prototype.export = function() {
  520. var res = {};
  521. clone(this, res, EXPORTED_SESSION_FIELDS);
  522. return res;
  523. };
  524. var clone = function(src, dest, includes) {
  525. var f;
  526. for(var i=0, l=includes.length; i<l; i++) {
  527. f = includes[i];
  528. dest[f] = src[f];
  529. }
  530. };
  531. var dclone = function(src) {
  532. var res = {};
  533. for(var f in src) {
  534. res[f] = src[f];
  535. }
  536. return res;
  537. };