概念 | 名称 |
---|---|
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
是对客户端连接的抽象,构造时会调用sessionService
,sessionService
是session
的具体底层是实现。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
只在前端服务器中存在,是对客户端连接的抽象,包含sid
、uid
、socket
。
参数 | 权限 | 描述 |
---|---|---|
id | 只读 | 当前会话的ID,全局唯一,自增方式来生成。 |
frontendId | 只读 | 维护当前会话的前端服务器的ID,即serverId。 |
uid | 只读 | 当前会话所绑定的用户ID |
settings | 读写 | 维护key-value map 用来描述会话的自定义属性 |
__socket__ |
只读 | 底层原生Socket的引用,即连接所对应的Socket。 |
__state__ |
只读 | 用来指明当前Session会话的生命周期状态 |
session
一旦建立id
、frontendId
、uid
、__socket__
、__state__
都是确定的,只读不可写的,settings
也不应该被随意修改。
-
sid
实际上是Socket创建时指定的id
,即sid = socket.id
。 -
sid
在连接服务器中生成,对应着sioconnector
。 -
sid
由connector
内部进行维护,是服务器生命周期内自增的一个ID。 -
uid
在session
创建时默认为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 中断开客户端连接。 |
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
会调用sessionService
,sessionService
是session
具体底层实现。 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进行一层封装形成自定义的siosocket
。siosocket
中具有两个属性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
中,不过与前端会话一样,后端会话也有push
、bind
、unbind
调用,它们的作用与前端会话一样都是用来修改原始中的settings
字段或绑定/解绑uid
的,不同的是后端会话的这些调用实际上都是namespace
为sys
的远程调用。