chatofpomelo源码分析
在进行客户端和服务端的分析之前,我们先来看一下pomelo对于服务器的配置
development
和production
为启动时设置的环境,根据pomelo start -e|-env development|production
中的参数,启动后选择不同的服务设置。id
:表示对应服务器的名字(同一类服务器命名应易于辨别)
host
:表示对应服务器的ip地址
port
:表示对应服务器对应的端口号
clientPort
:前端服务器对应的端口号
frontend
:对应的服务器是否是前端服务器,默认为false
{
"development":{
"connector":[
{"id":"connector-server-1", "host":"127.0.0.1", "port":4050, "clientPort": 3050, "frontend": true},
{"id":"connector-server-2", "host":"127.0.0.1", "port":4051, "clientPort": 3051, "frontend": true},
{"id":"connector-server-3", "host":"127.0.0.1", "port":4052, "clientPort": 3052, "frontend": true}
],
"chat":[
{"id":"chat-server-1", "host":"127.0.0.1", "port":6050},
{"id":"chat-server-2", "host":"127.0.0.1", "port":6051},
{"id":"chat-server-3", "host":"127.0.0.1", "port":6052}
],
"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},
{"id":"connector-server-2", "host":"127.0.0.1", "port":4051, "clientPort": 3051, "frontend": true},
{"id":"connector-server-3", "host":"127.0.0.1", "port":4052, "clientPort": 3052, "frontend": true}
],
"chat":[
{"id":"chat-server-1", "host":"127.0.0.1", "port":6050},
{"id":"chat-server-2", "host":"127.0.0.1", "port":6051},
{"id":"chat-server-3", "host":"127.0.0.1", "port":6052}
],
"gate":[
{"id": "gate-server-1", "host": "127.0.0.1", "clientPort": 3014, "frontend": true}
]
}
}
一.客户端结构
那么客户端是如何跟服务端进行通信的呢?我们抽取出部分的前端代码进行分析
pomelo.init(host,port)
首先与前端服务器gate进行初始化连接,再根据route路由到对应的接口进行访问,在与gate
的连接中获取到要连接的connector
服务器的ip和端口(如何获取到在服务端源码分析中会进行介绍),而后根据pomelo.disconnect()
断开连接
再由获取到的connector服务器的ip和port,与对应的connector服务器建立连接(Websocket长连接)。
// 此处uid为前端用户输入的登陆用户名
function queryEntry(uid, callback) {
var route = 'gate.gateHandler.queryEntry';
pomelo.init({
host: window.location.hostname,
port: 3014,
log: true
}, function() {
pomelo.request(route, {
uid: uid
}, function(data) {
pomelo.disconnect();
if(data.code === 500) {
showError(LOGIN_ERROR);
return;
}
callback(data.host, data.port);
});
});
// 根据gate服务器接口分配的connector服务器的ip和port与connector服务建立连接
queryEntry(username, function(host, port) {
pomelo.init({
host: host,
port: port,
log: true
}, function() {
var route = "connector.entryHandler.enter";
pomelo.request(route, {
username: username,
rid: rid
}, function(data) {
if(data.error) {
showError(DUPLICATE_ERROR);
return;
}
setName();
setRoom();
showChat();
initUserList(data);
});
});
});
二.服务端结构
servers目录下为pomelo的三种服务器(可根据业务自定义服务器),分别为gate、connector和chat,gate用于负载均衡(后续源码中会分析);connector用于与前端的连接服务;chat才是真正用于业务处理的服务器。服务器的配置在下面的源码中会进行分析。config目录下是各个配置的文件;logs目录为pomelo的日志文件夹。
服务端程序入口:app.js
(进行一些初始化配置后启动服务)
var pomelo = require('pomelo');
var routeUtil = require('./app/util/routeUtil');
/**
* Init app for client.
*/
var app = pomelo.createApp();
app.set('name', 'chatofpomelo');
// app configure
app.configure('production|development', function() {
// route configures
app.route('chat', routeUtil.chat);
app.set('connectorConfig', {
connector: pomelo.connectors.sioconnector,
// 'websocket', 'polling-xhr', 'polling-jsonp', 'polling'
transports: ['websocket', 'polling'],
heartbeats: true,
closeTimeout: 60 * 1000,
heartbeatTimeout: 60 * 1000,
heartbeatInterval: 25 * 1000
});
// filter configures
app.filter(pomelo.timeout());
});
// start app
app.start();
process.on('uncaughtException', function(err) {
console.error(' Caught exception: ' + err.stack);
});
其中
production|development
为服务启动时设置的参数(pomelo start -e production),默认启动为development。
接下来,我们来看一下前端与gate服务器的连接
在game-server/app/servers/gate/handler/gateHandler.js中:
var dispatcher = require('../../../util/dispatcher');
module.exports = function(app) {
return new Handler(app);
};
var Handler = function(app) {
this.app = app;
};
var handler = Handler.prototype;
/**
* Gate handler that dispatch user to connectors.
*
* @param {Object} msg message from client
* @param {Object} session
* @param {Function} next next stemp callback
*
*/
handler.queryEntry = function(msg, session, next) {
// 获取前端传的 uid---登陆用户名
var uid = msg.uid;
// 若未传入该参数,则返回500错误
if(!uid) {
next(null, {
code: 500
});
return;
}
// get all connectors
// 根据app对象获取所有的connector服务器
var connectors = this.app.getServersByType('connector');
if(!connectors || connectors.length === 0) {
next(null, {
code: 500
});
return;
}
// select connector
// 在dispatcher.js中根据uid分配一个connector服务器
var res = dispatcher.dispatch(uid, connectors);
next(null, {
code: 200,
host: res.host,
port: res.clientPort
});
};
那么,dispatcher.js又是如何根据uid分配connector服务器的呢?
var crc = require('crc');
module.exports.dispatch = function(uid, connectors) {
// 计算uid字段的crc32校验码,
//然后用这个校验码作为key, 跟同类应用服务器数目取余, 得到要路由到的服务器编号
var index = Math.abs(crc.crc32(uid)) % connectors.length;
return connectors[index];
};
根据路由 connector.entryHandler.enter
,对应源码如下:
module.exports = function(app) {
return new Handler(app);
};
var Handler = function(app) {
this.app = app;
};
var handler = Handler.prototype;
/**
* New client entry chat server.
*
* @param {Object} msg request message
* @param {Object} session current session object
* @param {Function} next next stemp callback
* @return {Void}
*/
handler.enter = function(msg, session, next) {
var self = this;
// rid表示 roomId,即用户进入聊天室时输入的房间号(channel)
var rid = msg.rid;
// 注意此处的uid与之前的(登陆时输入的uid)uid代表不同的含义,前面的uid仅指登陆的用户名,此处的uid则表示该房间中当前登陆用户的唯一标识
var uid = msg.username + '*' + rid
var sessionService = self.app.get('sessionService');
//duplicate log in
// 判断是否重复登陆,此处为拒绝重复登陆,若要支持多点登陆,可注释掉此判断
if( !! sessionService.getByUid(uid)) {
next(null, {
code: 500,
error: true
});
return;
}
// 将uid与session进行绑定,一旦连接建立,绑定的uid就是只读的
session.bind(uid);
// 将rid参数放入session中,便于chat服务器从session中获取rid的值
session.set('rid', rid);
// 使session的设置生效
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));
//put user into channel
// 将该登陆用户添加到由rid获得的channel中,并使uid与sid相对应
self.app.rpc.chat.chatRemote.add(session, uid, self.app.get('serverId'), rid, true, function(users){
next(null, {
users:users
});
});
};
/**
* User log out handler
*
* @param {Object} app current application
* @param {Object} session current session object
*
*/
var onUserLeave = function(app, session) {
if(!session || !session.uid) {
return;
}
// 进程间rpc调用,用户推出时将其从房间中踢出
app.rpc.chat.chatRemote.kick(session, session.uid, app.get('serverId'), session.get('rid'), null);
};
一旦与connector
建立连接,则可通过connector
对chat
进行访问,真实的业务逻辑在chat/handler/chatHandler.js
中进行处理,代码如下:
var chatRemote = require('../remote/chatRemote');
module.exports = function(app) {
return new Handler(app);
};
var Handler = function(app) {
this.app = app;
};
var handler = Handler.prototype;
/**
* Send messages to users
*
* @param {Object} msg message from client
* @param {Object} session
* @param {Function} next next stemp callback
*
*/
handler.send = function(msg, session, next) {
// 获取在connector中保存在session里的rid值
var rid = session.get('rid');
// 根据uid获取登陆的用户名,此uid=username+'*'+rid;
var username = session.uid.split('*')[0];
// 根据app对象获取channelService对象,可根据此对象向房间的所有成员push消息
var channelService = this.app.get('channelService');
// push的消息内容,以及对应前端监听的事件 'onChat'
var param = {
route: 'onChat',
msg: msg.content,
from: username,
target: msg.target
};
// 根据rid(roomId)获取对应的channel对象(消息的推送是以channel为基础的)
channel = channelService.getChannel(rid, false);
//the target is all users
if(msg.target == '*') {
// 根据channel推送消息,即推送给房间的所有成员
channel.pushMessage(param);
}
//the target is specific user
else {
// 发送消息给房间中的某个特定用户,得到他的uid
var tuid = msg.target + '*' + rid;
// 根据uid在channel中获取该用户对应连接的sid(serverId)
var tsid = channel.getMember(tuid)['sid'];
// 根据uids推送消息,即获取到某个uid连接的sid,再把消息发送到对应sid的用户
channelService.pushMessageByUids(param, [{
uid: tuid,
sid: tsid
}]);
}
next(null, {
route: msg.route
});
};
到这里,对于整个chatofpomelo的解析就结束了,可能对于uid、sid、rid等的各种概念不是很明白,我再单独解释一下。
- 用户在登陆界面输入的用户名
(name)
,即为在gateHandler.js
中接收到的uid
(其含义为userId
);- 用户在登陆界面输入的房间名
(channel)
,即为connector
中entryHandler.js
中接收到的rid(roomId)
,根据rid可获取到对应的channel进行消息的推送。在这里将rid
的值保存到session
里。connector
中的entryHandler.js
接收到的uid
,与 1 中的uid
不相同,这里的uid
表示的是name+'*'+rid
;并且该uid
在此时与session
进行绑定,在同一连接中,一旦绑定,则uid
在session
中是只读的。在这一步中,根据用户的uid
以及当前连接到的connector
的serverId(sid)
,根据channel.add(uid, sid)
添加到channel
中,因而推送消息时可根据uid
获取对应的sid
,进行消息的推送。
关于聊天服务搭建中新添加的一些东西以及遇到的一些坑,在后面会再整理出来,这里只就开发来对源码进行一定程度的解析,不涉及到pomelo框架中的具体实现,也不对其接口文档的内容做详细的介绍,有兴趣的朋友可以查看:官方文档