chatOfPomelo是一个聊天室程序,笔者将对chat的服务端game-server进行分析
首先来看它的文件结构
大的结构与hellopomelo一致,都有app,config,logs,node_modules,app.js.
有了hello的经验,我们直奔主题config/servers.json
这里配置着我们自定义的服务器配置
{
"development":{ "connector":[ {"id":"connector-server-1", "host":"127.0.0.1", "port":4050, "clientPort": 3050, "frontend": true} ], "chat":[ {"id":"chat-server-1", "host":"127.0.0.1", "port":6050} ], "gate":[ {"id": "gate-server-1", "host": "127.0.0.1", "clientPort": 3014, "frontend": true} ] },
"production":{ "connector":[ {"id":"connector-server-1", "host":"127.0.0.1", "port":4050, "clientPort": 3050, "frontend": true} ], "chat":[ {"id":"chat-server-1", "host":"127.0.0.1", "port":6050} ], "gate":[ {"id": "gate-server-1", "host": "127.0.0.1", "clientPort": 3014, "frontend": true} ] } }
这里看出来对于development和production环境都作了同样的配置
这次的聊天室是由三个自定义的服务组成
gate,chat,connector
gate– 前端网关服务,用于最初与服务器的连接,并分配connector给客户端
connector– 前端连接服务器,用于与客户端保持websocket长连接,并转发后端服务器处理的协议
chat– 后端聊天服务器,客户端不能与它进行连接,它处理好的逻辑通过connector传递给客户端
看看3个服务的目录结构
注意到gate,connector作为前端服务器,他们都有handler文件夹
而chat后端服务器,不仅有handler还有remote文件夹
在pomelo中,用户自定义的handler文件夹表示要处理与客户端连接相关的事件
而remote文件则表示处理服务器的rpc调用
handler文件夹里的文件会被作为组件加载.且放入到一个route中,以便于客户端向pomelo请求gate.gateHandler.queryEntry这样的请求时,会被精确定位到
gate/handler/gateHandler.js中的queryEntry接口
看看gateHandler.js的代码
module.exports = function(app) { // 导出handler类
return new Handler(app);
};
var Handler = function(app) {
this.app = app;
};
var handler = Handler.prototype;
handler.queryEntry = function(msg, session, next) { // 客户端访问gate的唯一接口
var uid = msg.uid;
if(!uid) {
next(null, {
code: 500
});
return;
}
// get all connectors
var connectors = this.app.getServersByType('connector');
if(!connectors || connectors.length === 0) {
next(null, {
code: 500
});
return;
}
// here we just start `ONE` connector server, so we return the connectors[0]
var res = connectors[0];
// next是向客户端返回消息
next(null, {
code: 200,
host: res.host,
port: res.clientPort
});
};
entryHandler.js(这个文件在connector目录底下,不管它叫什么名字,都是connector的模块)
module.exports = function(app) {
return new Handler(app);
};
var Handler = function(app) {
this.app = app;
};
var handler = Handler.prototype;
// 客户端访问connector的入口
handler.enter = function(msg, session, next) {
var self = this;
var rid = msg.rid;
var uid = msg.username + '*' + rid
// sessionService是pomelo提供的默认服务,维护session信息
var sessionService = self.app.get('sessionService');
//duplicate log in
if( !! sessionService.getByUid(uid)) {
next(null, {
code: 500,
error: true
});
return;
}
session.bind(uid);
session.set('rid', rid);
session.push('rid', function(err) {
if(err) {
console.error('set rid for session service failed! error is : %j', err.stack);
}
});
session.on('closed', onUserLeave.bind(null, self.app));
//通过rpc调用把玩家加入到chat服务中,并返回玩家列表给客户端
self.app.rpc.chat.chatRemote.add(session, uid, self.app.get('serverId'), rid, true, function(users){
next(null, {
users:users
});
});
};
// 玩家关闭浏览以后,通过rpc调用通知chatRemote把这个玩家删除
var onUserLeave = function(app, session) {
if(!session || !session.uid) {
return;
}
app.rpc.chat.chatRemote.kick(session, session.uid, app.get('serverId'), session.get('rid'), null);
};
chat分2部分,一部分是handler,另一部分是remote
handler说明它要处理客户端的信息
remote说明它要处理来自后端服务器的信息
由于我们的connecotr用到了chat的add以及kick,因此我们先看remote的内容
chatremote.js
module.exports = function(app) {
return new ChatRemote(app);
};
var ChatRemote = function(app) {
this.app = app;
this.channelService = app.get('channelService'); // channel也是pomelo的默认服务器
};
// 把玩家添加到指定的channel中 uid玩家id sid服务器id name 频道名 flag 频道标识符
ChatRemote.prototype.add = function(uid, sid, name, flag, cb) {
var channel = this.channelService.getChannel(name, flag);
var username = uid.split('*')[0];
var param = {
route: 'onAdd',
user: username
};
channel.pushMessage(param);
if( !! channel) {
channel.add(uid, sid);
}
cb(this.get(name, flag));
};
// 从channel中获取玩家信息 name 频道名 flag 频道参数 返回玩家列表
ChatRemote.prototype.get = function(name, flag) {
var users = [];
var channel = this.channelService.getChannel(name, flag);
if( !! channel) {
users = channel.getMembers();
}
for(var i = 0; i < users.length; i++) {
users[i] = users[i].split('*')[0];
}
return users;
};
//把玩家从channel中踢出
ChatRemote.prototype.kick = function(uid, sid, name) {
var channel = this.channelService.getChannel(name, false);
// leave channel
if( !! channel) {
channel.leave(uid, sid);
}
var username = uid.split('*')[0];
var param = {
route: 'onLeave',
user: username
};
channel.pushMessage(param);
};
从代码上看出都是与pomelo的内部组件打交道的方法,代码简单,不过这里的所有发往客户端的message都是通过channel来完成的.channel又是通过connector来完成,所以此服务器与客户端并不直接相联,都通过connector来中转
最后是 chat.Handler,在pomelo中,handler都是处理客户端消息的地方,不过由于chat不是前端服务器,这里的send事件是由connector捕捉到以后route过来的,而不是客户端直接与chat服务器进行的连接.
var chatRemote = require('../remote/chatRemote');
module.exports = function(app) {
return new Handler(app);
};
var Handler = function(app) {
this.app = app;
};
var handler = Handler.prototype;
// 处理收到的来自connecotr转发的客户端消息
handler.send = function(msg, session, next) {
var rid = session.get('rid'); //获取玩家的填的channel id
var username = session.uid.split('*')[0];
var channelService = this.app.get('channelService');
var param = {
msg: msg.content,
from: username,
target: msg.target
};
channel = channelService.getChannel(rid, false); // 通过channelid获取与此服务器对应的channel实例
//the target is all users
if(msg.target == '*') {
channel.pushMessage('onChat', param); // channel通过connector把消息发送到客户端
}
else { //the target is specific user
var tuid = msg.target + '*' + rid;
var tsid = channel.getMember(tuid)['sid'];
channelService.pushMessageByUids('onChat', param, [{
uid: tuid,
sid: tsid
}]);
}
next(null, {
route: msg.route
});
};
最后我们来看一下整个网络结构图
gate是唯一的,起到大门的作用.客户端首先发起连接是和gate产生的,由gate决定客户端与哪个connecotr连接,gate可以起到负载均衡的作用
这里可以有多个connector群.负责承载来自客户端的连接,并与后端的chatroom进行交互.
这里也可以有多个chatroom,每个chat可以对应到不同的connector.
后端的chatroom不与客户端直接连接