开始学习以nodejs为技术支持的网易开源游戏服务器pomelo,撰文权作笔记。

Phase 0. 目录

Phase I. 了解Pomelo

在学习pomelo之前,我们需要对pomelo有一个大略的了解。首先是资料的来源,pomelo中文wiki主页,几乎所有你需要的子文档都在这个入口。接下来我们可以从《pomelo介绍》入手,大致了解下pomelo是什么。接下来是《pomelo架构概览》,来了解pomelo的架构,大致由哪几部分组成,它们分别负责做什么事情。

了解了基本知识之后,我们接下来可以通过《pomelo快速使用指南》,学习如何安装pomelo,如何创建pomelo项目,如何启动、查看、关闭服务器。之后,我们需要通过《pomelo启动流程》了解,在我们启动pomelo服务器之后,pomelo在启动流程中干了点什么,以及了解各种类型的组件。

关于pomelo的性能,官方有一份测试数值,《pomelo性能参考》。

此外,还有两个很有用的地方,就是pomelo的github问题列表,分为中文和英文两个:https://github.com/NetEase/pomelo/issues 和 https://github.com/NetEase/pomelo-cn/issues

Phase II. 学习Pomelo之前

一开始我想说网易的文档写的不错,至少我能看到很多很多文档。看完之后发现,真TM垃圾,几篇文章你看着貌似不同,其实讲的都是差不多的东西。我很想在这里写,请读XXX文章,然后你就会Pomelo了,至少这篇文章把事情讲清楚了,但是很遗憾的是没有。《Pomelo Framework》这篇文章其实和《pomelo架构概览》这篇文章讲的内容差不多,都是简单描述了下pomelo大致的结构组成,以及每个组件的一些设计思想。我不能说写得不好,这些文章有其自己的作用,但是,这两篇文章最大的问题就是过于抽象。举个例子来说,你要去买房,中介扔了两张房屋设计图,你看了就明白屋子的光照、朝向、下水、空间感、共用部分,等等细节了?不可能。这种房子你敢买?

好了,吐槽就到这里,总之,如果需要对pomelo有一个简单概念,并了解其中的结构的话,前述的两篇文章还可以,接下来的学习,还是要靠自己写demo以及读代码来解决。

Phase III. Pomelo使用的第三方模块

Pomelo使用了express作为其前端web框架。而后端则没有使用任何第三方的模块。读者可以

ls -l game-server/node_modules
ls -l web-server/node_modules

来查看第三方模块。

这里需要提的是express可以使用jade作为其前端显示渲染组件,而其默认在express安装的时候是不安装的,这里需要读者手动进行安装:

cd web-server/node_modules/express
npm install jade

这样就可以了,否则在运行express的时候,它会提示你模块jade无法找到。

此外,如果你在写nodejs后端程序的时候也需要用到模板引擎jade的话,也需要安装:

cd game-server
npm install jade

在这篇文章中,我就不详细描述jade和express的使用了,请参考我写的文章:

我会持续更新,此外,在pomelo学习中,如果我用到了第三方模块组件的话,我会在下面附加内容,持续更新。

其他第三方组件

Phase IV. 学习pomelo

服务器的配置和启动

configure

这个函数我觉得叫configure这个名字未免有点歧义,它所做的事情远远超过了config的范畴。我对它的理解是,在服务器启动之前针对某一类(或全体类型)服务器的初始化事件。它包含三个参数,第一个是针对的环境,可以写多个用"|"分隔,e.g 'production|development';第二个是针对的服务器类型,同样可以使用"|"分隔,e.g 'game';第三个参数是真实执行的操作函数。

这个函数还有一个非常有用的地方,就是将很多服务器的相同的初始化逻辑写在一个公共的configure函数中,将初始化好的资源用set写入到app中,然后在对应的服务器代码中get出来,就能直接用了。在我写的demo中,我将初始化连接好的redisClient在configure中处理了。

[codesyntax lang="javascript"]

// app configure
app.configure('production|development', function() {
    // route configures
    var gameRouter = rsloader.loadCode('GameRouter');
    app.route('game', gameRouter.route);
    // filter configures
    app.filter(pomelo.timeout());
    // redis
    app.set('redis', rclient);
});

