pomelo中组件是可重用的服务单位,一个组件实例提供若干种服务,比如说处理机组件载入后处理机代码后将会把客户端消息传递给请求处理机。
- pomelo是“微内核+插件”的实现方式,由松耦合的组件构成,每个组件完成特定的任务。
- pomelo的核心功能都是由组件完成的,整个pomelo框架可以看作是一个组件容器,完成组件的加载以及生命周期管理。
生命周期
- 每个组件都要定义
start
、afterStart
、stop
等调用(hook钩子函数),供pomelo管理其生命周期。 - 各个组件需要调用
cb
回调函数来持续后续步骤 - 组件也可以传入一个参数给
cb
来表示当前组件启动失败,以此来使应用结束这个进程。
钩子函数 | 生命周期 | 描述 |
---|---|---|
start(cb) | 组件开始阶段 | 当组件启动时被调用 |
afterStart(cb) | 组件声明阶段,当组件启动之后回调 | 在当前进程所有被注册组件启动之后调用,给组件进行协作兼初始化的机会。 |
stop(cb) | 组件停止阶段,当组件停止时回调 | 当服务将要停止的时候调用 |
组件启动流程:
- pomelo先调用加载的每个组件的
start(cb)
,当全部调用完毕后才会去调用其加载的每个组件的afterStart(cb)
,注意按顺序调用。 - 因为调用
afterStart(cb)
时,所有组件的start(cb)
已经调用完毕,此时可添加些需全局就绪的操作。 -
stop(cb)
用来程序结束时对组件进行清理。可以做些清理作业,比如冲刷此周期中的数据到数据库。force参数若为true则表示所有组件都需要被立即停止。
例如:自定义组件
- 应用配置中加载自定义组件并传入参数
$ vim app.js
app.configure('production|development', 'master', function() {
app.load(require('./app/components/Hello'), {interval: 5000});
});
- 创建自定义组件以及生命周期函数
$ vim app/components/Hello.js
var DEFAULT_INTERVAL = 3000
//组件类
var Component = function(app, opts) {
this.app = app
this.interval = opts.interval | DEFAULT_INTERVAL
this.timerId = null
};
var pro = Component.prototype
//组件名称
pro.name = '__Hello__'
//组件生命周期钩子函数
pro.start = function(cb) {
console.log('Component Start')
var self = this
this.timerId = setInterval(function() {
console.log(self.app.getServerId() + ": Component!");
}, this.interval);
process.nextTick(cb)
}
pro.afterStart = function (cb) {
console.log('Component afterStart');
process.nextTick(cb)
}
pro.stop = function(force, cb) {
cosole.log('Component stop')
clearInterval(this.timerId)
process.nextTick(cb)
}
// 对外导出工厂函数
module.exports = function(app, opts) {
return new Component(app, opts)
}
定义组件
定义组件时一般会向外导出一个工厂函数,注意不是对象。当app
加载自定义组件时,若组件存在工厂函数,app
会将自身作为上下文信息以及其后的opts
作为参数传递给工厂函数,并使用工厂函数的返回值作为component
组件对象。
// 对外导出工厂函数
module.exports = function(app, opts) {
return new Component(app, opts)
}
代码中组件是一个简单的类,实现了必须的生命周期接口,应用需要触发生命周期每个阶段每个组件所需的回调函数。
注册组件
只有组件实例被注册进入进程上下文,应用才能获得该组件实例提供的能力。
使用app.load()
将自定义组件注册到进程上下文
-
app.load()
用于加载组件,返回调用链app
应用实例。
app.load([name], comp, [opts]);
参数 | 描述 |
---|---|
name | 组件名称,可选。 |
comp | 组件实例或组件工厂函数 |
opts | 将被传入到组件工厂函数的第2个参数,可选项。 |
- 具名组件实例载入后可通过
app.components.name
进行访问 - 若
comp
是一个函数,那么app
将会把它作为一个工厂函数并让其返回一个组件实例。 - 工厂函数接受两个参数
app
和opts
返回一个组件实例。
app.load()
底层函数声明
Application.load = function(name, component, opts)
参数 | 必填 | 描述 |
---|---|---|
name | 可选 | 组件名称 |
component | 必填 | 组件工厂函数创建的实例 |
opts | 可选 | 组件工厂函数构造器参数 |
组件交互
组件实例可以通过应用和其它组件进行交互合作。
比如:一个连接组件接收到一个客户端请求,然后将其发送给应用,处理机组件稍后就有可能从应用中获取这条消息。
基于组件的系统应用实际上是进程的骨干,它载入了所有的注册组件,鞭策它们穿越了整个生命周期。单应用不能涉及到各组件的细节。所有定制服务端进程的作业仅仅只是挑选必须的组件构成一个应用。所以应用是非常干净和灵活的,而组件的可重用性非常高。此外,组件系统最终将所有服务端类型装入一个统一的进程中。
命名规则
每个组件的名字都在自己的name
属性中,通常为js
文件名前后加双下划线。
例如:connector.js
的组件名称为__connector__
var pro = Component.prototype;
pro.name = '__connector__';
组件获取
内置组件位于components
文件夹下,组件可通过Pomelo.components
或Pomelo
按名或取,也可以通过Application.components
来按名获取。
加载流程
Pomelo框架的核心是两个类Pomelo
和Application
, Application
实例由Pomelo.createApp()
创建,Pomelo实际上由一系列组件以及一个全局上下文Application
实例组成。
在类图上所有组件都是抽象类Component
的子类。每个组件都完成其相应的功能,不同的服务器将会加载不同的组件。
Pomelo应用程序执行的过程是对相应组件的生命周期的管理,实际的逻辑均由组件提供。
$ vim pomelo/lib/pomelo.js
$ vim game-server/app.js
- 导出pomelo对象
const pomelo = require('pomelo');
- pomelo.js文件的作用是初始化所有配置信息并自动加载所有组件
- pomelo中的各个功能模块都是以
component
组件的形式进行封装
/**
* 自定加载已绑定的组件
*/
fs.readdirSync(__dirname + '/components').forEach(function (filename) {
//文件类型验证
if (!/\.js$/.test(filename)) {
return;
}
//加载文件
var name = path.basename(filename, '.js');
var _load = load.bind(null, './components/', name);
//将文件名作为name,将加载得到的function函数作为value,保存到pomelo对象中。
Pomelo.components.__defineGetter__(name, _load);
Pomelo.__defineGetter__(name, _load);
});
//加载handler
fs.readdirSync(__dirname + '/filters/handler').forEach(function (filename) {
if (!/\.js$/.test(filename)) {
return;
}
var name = path.basename(filename, '.js');
var _load = load.bind(null, './filters/handler/', name);
Pomelo.filters.__defineGetter__(name, _load);
Pomelo.__defineGetter__(name, _load);
});
//加载rpc
fs.readdirSync(__dirname + '/filters/rpc').forEach(function (filename) {
if (!/\.js$/.test(filename)) {
return;
}
var name = path.basename(filename, '.js');
var _load = load.bind(null, './filters/rpc/', name);
Pomelo.rpcFilters.__defineGetter__(name, _load);
});
function load(path, name) {
if (name) {
return require(path + name);
}
return require(path);
}
当使用require("pomelo")
导出pomelo对象时,会发现在pomelo中会直接使用fs.readdirSync
读取文件载入的过程。
由于是执行函数所以在require
时会直接执行,此时会载入node_modules/pomelo/lib
下的components
、filters/handler
、fitler/rpc
文件夹下的所有js模块,并写入到pomelo。
其中pomelo.componenets
是pomelo平台的所有组件,对于每个组件而言都有一个application应用实例,每个应用实例都会通过pomelo对象加载对应的components组件并实例化。
Pomelo.__defineGetter__(name, _load)
用于将所有组件以文件名作为name,文件加载得到的function函数作为value,保存到pomelo对象中。
- Application应用初始化流程
//创建应用 为客户端初始化应用
const app = pomelo.createApp();
当使用pomelo.createApp
创建出Application
应用对象app
并初始化后,经过一系列app.set
和app.configure
参数配置后app.start()
就开启了项目。
$ pomelo/lib/pomelo.js
/**
* 创建一个pomelo应用实例
*
* @return {Application}
* @memberOf Pomelo
* @api public
*/
Pomelo.createApp = function (opts) {
var app = application;
//应用初始化
app.init(opts);
self.app = app;
return app;
};
pomelo.createApp
会调用Application.init
对应用进行初始化,初始化过程中会调用AppUtil.defaultConfiguration
来读入默认配置。
例如:从master.json
中读取master
服务器配置(Application.master),从servers.json
中读入服务器集群各个进程的type
、host
和port
配置,这里也可以通过Application.get("__serverMap__")
进行获取。
$ vim pomelo/lib/application.js
/**初始化服务器 设置默认的配置 */
Application.init = function(opts) {
opts = opts || {};
this.loaded = []; // 已加载的组件列表
this.components = {}; // name -> component map
this.settings = {}; // collection keep set/get
var base = opts.base || path.dirname(require.main.filename);
this.set(Constants.RESERVED.BASE, base, true);
this.event = new EventEmitter(); // event object to sub/pub events
// 当前服务器信息
this.serverId = null; // 当前服务器id
this.serverType = null; // 当前服务器类型
this.curServer = null; // 当前服务器信息
this.startTime = null; // 当前服务器开启事件
// 全局服务器信息
this.master = null; // 主服务器信息
this.servers = {}; // 当前全局服务器信息映射集合,格式为id -> info
this.serverTypeMaps = {}; // 当前全局服务器类型映射集合,格式为type -> [info]
this.serverTypes = []; // 当前全局服务器类型列表
this.lifecycleCbs = {}; // 当前服务器自定生命周期回调函数
this.clusterSeq = {}; // 集群ID序列
//读取默认配置
appUtil.defaultConfiguration(this);
//设置当前服务器状态
this.state = STATE_INITED;
//日志记录
logger.info('application inited: %j', this.getServerId());
};
lifecycleCbs
生命周期回调可以让开发人员在不同类型的服务器生命周期中进行详细操作,生命周期回调函数包括beforeStartup
、afterStartup
、beforeShutdown
、afterShutdown
。
当Application.start
时会加载默认的两个组件master
和monitor
,使用Application.load
加载组件时会将组件存储到app
的load
和component
中,不过需要注意的是这里的组件是组件实例化后的对象。
//启动应用
app.start();
$ vim pomelo/lib/application.js
/**
* 开启应用,将会加载默认组件并开启已加载的组件。
* @param {Function} cb callback function
* @memberOf Application
*/
Application.start = function(cb) {
this.startTime = Date.now();
//当前状态判断
if(this.state > STATE_INITED) {
utils.invokeCallback(cb, new Error('application has already start.'));
return;
}
//根据服务器类型加载默认组件
var self = this;
appUtil.startByType(self, function() {
//加载默认的组件:master和monitor
appUtil.loadDefaultComponents(self);
var startUp = function() {
appUtil.optComponents(self.loaded, Constants.RESERVED.START, function(err) {
self.state = STATE_START;
if(err) {
utils.invokeCallback(cb, err);
} else {
logger.info('%j enter after start...', self.getServerId());
self.afterStart(cb);
}
});
};
var beforeFun = self.lifecycleCbs[Constants.LIFECYCLE.BEFORE_STARTUP];
if(!!beforeFun) {
beforeFun.call(null, self, startUp);
} else {
startUp();
}
});
};
start
函数是Application
的启动函数,由pomelo
继承,当在app.js
中使用app.start()
是会被调用。
- 组件加载运行
-
pomelo
遍历components
文件夹中各个js
文件,require
到pomelo
和pomelo.componenets
中。 -
Application.start
开启后会先调用AppUtil.loadDefaultComponents
-
loadDefaultComponents
中会根据Application.serverType
来Application.load
所需的components
。 -
Application.load
中会将Pomelo的components
放到自己的components
中 -
Application.start/stop/afterStop
等方法会统一地执行各components
中对应的start/stop/afterStart
钩子函数
内建组件
Pomelo内建组件适用于不同的服务器,主要包括:master组件、monitor组件、connector组件、session组件、connection组件、server组件、pushScheduler组件、proxy组件、remote组件、dictionary组件、protobuf组件、channel组件、backendSession组件。
不同类型的服务器启动的组件
服务器 | 组件 | 描述 |
---|---|---|
所有服务器 | monitor | - |
主服务器 | - | 启动所有服务器及相应的监控或统计等服务 |
后端服务器 | server | 服务器对外服务接口,用于路由解释、转发、请求处理等。 |
后端服务器 | proxy | RPC客户端代理,服务器账号策略由app配置,默认路由算法。 |
后端服务器 | channel | 为广播消息服务 |
前端服务器 | connection | 统计使用 |
前端服务器 | connector | 客户端与服务器的直接连接 |
前端服务器 | session | 会话管理 |
RPC服务器 | remote | RPC服务器组件 |
RPC服务器 | localSession | 由connector发送消息时copy过来的session数据 |
组件职责
组件 | 职责 |
---|---|
master | 负责启动master服务器 |
monitor | 负责启动各个服务器的monitor服务,该服务负责收集服务器的信息并定期向master进行消息推送,保持master与各个服务器的心跳连接。 |
proxy | 负责生成服务器rpc客户端,由于系统中存在多个服务器进程,不同服务器进程之间相互通信需要通过RPC调用,master服务器除外。 |
remote | 负责加载后端服务器的服务并生成服务器RPC服务端 |
server | 负责启动所有服务器的用户请求处理服务 |
connector | 负责启动前端服务器的session服务和接收用户请求,可加载connector组件时指定自定义的实现,以选择合适的连接模式或数据通信协议。 |
sync | 负责启动数据同步模块并对外提供数据同步功能 |
connection | 负责启动用户连接信息的统计服务 |
channel | 负责启动channelService服务,channelService服务提供channel相关功能,包括创建channel,并通过channel进行消息推送等。 |
session | 负责启动sessionService服务,该服务主要用来对前端服务器的用户session进行统一管理。 |
localSession | 负责启动localSession服务,localSession服务负责维护服务器本地session并与前端服务器进行交互。 |
dictionary | 负责生成handler的字典 |
protobuf | 负责解析服务端和客户端的protobuffer的定义,从而对客户端和服务端的通信内容进行压缩。 |
- 服务器组件是一个功能复杂的组件,它被除
master
以外的服务器加载 - 服务器组件会加载并维护自身的
Filter
信息和Handler
信息
处理流程
如果客户端请求的服务是由前端服务器提供
- 服务器组件会从
connector
组件的回调中获得相应的客户端请求或通知 - 然后使用自己的
before filters
对消息进行过滤 - 再次调用自己相应的
Handler
进行请求的逻辑处理 - 将响应通过回调的方式发送给
connector
进行处理 - 最后再调用
after filter
进行清理处理
如果客户端请求的服务是后端服务器提供的服务
- 此时会出现
sys rpc
调用 - 前端服务器自己处理的情况具体调用更为
doHandler
,而发起rpc
调用的情况则为doForward
。
这两种处理流程不同点在于
- 对于自身的请求,调用自己的
filter-handler
链进行处理。 - 对于不是前端服务器自己的服务,则是发起一个
sys rpc
,然后将rpc
调用的结果作为响应,发送给connector
进行处理。
对于后端服务器来说其客户端请求不是直接来源于真实的客户端,而是来源于前端服务器对其发起的sys rpc
调用,这个rpc
调用的实现是pomelo
内建的msgReote
,而msgRemote
实现会将来自前端服务器的sys rpc
调用请求派发给后端服务器的server组件,然后后端服务器会启用filter-handler
链对其进行处理,最后通过rpc调用的返回将具体的响应返回给前端服务器。
前端服务器将客户端请求向后端服务器分派时,由于同类型的后端服务器往往有很多,因此需要一个路由策略,一般情况下用户通过Application.route
调用为后端服务器配置的路由。
app.components.proxy
app.components.__proxy__
是RPC客户端组件,源码位于pomelo/lib/common/components/proxy.js
。
- proxy 负责生成服务器rpc客户端,由于系统中存在多个服务器进程,不同服务器进程之间相互通信需要通过RPC调用,master服务器除外。
app.components.__proxy__.client._station