pomelo session

概念 名称
Session 会话
Client 游戏客户端
Frondend 前端服务器
Backend 后端服务器
key/value 键值对
GlobalSession 全局会话
LocalSession 本地会话

Session是游戏服务器存放用户连接的抽象,基于长连接一旦建立就会一直保持。会话是用于保持状态的键值对对象,比如以玩家ID作为键名进行保存玩家信息。

  • 会话对”客户端连接“的抽象,维持着客户端连接和用户信息之间的关系。
  • 会话存在于每个客户端连接中,在客户端可通过标识后绑定到用户ID。

Pomelo中会话是基于key/value的对象,用于维护当前用户信息。比如:用户的ID、连接的前端服务器ID等。

名称 服务器类型 会话类型 会话服务 会话代理类
前端会话 Frontend GlobalSession SessionService FrontendSession
后端会话 Backend LocalSession BackendSessionService BackendSession

GlobalSession 与 LocalSession

  • 全局会话由与客户端直连的前端服务器生成,位于前端服务器,用于存储玩家信息的全局位置。
  • 当传递客户端消息时, 全局会话会被拷贝一份快照,然后和客户端消息一起传递给后端服务器,后端服务器得到一份拷贝的会话也就是本地会话。
  • 本地会话是一个只读对象,只在本地是可读写的。因此对本地会话修改不会影响到全局会话。
  • 如果想将本地会话同步到全局会话,就需要调用push()来推送。

FrontendSession 与 BackendSession

  • 会话由前端服务器维护,前端服务器分发请求给后端服务器时,会复制前端服务器并连同请求一起发送。
  • 直接在前端服务器上的修改只会对本服务器进程生效,并不会影响到用户的全局状态。
  • 如需修改全局会话中的状态信息,需要调用前端服务器提供的RPC服务。
  • 会话在前端服务器创建的,不应在Handler处理程序中访问。

Session

session是对客户端连接的抽象,构造时会调用sessionServicesessionServicesession的具体底层是实现。session实际是抽象了sessionService,先应从sessionService下手。

$ vim pomelo/lib/common/service/sessionService.js

/**
 * 会话用于维持客户端连接与用户信息之间的关系
 * 每个客户端连接都会分配一个会话,当客户端通过认证后应绑定一个用户的唯一ID。
 * 会话应在前端服务器中创建,不应该在处理器中被访问。
 * 前端服务器中存在一个名为FrontendSession的session代理类
 * 后端服务器中存在一个名为BackendSession的session代理类
 */
var Session = function(sid, frontendId, socket, service) {
  EventEmitter.call(this);
  this.id = sid;          // 只读
  this.frontendId = frontendId; // 只读
  this.uid = null;        // 只读
  this.settings = {};

  // 私有
  this.__socket__ = socket;
  this.__sessionService__ = service;
  this.__state__ = ST_INITED;
};

session只在前端服务器中存在,是对客户端连接的抽象,包含siduidsocket

参数 权限 描述
id 只读 当前会话的ID,全局唯一,自增方式来生成。
frontendId 只读 维护当前会话的前端服务器的ID,即serverId。
uid 只读 当前会话所绑定的用户ID
settings 读写 维护key-value map用来描述会话的自定义属性
__socket__ 只读 底层原生Socket的引用,即连接所对应的Socket。
__state__ 只读 用来指明当前Session会话的生命周期状态

session 一旦建立idfrontendIduid__socket____state__都是确定的,只读不可写的,settings也不应该被随意修改。

  • sid实际上是Socket创建时指定的id,即sid = socket.id
  • sid在连接服务器中生成,对应着sioconnector
  • sidconnector内部进行维护,是服务器生命周期内自增的一个ID。
  • uidsession创建时默认为null,当使用session.bind(uid)时,uid在绑定事件发生时出现,bind()被调用时会将uid传递并赋值。
会话方法 描述
session.bind(uid) 使用用户uid绑定会话
session.unbind(uid) 使用用户uid解绑会话
session.set(key, value) 为会话设置一个或多个值
session.remove(key) 根据键名从会话中移除键值
session.get(key) 根据键名从会话中获得键值
session.closed() 会话的已关闭回调,该会话将在下一个tick中断开客户端连接。
session与sessionService

sessionConfig

servers.json服务器配置文件中若设置"frontend": true时表示前端服务器。

{"id": "gate-server-1", "host": "127.0.0.1", "clientPort": 3010, "frontend": true}

Pomelo组件加载过程,在前端服务器中只有设置"frontend": true的服务器才具有全局会话,其余则只具有本地会话。因为只有前端服务器才会接收用户连接。

$ vim pomelo/lib/util/appUtil.js
/**
 * 为应用加载默认组件
 */
