烦恼一般都是想太多了。

0%

Pomelo中登录流程

Pomelo做棋牌和小游戏确实是不错的,所以来了解一下其登录流程。客户端连接的建立是由 connector 组件完成的,在我们配置的 servers.json 中,只有前端服务器 frontend:true 才会加载这个组件(一般来说,我们只会设置 game, connector 为前端服务器)。同时这个组件会依赖 session 组件来维护连接信息。

客户端

一般来说,我们会和 gate 建立连接,请求一个可以接入服务器的 connector。 之后我们就会和对应的 connector 建立连接。我们以 网易 demo中的 lordofpomelo 来看。

// web-server/public/js/ui/clientManager.js

$.post(httpHost + 'login', {username: username, password: pwd}, function(data) {
if (data.code === 501) {
alert('Username or password is invalid!');
loading = false;
return;
}
if (data.code !== 200) {
alert('Username is not exists!');
loading = false;
return;
}

authEntry(data.uid, data.token, function() {
loading = false;
});
localStorage.setItem('username', username);
});

有点意外哈,其是用 post 的方式在当前网页上进行登录的。我们看一下登录的过程:

// web-server/app.js

app.post('/login', function(req, res) {
var msg = req.body;

var username = msg.username;
var pwd = msg.password;
if (!username || !pwd) {
res.send({code: 500});
return;
}

userDao.getUserByName(username, function(err, user) {
if (err || !user) {
console.log('username not exist!');
res.send({code: 500});
return;
}
if (pwd !== user.password) {
// TODO code
// password is wrong
console.log('password incorrect!');
res.send({code: 501});
return;
}

console.log(username + ' login!');
res.send({code: 200, token: Token.create(user.id, Date.now(), secret), uid: user.id});
});
});

简单的验证了一下账号密码后,通过 user.id,和当前时间生成一个 token 进行返回。

从第一部分我们知道,在登录成功后,其调用了 authEntry() 方法。

function authEntry(uid, token, callback) {
queryEntry(uid, function(host, port) {
entry(host, port, token, callback);
});
}

function queryEntry(uid, callback) {
pomelo.init({host: config.GATE_HOST, port: config.GATE_PORT, log: true}, function() {
pomelo.request('gate.gateHandler.queryEntry', { uid: uid}, function(data) {
pomelo.disconnect();

if(data.code === 2001) {
alert('Servers error!');
return;
}

callback(data.host, data.port);
});
});

function entry(host, port, token, callback) {
// init socketClient
// TODO for development
if(host === '127.0.0.1') {
host = config.GATE_HOST;
}
pomelo.init({host: host, port: port, log: true}, function() {
pomelo.request('connector.entryHandler.entry', {token: token}, function(data) {
var player = data.player;

if (callback) {
callback(data.code);
}

if (data.code == 1001) {
alert('Login fail!');
return;
} else if (data.code == 1003) {
alert('Username not exists!');
return;
}

if (data.code != 200) {
alert('Login Fail!');
return;
}

// init handler
loginMsgHandler.init();
gameMsgHandler.init();

if (!player || player.id <= 0) {
switchManager.selectView("heroSelectPanel");
} else {
afterLogin(data);
}
});
});
}

其基本经历了,与 game建立连接,获取 connector, 与 connector 建立连接,进行进入请求的过程。接下来到客户端我们的处理。

服务端

客户端最终发送了一个 pomelo.request('connector.entryHandler.entry', {token: token}, function(data) {} 的请求,这个请求根据其路由回由 connector.entryHandler.entry 处理,一目了然。

pro.entry = function(msg, session, next) {
var token = msg.token, self = this;

if(!token) {
next(new Error('invalid entry request: empty token'), {code: Code.FAIL});
return;
}

var uid, players, player;
async.waterfall([
function(cb) {
// auth token 根据 token 来计算机出 user
self.app.rpc.auth.authRemote.auth(session, token, cb);
}, function(code, user, cb) {
// query player info by user id
if(code !== Code.OK) {
next(null, {code: code});
return;
}

if(!user) {
next(null, {code: Code.ENTRY.FA_USER_NOT_EXIST});
return;
}

uid = user.id;
userDao.getPlayersByUid(user.id, cb);
}, function(res, cb) {
// generate session and register chat status
players = res;
self.app.get('sessionService').kick(uid, cb);
}, function(cb) {
// 将 serssion 与 uid 相绑定。
session.bind(uid, cb);
}, function(cb) {
if(!players || players.length === 0) {
next(null, {code: Code.OK});
return;
}

player = players[0];

// 设置一些信息到 session 中
session.set('serverId', self.app.get('areaIdMap')[player.areaId]);
session.set('playername', player.name);
session.set('playerId', player.id);
session.on('closed', onUserLeave.bind(null, self.app));
session.pushAll(cb);
}, function(cb) {
// 加入频道
self.app.rpc.chat.chatRemote.add(session, player.userId, player.name,
channelUtil.getGlobalChannelName(), cb);
}
], function(err) {
if(err) {
next(err, {code: Code.FAIL});
return;
}

next(null, {code: Code.OK, player: players ? players[0] : null});
});
};

看起来很简单,先是验证了一下 token 是否有效,接着就调用了一系列的函数。这里不禁就要问一下 waterfall 是什么意思。根据 官方定义。 waterfall 是序列化的执行一个数组内的异步函数,每个函数的输出作为下一函数的输入;如果有任何函数出错,那么就会立刻执行最后的那个回调函数。同时异步函数的形式的最后一个回调函数形入 function cb(err, results….)。

其执行过程是:

  1. 验证 token(是否过期,用户是否有效),成功则返回一个用户。
  2. 获取用户角色列表。
  3. 从session里面踢掉以前登录的这个用户,或者生成一个新的session。
  4. 绑定当前登录的用户到 session
  5. 设置 session 的一些键值
  6. 在全局频道内添加此用户
  7. 返回。

我在这里有一个疑问就是,session 是在哪里建立的?是 connector 服务器(前端服务器) 加载的 connector组件, session 组件在连接建立的时候创建的。所以能传递给 entry 函数。