Pomelo聊天服务搭建(二)

chatofpomelo源码分析

在进行客户端和服务端的分析之前,我们先来看一下pomelo对于服务器的配置

  1. developmentproduction为启动时设置的环境,根据 pomelo start -e|-env development|production 中的参数,启动后选择不同的服务设置。
  2. 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建立连接,则可通过connectorchat进行访问,真实的业务逻辑在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等的各种概念不是很明白,我再单独解释一下。

  1. 用户在登陆界面输入的用户名(name),即为在gateHandler.js中接收到的uid(其含义为userId);
  2. 用户在登陆界面输入的房间名(channel),即为connectorentryHandler.js中接收到的rid(roomId),根据rid可获取到对应的channel进行消息的推送。在这里将rid的值保存到session里。
  3. connector中的entryHandler.js接收到的uid,与 1 中的uid不相同,这里的uid表示的是name+'*'+rid;并且该uid在此时与session进行绑定,在同一连接中,一旦绑定,则uidsession中是只读的。在这一步中,根据用户的uid以及当前连接到的connectorserverId(sid),根据channel.add(uid, sid)添加到channel中,因而推送消息时可根据uid获取对应的sid,进行消息的推送。

关于聊天服务搭建中新添加的一些东西以及遇到的一些坑,在后面会再整理出来,这里只就开发来对源码进行一定程度的解析,不涉及到pomelo框架中的具体实现,也不对其接口文档的内容做详细的介绍,有兴趣的朋友可以查看:官方文档

猜你喜欢

转载自blog.csdn.net/diagnoa_wleng/article/details/81585966