module.exports.loadDefaultComponents = function(app) {
  var pomelo = require('../pomelo');
  // 加载系统默认组件
  if (app.serverType === Constants.RESERVED.MASTER) {
    app.load(pomelo.master, app.get('masterConfig'));
  } else {
    app.load(pomelo.proxy, app.get('proxyConfig'));
    if (app.getCurServer().port) {
      app.load(pomelo.remote, app.get('remoteConfig'));
    }
    // 前端服务器加载所需组件
    if (app.isFrontend()) {
      // 加载connection组件用于维护连接,比如连接的状态。
      app.load(pomelo.connection, app.get('connectionConfig'));
      //加载connector组件用于从客户端接收socket并为其分配session等。
      app.load(pomelo.connector, app.get('connectorConfig'));
     // 加载session组件用于管理维护会话,注意只有前端服务器才具有session。
      app.load(pomelo.session, app.get('sessionConfig'));
      // compatible for schedulerConfig
      if(app.get('schedulerConfig')) {
        app.load(pomelo.pushScheduler, app.get('schedulerConfig'));
      } else {
        app.load(pomelo.pushScheduler, app.get('pushSchedulerConfig'));
      }
    }
    app.load(pomelo.backendSession, app.get('backendSessionConfig'));
    app.load(pomelo.channel, app.get('channelConfig'));
    app.load(pomelo.server, app.get('serverConfig'));
  }
  app.load(pomelo.monitor, app.get('monitorConfig'));
};

配置session组件

app.set("sessionConfig", opts);
配置 描述
singleSession true则不允许一个用户同时绑定多个会话,绑定用户一次后将会失效。

sessionService

Pomelo核心中的会话组件是对sessionService的抽象,sessionService服务位于service下,实现了会话管理的具体工作。会话组件实际上是对sessionService服务做了一层代理,将sessionService服务的函数都加载到会话组件中。

一个连接与一个session对应,会话组件还维护具体登录用户与session的绑定信息。一个用户可以有多个客户端登录,对应于多个会话,当需要给客户端推送消息或给客户端返回响应的话,必须通过会话组件获得具体的客户端连接来进行。

比如:网关负责接收外部连接做前端服务器的负载均衡,将连接分派到不同的前端服务器上。连接服务器做路由处理,将路由转发到不同的后端服务器(逻辑服务器)上。

session会调用sessionServicesessionServicesession具体底层实现。 sessionService位于service下,实现session管理的具体工作。

  • sessionService维护所有的原始会话信息,包括不可访问的字段,绑定的uid以及用户自定义的字段。
  • sessionService只存在于前端服务器,会话以每个客户端请求自增1的形式生成,用于管理连接Pomelo的客户端。

如果前端服务器不进行控制,每个请求都会产生一个会话,客户端会在前端服务器的会话服务产生一个会话,自Pomelo0.4.x支持同一账号多处登录,所以sessionService中的会话对应的是一个数组。

如果对会话不做任何处理的话,每刷新一次页面都会对这个会话数组自增1,从暴露的API可以看出sessionService可以用于对连接在前端服务器的客户端踢下线或利用会话id直接在前端服务器发消息给客户端。

例如:判断玩家是否登录

//判断是否重复登录 若session中存在标记则说明已登录
const sessionService = this.app.get("sessionService");
//拒绝重复登录,若需要支持多点登录可注释判断
if(!!sessionService.getByUid(uid)){
    next(null, {code:500, msg:`session service: user ${uid} has been logon`});
    return;
}
方法 描述
sessionService.create(sid, frontendId, socket) 创建并返回内部会话
sessionService.bind(sid, uid, cb) 绑定用户UID和会话ID
sessionService.unbind(sid, uid, cb) 解绑用户UID和会话ID
sessionService.get(sid) 使用指定SESSION ID获取会话
sessionService.getByUid(uid) 使用用户UID获取会话,若不存在则返回undefined。
sessionService.remove(sid) 根据指定SESSION ID移除会话
sessionService.import(sid, key, value, cb) 导入键值对到指定SESSION ID的会话
sessionService.importAll(sid, settings, cb) 为指定SESSION ID的会话导入新的值
sessionService.kick(uid, reason cb) 指定用户ID踢下线
sessionService.kickBySessionId(sid, reason, cb) 指定会话ID对用户踢下线

Pomelo对接收到的Socket进行一层封装形成自定义的siosocketsiosocket中具有两个属性id和真正的Socket,并设置了事件处理函数。当创建自定义的siosocket时会激发connection事件。

前端服务器加载connector组件接收用户连接,连接采用websocket的形式。当websocket接收到Socket时会发射connector事件用于通知外面的connector。当前connector监听到connection事件后,会传入真正的Socket。此时会将事件和Socket进行绑定,绑定过程中若存在connection对象则会增加当前connection的连接数量。然后会为当前的Socket分配session

会话组件跟connector相关,仅被前端服务器加载,为SessionService提供一个组件包装。加载会话组件后,会在app上下文中增加sessionService。可通过app.get("sessionService")获取,用来维护客户端的连接信息,同时生成会话并维护。

与经典TCP进行类比的话,会话中维护的连接可以认为是TCP服务端accept返回的Socket句柄。

FrontendSession