[/codesyntax]

route

路由,在服务器接收到某个请求的时候,针对某一类型服务器进行的分发策略。两个参数,第一个是服务器类型名,第二个是路由函数。

[codesyntax lang="javascript"]

var GameRouter = function() {};
GameRouter.router = rsloader.loadCode('Router');
GameRouter.route = function(session, msg, app, callback) {
    var roomId = session.get('roomId'); // room id
    if (roomId == null || !roomId) {
        // callback(new Error('[GameRouter] Cannot find room id in session!'));
        throw new Error('[GameRouter] Cannot find room id in session!');
    } else {
        var server = GameRouter.router.routeServerViaId(app, roomId, 'game');
        callback(null, server.id);
    }
};

module.exports = GameRouter;

[/codesyntax]

filter

对于filter了解不多,现在用的只有一个timeout。请参照configure部分的示例代码。

set / get

set后,get出来,这个就不多说了。这里要提的只有一点,set还有第三个参数,如果给true的话,则可以直接在app中访问。

[codesyntax lang="javascript"]

app.set('redis', redisClient);
app.get('redis'); // redisClient
app.set('redis', redisClient, true);
app.redis; // redisClient

[/codesyntax]

Session控制

session在一个有状态的服务器中起着非常重要的作用,接下来我们就看下在pomelo中session是如何使用的。

session的获得

在充当连接的frontend服务器上,每个请求都能直接获取到session:

[codesyntax lang="javascript"]

/**
 * Create a room.
 * @param  {Object}   msg     request message
 * @param  {Object}   session current session object
 * @param  {Function} next    next stemp callback
 * @return {Void}
 */
proto.createRoom = function(msg, session, next) {
    ...
};

[/codesyntax]

此外,在frontend服务器上,也能通过Application的sessionService来执行获得、创建session等一系列的操作。具体的API请参考源代码,这里提供文件位置:node_modules/pomelo/lib/common/service/sessionService.js。

[codesyntax lang="javascript"]

var GateHandler = function GateHandler(app) {
    ...
    this.sessionService = app.get('sessionService');
};

[/codesyntax]

session的存取

这里需要了解的一点,session分为本地和global两种,在每个frontend请求中或得到的,和从sessionService中获得到的session对象都是一个本地session对象,你可以理解为一个只读的session对象,在对本地对象进行操作之后,如果需要将其更新到其他API访问得到的global对象中的话,需要额外的push操作。

[codesyntax lang="javascript"]

session.set('userId', 21);
session.get('userId'); // 21
session.push('userId', function(err) { // push single key
    if (err != null) {
        console.log('Push session "userId" to global failed!');
    } else {
        console.log('Push session "userId" to global succeeded!');
    }
});
session.pushAll(); // push all

[/codesyntax]

后端服务器中的session

前面我们说的session操作,都是在frontend服务器中,也就是和客户端直接连接的服务器中,而根据pomelo的设计,后端的远程调用服务器(RPC)是无法访问global session的。也就是说,backend服务器无法在其代码中直接访问和操作global session。当然,pomelo还是提供了方法让我们能够操作global session,下面就来看点例子:

我们可以使用tricky方法在backend的remote服务器上获得到sessionService。在app.js,总启动脚本中,添加以下代码(这里我们假设远程服务器名为game)。

[codesyntax lang="javascript"]

