pomelo component

pomelo中组件是可重用的服务单位,一个组件实例提供若干种服务,比如说处理机组件载入后处理机代码后将会把客户端消息传递给请求处理机。

  • pomelo是“微内核+插件”的实现方式,由松耦合的组件构成,每个组件完成特定的任务。
  • pomelo的核心功能都是由组件完成的,整个pomelo框架可以看作是一个组件容器,完成组件的加载以及生命周期管理。

生命周期

  • 每个组件都要定义startafterStartstop等调用(hook钩子函数),供pomelo管理其生命周期。
  • 各个组件需要调用cb回调函数来持续后续步骤
  • 组件也可以传入一个参数给cb来表示当前组件启动失败,以此来使应用结束这个进程。
钩子函数 生命周期 描述
start(cb) 组件开始阶段 当组件启动时被调用
afterStart(cb) 组件声明阶段,当组件启动之后回调 在当前进程所有被注册组件启动之后调用,给组件进行协作兼初始化的机会。
stop(cb) 组件停止阶段,当组件停止时回调 当服务将要停止的时候调用

组件启动流程:

  1. pomelo先调用加载的每个组件的start(cb),当全部调用完毕后才会去调用其加载的每个组件的afterStart(cb),注意按顺序调用。
  2. 因为调用afterStart(cb)时,所有组件的start(cb)已经调用完毕,此时可添加些需全局就绪的操作。
  3. stop(cb)用来程序结束时对组件进行清理。可以做些清理作业,比如冲刷此周期中的数据到数据库。force参数若为true则表示所有组件都需要被立即停止。

例如:自定义组件

  1. 应用配置中加载自定义组件并传入参数
$ vim app.js
app.configure('production|development', 'master', function() {
  app.load(require('./app/components/Hello'), {interval: 5000});
});
  1. 创建自定义组件以及生命周期函数
$ 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将会把它作为一个工厂函数并让其返回一个组件实例。
  • 工厂函数接受两个参数appopts返回一个组件实例。

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.componentsPomelo按名或取,也可以通过Application.components来按名获取。

加载流程

Pomelo框架的核心是两个类PomeloApplicationApplication实例由Pomelo.createApp()创建,Pomelo实际上由一系列组件以及一个全局上下文Application实例组成。

在类图上所有组件都是抽象类Component的子类。每个组件都完成其相应的功能,不同的服务器将会加载不同的组件。

Pomelo应用程序执行的过程是对相应组件的生命周期的管理,实际的逻辑均由组件提供。

$ vim pomelo/lib/pomelo.js
$ vim game-server/app.js
  1. 导出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下的componentsfilters/handlerfitler/rpc文件夹下的所有js模块,并写入到pomelo。

其中pomelo.componenets是pomelo平台的所有组件,对于每个组件而言都有一个application应用实例,每个应用实例都会通过pomelo对象加载对应的components组件并实例化。

Pomelo.__defineGetter__(name, _load)

用于将所有组件以文件名作为name,文件加载得到的function函数作为value,保存到pomelo对象中。

  1. Application应用初始化流程
//创建应用 为客户端初始化应用
const app = pomelo.createApp();

当使用pomelo.createApp创建出Application应用对象app并初始化后,经过一系列app.setapp.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中读入服务器集群各个进程的typehostport配置,这里也可以通过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生命周期回调可以让开发人员在不同类型的服务器生命周期中进行详细操作,生命周期回调函数包括beforeStartupafterStartupbeforeShutdownafterShutdown

Application.start时会加载默认的两个组件mastermonitor,使用Application.load加载组件时会将组件存储到apploadcomponent中,不过需要注意的是这里的组件是组件实例化后的对象。

//启动应用
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()是会被调用。

  1. 组件加载运行
  • pomelo遍历components文件夹中各个js文件,requirepomelopomelo.componenets中。
  • Application.start开启后会先调用AppUtil.loadDefaultComponents
  • loadDefaultComponents中会根据Application.serverTypeApplication.load所需的components
  • Application.load中会将Pomelo的components放到自己的components
  • Application.start/stop/afterStop等方法会统一地执行各components中对应的start/stop/afterStart钩子函数

内建组件

Pomelo总体架构

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信息

处理流程

如果客户端请求的服务是由前端服务器提供

  1. 服务器组件会从connector组件的回调中获得相应的客户端请求或通知
  2. 然后使用自己的before filters对消息进行过滤
  3. 再次调用自己相应的Handler进行请求的逻辑处理
  4. 将响应通过回调的方式发送给connector进行处理
  5. 最后再调用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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容

  • 翻译:死月,校对:叶岬,原文连接 Pomelo(柚子)是一个可以让开发者便捷开发的游戏服务端框架。下面是其一些Po...
    機巧死月不會碼代碼阅读 19,151评论 4 16
  • 高并发平台架构 设计理念 1. 空间换时间 多级缓存,静态化前端页面缓存(HTTP Header中包含Expire...
    AkaTBS阅读 3,017评论 0 13
  • 游戏服务器往往需要处理大量任务,比如管理客户端的连接、维护游戏世界的状态、执行游戏的逻辑等。每一项任务所需的系统资...
    JunChow520阅读 516评论 0 1
  • 问题导读: 1.如何构建高并发电商平台架构 2.哈希、B树、倒排、bitmap的作用是什么? 3.作为软件工程师,...
    MaLiang阅读 5,115评论 1 70
  • 书名:《完全写作指南》清单名:从提笔就怕到什么都能写,你需要做到这10点001 明目的明确写作的目标很重要,虽然...
    精力满满阅读 218评论 0 5