在前端服务器中引入了前端会话,可将其看作是一个内部会话在前端服务器中的傀儡。

前端会话的字段结构

FrontendSession {
    _events: [Object: null prototype] {},
    _eventsCount: 0,
    _maxListeners: undefined,
    id: 1,
    frontendId: 'pkcon-00',
    uid: null,
    __sessionService__: SessionService {
      singleSession: undefined,
      sessions: [Object],
      uidMap: {}
    },
    settings: {},
    __session__: Session {
      _events: [Object: null prototype],
      _eventsCount: 3,
      _maxListeners: undefined,
      id: 1,
      frontendId: 'pkcon-00',
      uid: null,
      settings: {},
      __socket__: [Socket],
      __sessionService__: [SessionService],
      __state__: 0,
      [Symbol(kCapture)]: false
    },
    [Symbol(kCapture)]: false
}
字段 权限 描述
id <session id> 只读 会话编号
frontendId <frontend server id> 只读 前端服务器ID
uid <bound uid> 只读 绑定用户的UID
settings <key-value map> 读写 键值对映射

前端会话专用方法

// 将前端设置键名的值同步到原始的会话中
FrontendSession.prototype.push = function(key, cb) {
  this.__sessionService__.import(this.id, key, this.get(key), cb);
};

FrontendSession.prototype.pushAll = function(cb) {
  this.__sessionService__.importAll(this.id, this.settings, cb);
};

FrontendSession.prototype.on = function(event, listener) {
  EventEmitter.prototype.on.call(this, event, listener);
  this.__session__.on(event, listener);
};

前端会话的作用

  • 通过前端会话可以对settings字段进行设置值,然后通过调用前端会话的push()方法,将设置的settings的值同步到原始的会话中。
  • 通过前端会话的bind()调用可以给会话绑定用户uid
  • 通过前端会话访问会话的只读字段,不过对前端会话中与会话中相同的只读字段的修改并不会反映到原始的会话中。

客户端和服务器通信通过Socket连接,需要session去保持这个连接。

$ vim pomelo/lib/common/service/sessionService.js
/**
 * Bind the session with the the uid.
 *
 * @param {Number} uid User id
 * @api public
 */
Session.prototype.bind = function(uid) {
  this.uid = uid;
  this.emit('bind', uid);
};

当进入游戏时一般会采用session.bind(uid)uid是服务端唯一识别客户端的编号,用于保持服务器和客户端的连接。

例如:获取客户端的IP与端口,采用host:port的形式作为uid

const ip = session.__session__.__socket__.remoteAddress.ip;
const port = session.__session__.__socket__.remoteAddress.port;

发送消息时若需要获得session中的uid,可使用pushMessageByUids

$ vim pomelo/lib/components/connector.js
var getSession = function(self, socket) {
  session.on('bind', function(uid) {
    logger.debug('session on [%s] bind with uid: %s', self.app.serverId, uid);
    // update connection statistics if necessary
    if (self.connection) {
      self.connection.addLoginedUser(uid, {
        loginTime: Date.now(),
        uid: uid,
        address: socket.remoteAddress.ip + ':' + socket.remoteAddress.port
      });
    }
    self.app.event.emit(events.BIND_SESSION, session);
  });
};

前端服务器的sessionService服务会维护内部的session信息,这个session信息会维护连接等信息,用户不应该直接访问或修改它。对于前端服务器访问的时候会使用FrontendSession,可以看作是当前内部session的快照。对于后端服务器的而言则是BackendSession。若要修改内部session的属性,只能通过push方法,修改后下次获取时,无论是FrontendSession还是BackendSession都会是内部session最新的快照。

如果session.set后并没有push,将会影响本次请求后续处理部分所使用的session,而不会影响到下次请求。因为下次请求时使用的session依旧是内部session最新的快照。如果不push仅仅set的话,当请求处理完成后,对其进行的修改将会被丢弃。

BackendSession

后端会话与前端会话类似,后端会话是用于后端服务器的,可以看作是原始会话的代理,其数据字段跟前端会话基本一致。

后端会话是由BackendSessionService创建并维护的,后端服务器接收到请求后,由BackendSessionService根据前端服务器RPC的参数进行创建。

backendSession可以看作是BackendSessionService的组件包装,加载该组件后,会在app的上下文中加入backendSessionService,可通过app.get("backendSessionService")调用获取。

可以被除了master之外的服务器加载。主要是为后端服务器提供BackendSession信息,并通过RPC调用完成一些诸如对原始会话绑定用户uid等操作。另外,backendSession组件无配置选项。

对后端会话服务的每次方法调用实际上都会生成一个远程过程调用,比如通过一个sid获取其BackendSession

同样对于后端会话中字段的修改也不会反映到原始的session中,不过与前端会话一样,后端会话也有pushbindunbind调用,它们的作用与前端会话一样都是用来修改原始中的settings字段或绑定/解绑uid的,不同的是后端会话的这些调用实际上都是namespacesys的远程调用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容