app.configure('production|development', 'game', function() {
    app.load(pomelo.session, app.get('sessionConfig'));
};

[/codesyntax]

然后你就会很惊奇地发现,在gameRemote的app中,有了sessionService这个对象。但是,这其实是没有意义的,因为这个sessionService和frontend服务器中的sessionService不是一个东西,他们也无法互相同步数据。

所以,官方提供给我们的是其他方法:

[codesyntax lang="javascript"]

var GameRemote = function GameRemote(app) {
    ...
    this.localSessionService = this.app.components.__localSession__;
};

var proto = GameRemote.prototype;

proto.resetPlayerSession = function(roomId, callback) {
    ...
    this.localSessionService.pushAll(info.frontendId, info.sessionId, {'roomId': null, 'playing': false}, function(err, result) {
        ...
    });
};

[/codesyntax]

这里我们使用的是remote服务器中的local session,使用其push功能,将内容更新到global session中。当然,这个local session还有很多其他API,这里给出源代码地址:node_modules/pomelo/lib/common/service/localSessionService.js

frontend和backend之间的RPC

从结构上来看,frontend作为承载连接的服务器和backend后端的逻辑服务器分开,对于进行横向扩展有非常好的效果,所以一般都是需要有frontend和backend两种服务器分离的。那么这里就牵涉到两者之间相互交流的RPC调用了。在pomelo里,RPC调用被封装得非常容易使用,看看例子就明白了,这里需要明确的只有一点,就是rpc调用在frontend侧,需要多传入一个session参数,这个参数不会被backend接收到,一般是用来储存route逻辑需要的一些信息。

[codesyntax lang="javascript"]

// frontend连接服务器:gateHandler.js
/**
 * Join a room.
 * @param  {Object}   msg     request message
 * @param  {Object}   session current session object
 * @param  {Function} next    next stemp callback
 * @return {Void}
 */
proto.joinRoom = function(msg, session, next) {
    ...
    this.app.rpc.game.gameRemote.join(session, session.id, session.frontendId, roomId, this.app.get('serverId'), userId, userName, function(err, reply) {
        ...
    });
};
// backend逻辑服务器:gameRemote.js
/**
 * Join into a room.
 * @param {Number} sessionId
 * @param {Number} frontendId
 * @param {Number} roomId
 * @param {String} serverId
 * @param {Number} userId
 * @param {String} userName
 * @param {Function} callback
 * @return {Void}
 */
proto.join = function(sessionId, frontendId, roomId, serverId, userId, userName, callback) {
    ...
};

[/codesyntax]

服务器的监控

Pomelo官方提供的监控工具

官方提供的监控工具一共有三个:

这三者,pomelo-monitor是最基础的工具,它提供了系统级别的性能监控api和单服务器级别的node节点监控api。然后pomelo-admin是建筑在它之上的一个监控系统组件,允许第三方为它添加监控模块和脚本,并能插入到某个pomelo服务器的运行中,进行实时监控。pomelo-admin-web是一个基于express的网页pomelo-admin界面,它没有什么很实质的功能,只是将pomelo-admin收集的数据进行展示。

pomelo-minitor的使用请参照官方文档中的范例,基本上就没问题了,其中有几个bug,请参考这里:https://github.com/NetEase/pomelo-cn/issues/93。pomelo-admin的使用请参照下面的代码,将其插入到pomelo服务器的app.js文件中,在服务器运行之前执行。pomelo-admin-web基本上是一个单独的web服务器,请参照官方文档中的方法将其启动。

[codesyntax lang="javascript"]

// dev monitor
app.configure('development', function() {
    app.enable('systemMonitor'); // enable pomelo-admin (viewable with pomelo-admin-web)
});

[/codesyntax]

此外,如果需要启动官方自带的RPC日志和FORWARD日志的话,需要一点小改动。查看了源代码你会发现rpc日志和forward日志都是用info级别函数打出来的,而这两个日志的级别是配置在game-server/config/log4js.js里的levels这个字段里的,将其配置为info或低于info的级别都可以。

而rpc日志还有个bug,具体请参考这里:https://github.com/NetEase/pomelo-cn/issues/95

修改node_modules/pomelo/lib/application.js:139:

[codesyntax lang="javascript"]

// before
    this.load(pomelo.proxy, this.get('proxyConfig'));
// after
    var proxyConfig = this.get('proxyConfig');
    if (typeof proxyConfig == 'undefined' && this.get('env') == 'development') {
        proxyConfig = {enableRpcLog: true};
    }
    this.load(pomelo.proxy, proxyConfig);

[/codesyntax]

这样rpc和forward日志就正常运作了,pomelo-admin-web里的这两个监控项也不再是白板了。