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作為前端服務濃ky"/kf/ware/vc/" target="_blank" class="keylink">vcsy/vDx7a809BoYW5kbGVyzsS8/rzQPGJyIC8+DQq2+GNoYXS687bLt/7O8cb3LLK7vfbT0GhhbmRsZXK7udPQcmVtb3RlzsS8/rzQPGJyIC8+DQrU2nBvbWVsb9bQLNPDu6fX1Lao0uW1xGhhbmRsZXLOxLz+vNCx7cq+0qq0psDt0+u/zbuntsvBrL3Tz+C52LXEysK8/jxiciAvPg0KtvhyZW1vdGXOxLz+1PKx7cq+tKbA7bf+zvHG97XEcnBjtffTwzxiciAvPg0KaGFuZGxlcs7EvP680MDvtcTOxLz+u+Gxu9f3zqrX6bz+vNPU2C7H0rfFyOu1vdK7uPZyb3V0ZdbQLNLUsePT2r/Nu6e2y8/ycG9tZWxvx+vH82dhdGUuZ2F0ZUhhbmRsZXIucXVlcnlFbnRyedXi0fm1xMfrx/PKsSy74bG7vqvIt7aozru1vTxiciAvPg0KZ2F0ZS9oYW5kbGVyL2dhdGVIYW5kbGVyLmpz1tC1xHF1ZXJ5RW50cnm907/aPGJyIC8+DQq/tL+0Z2F0ZUhhbmRsZXIuanO1xLT6wus8L3A+DQo8cHJlIGNsYXNzPQ=="brush:java;">
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不與客戶端直接連接