pomelo解說

nodejs+pomelo+mysql實戰經驗分享

Pomelo

我的倉庫地址

https://github.com/NetEase

Overview

介紹

  • pomelo 是一個與以往單進程的遊戲框架不同,擁有高性能高可伸縮性分佈式多進程的遊戲服務器框架。Easy configure , Easy use!
  • 它包括基礎開發框架和一系列相關工具和庫
  • pomelo-rpc,pomelo-rpc-zeromq,pomelo-scheduler,pomelo-status-plugin,bearcat,pomelo-logger,seq-queue,pomelo-cli,pomelo-robot,pomelo-admin-web
  • 組成:框架,庫,工具,客戶端庫(unity,cocos)

選擇Pomelo理由

  • 架構的可伸縮性好,採用多進程單進程的運行架構擴展服務器非常方便,node.js的網絡io優勢提供了高可伸縮性,寫好的應用只需要簡單地修改一下配置就能輕鬆地伸縮擴充
  • 易用. pomelo基於輕量級的nodejs,其開發模型與web應用的開發類似,基於convention over configuration的理念, 幾乎零配置, api的設計也很精簡,很容易上手,開發快速
  • 框架的鬆耦合和可擴展性好. 遵循node.js微模塊的原則, 框架本身只有很少的代碼,所有component、庫、工具都可以用npm module的形式擴展進來,任何第三方都可以根據自己的需要開發自定義module,並把它整合到pomelo的框架中
  • 完整的demo和文檔. 不過要吐槽一句 坑很多 對於初次接觸的人來說 ,現在基本上也沒有人來維護這個框架了。而且和nodejs的版本兼容的不是太好。 Pomelo開發者羣裏面 經常有人問到: 你們的nodejs用的哪個版本,我的版本安裝pomelo失敗。

安裝[node版本兼容問題 經常安裝失敗]

  • 第一種方式

npm install pomelo -g

  • 第二種方式

git clone https://github.com/NetEase/pomelo.git

cd pomelo

npm install -g

項目結構

項目創建成功後的項目結構如下圖

啓動

pomelo start 啓動服務器

pomelo list 查看服務器狀態

pomelo stop 停止應用

術語解釋

gate服務器

  • 負責前端的負載均衡
  • 配置中 只有clientPort字段 沒有port字段
  • 客戶端首先向gate服務器發送請求,gate會給客戶端分配具體的connector服務器。具體的分配邏輯自定
  • “frontend”: true

connector服務器

  • 負責接收客戶端的連接請求,創建與客戶端的連接,維護客戶端的session信息
  • 接收客戶端對後端服務器的請求,按照用戶配置的路由策略,將請求路由給具體的後端服務器
  • 扮演一箇中間角色,負責接收請求,並將處理結果發送給客戶端
  • 配置中同時具有clientPort和port,clientPort用來監聽客戶端的連接,port端口用來給後端提供服務
  • “frontend”: true

應用邏輯服務器

  • gate服務器和connector服務器又都被稱作前端服務器,應用邏輯服務器是後端服務器
  • 後端服務器之間通過rpc進行交互
  • 後端服務器不會和客戶端有直接連接,只需監聽其提供服務的端口

master服務器

  • 加載配置文件,通過讀取配置文件,啓動所配置的服務器集羣,並對所有服務器進行管理

RPC調用

  • 分類: sys 系統rpc調用 和 用戶自定義的rpc 調用
  • sys rpc調用
    • 後端向前端請求session信息
    • 後端通過channel推送消息時對前端服務器發起的rpc調用
    • 前端服務器將用戶請求給後端服務器
  • 參數
    • session , args , callback
    • 第一個參數session是用來做路由計算的

route router

  • route用來標識一個具體服務或者客戶端接受服務端推送消息的位置; 例如"chat.chatHandler.send", chat就是服務器類型,chatHandler是chat服務器中定義的一個Handler,send則爲這個Handler中的一個handle方法
  • 可以粗略地認爲router就是根據用戶的session以及其請求內容,做一些運算後,將其映射到一個具體的應用服務器id。可以通過application的route調用給某一類型的服務器配置其router
  • 如果不配置的話,pomelo框架會使用一個默認的router。pomelo默認的路由函數是使用session裏面的uid字段,計算uid字段的crc32校驗碼,然後用這個校驗碼作爲key,跟同類應用服務器數目取餘,得到要路由到的服務器編號
  • 注意這裏有一個陷阱,就是如果session沒有綁定uid的話,此時uid字段爲undefined,可能會造成所有的請求都路由到同一臺服務器。所以在實際開發中還是需要自己來配置router

session sessionService

  • session 是由sessionService創建
  • BackendSessionService創建 BackendSession
  • SessionService,FrontendSession,Session
gate session [FrontendSession]

gateSession

connector session [FrontendSession]

connectorSession

chat session [BackendSession]

1

chat

channel

  • 可以看作是一個玩家ID的容器,主要用於需要廣播推送消息的場景,例如聊天。 個人私聊,世界頻道,工會頻道,都是根據channel去篩選, 要給具體的channel裏的玩家推送消息
  • channel 在應用服務器之前不是共享的。服務器A上創建的channel,只有服務器 A才能給它推送消息

filter

  • 分類:before after
  • before 對請求進行過濾 做一些前置處理 例如檢測玩家session信息,以及是否登陸,以及黑名單白名單等
  • after 進行請求後置處理 特殊處理 發送郵件等

handler

  • 實現具體業務邏輯 在before filter和after filter之間,next回調函數串通
  • msg, session, next 參數,通過next 進入到after filter處理流程

error handler

  • 處理全局異常情況的地方 可發郵件通知維護

component

  • pomelo 核心是由一系列鬆耦合的component組成
  • 可自己定製component , 在服務器啓動時加載,完成一定的功能
  • start, afterStart, stop
  • 導出工廠函數,而不是一個對象,app會將自己作爲上下文信息以及後面的opts作爲參數傳遞給這個函數,並返回一個component 對象
  • app.load(componentName);

增加admin module

  • 線上admin 工具
  • 三主體:master,monitor,client
  • 註冊admin module; app.registerAdmin(moduleName,{app:app});
  • moduleId , monitorHandler,masterHandler,clientHandler
register admin

resigter admin

process admin module

adminModule

  • 線上admin , client 向master服務器發送request,請求中帶有moduleId和對應的回調參數
module request

admin request

熱更新

  • hot文件夾下
#例子

var Handler = function(app) {
    this.app = app;
    this.channelService = app.get('channelService');
};

var handler = Handler.prototype;

handler.send = function(msg,session,next){
    next();
};

module.exports = {
    id: "chatHandler",
    func: Handler
};

bearcat

  • context.json 配置
{
	"name": "arpg_name",
	"beans": [],
	"scan": "app"
}
  • createApp
var contextPath = require.resolve('./context.json');
bearcat.createApp([contextPath]);

bearcat.start(function() {
    Configure(); //app configure
    // start app
    app.start();
});

pomelo服務器啓動過程

啓動過程

pomelo start

pomelo start 啓動master服務器, master服務器調用進程創建子進程,即應用服務器

process

node <BasePath>/app.js env=development id=connector-server-1

Application 初始化

initcreate app 創建應用init app 初始化應用的配置信息setenv 設置環境變了

processArgs 創建應用服務器的處理過程 , 就是上面的node …/app.js …

應用創建成功之後, 還會對app進行一些設置,比如服務器的路由設置,以及上下文變量的設置

Master服務器啓動

master

  • app.start() 後,加載默認的組件,對於master來說加載的組件爲master和monitor組件
  • Master組件的創建過程會創建MasterConsole, MasterConsole會創建MasterAgent, MasterAgent會創建監聽Socket, 用來監聽應用服務器的監控和管理請求
  • 加載完組件後,會啓動所有的組件。Master有自己的組件,還會啓動用戶自定義的Module, 在app.start()之前調用app.registerAdmin 掛在app上
  • 所有的Module掛到app後,Master組件會啓動MasterConsoleService.啓動MasterConsoleService時,MasterConsoleService會從app處拿到所有掛在其上面的Module,然後將Module註冊到自己的Module倉庫中,這一步實際上就是Module放到一個以ModuleId做鍵的Map中,以使得後來有請求時,可以直接進行查詢回調
  • 然後開啓MasterAgent監聽,此時,Master組件就可以接受監控管理請求了
  • 下一步,啓動所有的Module.
  • 啓動所有的應用服務器。Master組件完成了所有的其自身的Module的初始化和開啓任務後,Master會委託Starter完成整個服務器羣的啓動。 Master服務器負責管理所有應用服務器

應用服務器啓動

  • 參數啓動
  • 配置 app.configure(‘production|development’, ‘chat’,function(){…}) 配置數據庫以及其他配置信息
  • 細節的東西比較多 不一一講了

服務器關閉

  • pomelo stop
  • 線上服務器可通過自定義Module工具停服

簡單介紹完畢

到此pomelo的簡單介紹算是結束了 要說的東西實在不是一句兩句能說完的 大家可以去Github上去看文檔

實戰 [以下是我之前的項目部分代碼]

app.js

var pomelo = require('pomelo');
var zmq = require('pomelo-rpc-zeromq');
var bearcat = require('bearcat');
var mysql = require('mysql');
var path = require('path');

//mysql config
var mysqlConfig = require('./config/mysql.js');



var app = pomelo.createApp();
app.set('name','appname');

//全局變量設置 有個地方會用到
global.app = pomelo.app;

//下面設置的變量 在自定義pomelo-cli工具會用到 如果不掛載在app上 其他地方引用不到
global.app.bearcat = bearcat;
global.app.async = async;
global.app.mysqlModule = mysql;
global.app.mysqlConfig = mysqlConfig;


//最重要的配置服務器

var Configure = function(){
	app.configure('production|development', function () {
		//todo 坑 : 加上之後服務器啓動之後 一會兒就會宕機 尚未找到原因
        //app.enable('systemMonitor'); 

        app['myLoader'] = myLoader;
        app['redis'] = app.myLoader.load(__dirname + '/lib/redis.js');
        //設置路由函數router
        var routeUtil = app.myLoader.load(__dirname + '/app/util/route.js');
        app.route('chat', routeUtil.chat);
        app.route('gate', routeUtil.gate);
        app.route('connector', routeUtil.connector);
        app.route('data', routeUtil.data);
       

        //全局路由過濾函數
        var globalFilter = require('./app/servers/filter/globalFilter.js');
        app.globalFilter(globalFilter()); //處理未登錄玩家 黑名單 白名單等

        //全局錯誤處理
        var GlobalHandler = require('./app/globalHandler/globalErrorHandler.js');
        var globalErrorHandler = new GlobalHandler();
        app.set('globalErrorHandler',globalErrorHandler.globalHandler);
        app.set('errorHandler',globalErrorHandler.globalHandler);

        app.connDispatch = {};//鏈接調度
        app.roomDispatch = {};//房間調度

		//rpc client 
        app.set('proxyConfig', {
            rpcClient: zmq.client
        });
		
		//rpc server
        app.set('remoteConfig', {
            rpcServer: zmq.server
        });

    });
    
    
    //gate服務器配置 
    app.configure('production|development', 'gate', function () {
        app.set('connectorConfig',
            {
                connector: pomelo.connectors.hybridconnector,
                heartbeat: 8, //心跳
                useDict: true,
                useProtobuf: true,
                disconnectOnTimeout: true 超時斷開連接
            });
    });
    
    //chat 服務器配置
    app.configure('production|development', 'chat', function () {

    });
    
    //connector服務器配置
    app.configure('production|development', 'connector', function () {
        app.set('connectorConfig',
            {
                connector: pomelo.connectors.hybridconnector,
                heartbeat: 8,
                useDict: true,
                useProtobuf: true,
                disconnectOnTimeout : true
            });
    });
    
    //data 應用服務器配置
    app.configure('production|development', 'data', function () {
	    //配置mysql
        var mysqlConf = app.myLoader.load(__dirname + '/config/mysql.js');
        app['mysql'] = app.myLoader.load(__dirname + '/lib/mysql.js');
        app['mysql'].config(mysqlConf);
        global.app.mysql = app.mysql;
    });
    
    //master
    app.configure('production|development', 'master', function() {
        /*
        這裏可以寫一些初始化服務器的功能 比如說刪除一些以前的無用信息
        服務器機器人的初始化
        清理redis緩存信息
        等等
        */
        //清理停服狀態  每次停服都是先設置狀態 再主動踢出玩家 
        global.app.redis.get('gameServerStatus',function(){});           
    });
    
}

//use bearcat start app
var contextPath = require.resolve('./context.json');
bearcat.createApp([contextPath]);

bearcat.start(function() {
    Configure();
    // start app
    app.start();
});

//配置日誌信息格式
var logger = require('pomelo-logger').configure(path.join(app.getBase(),'/lib/log4js.json'),{base : app.getBase()});


//異常錯誤捕捉處理
process.on('uncaughtException',function(err){
	//錯誤日報 發送郵件 nodemailer庫
    var mailModel = bearcat.getBean('sendMailModel');
    mailModel.sendMail(err.stack);
});


//正式環境 去掉connsole log輸出
var env = app.get('env');
if(env != 'development'){
   console.log = function(){};
   console.info = function(){};
   console.warn = function(){};
}

配置文件

package.json 主要是版本

{
  "name": "appname",
  "version": "0.0.1",
  "private": false,
  "dependencies": {
    "pomelo": "1.1.3",
    "request": "2.34.0",
    "zmq": "2.6.0",
    "crc": "0.2.1",
    "bearcat": "^0.2.37",
    "pomelo-logger": "0.1.2",
    "nodemailer": "1.3.0",
    "nodemailer-smtp-transport": "0.1.13",
    "async": "0.2.10",
    "mysql": "^2.9.0", [版本問題坑過一次,老是斷開連接]
    "pomelo-rpc-zeromq": "0.0.8",
    "redis": "0.10.0",
    "redis-jsonify": "0.0.4",
    "underscore": "1.7.0",
    "ws": "*",
    "socket.io": "0.9.17",
    "pomelo-protocol": "*",
    "pomelo-protobuf": "*",
    "fs": "0.0.2",
    "express": "^4.13.3",
    "body-parser": "^1.14.1"
  }
}

context.json

# bearcat配置
{
	"name": "appname",
	"beans": [],
	"scan": "app" //指的是game-server/app文件夾下的*.js
}

mysql.js

var mysql = require('mysql');
var pool;//mysql 連接池

var db = function(){
    return {config : function(conf){
       pool =  mysql.createPool({
            host     : conf.host,
            user     : conf.user,
            password : conf.password,
            database : conf.database,
            acquireTimeout : conf.acquireTimeout
        });

        pool.getConnection(function(err, connection) {
            if(err){
                console.log("mysql初始化失敗!");
            }else{
                console.log("mysql初始化成功!");
                connection.release();
            }
        });
    }};
}

//
exports.mysql = function(){
    return pool;
};

router函數設置

var serversConfig = require('../../config/servers.json');

exp.chat = function (route, msg, app, cb) {
    var chat = app.getServersByType('chat')[0];
    if (!chat) {
        console.log('failed to route to chat.');
        return;
    }
    cb(null, chat.id);
};

exp.connector = function (frontendId, msg, app, cb) {
    if (!frontendId) {
        console.log('failed to route to connector.');
        return;
    }
    cb(null, frontendId);
};

//gate服務器
exp.gate = function (serverId, msg, app, cb) {
    var gate = app.getServersByType('gate')[0];
    if (!gate) {
        console.log('failed to route to gate.');
        return;
    }
    cb(null, gate.id);
}

/*
data 應用服務器 應用服務器的設置 可能會比較複雜一些,因爲應用服務器會比gate,connector服務器要多
第一個參數session , 在我們的應用裏有可能是session, characterId[string] 
*/
exp.data = function (session, msg, app, cb) {
    var env = app.env;
    var envConfig = serversConfig[env];
    var servers = envConfig['data'];
    var data,index ;
    var charId;

    if(!!session && typeof(session) == 'object'){
        if(typeof(session.get) == 'function'){
            charId  = session.get('character_id');
        }

        if(!charId){//沒有角色Id的情況 : 沒有登錄和同步
            //index = parseInt(Math.random() * servers.length);
            index = servers.length - 1;
        } else {
            index = parseInt(charId) % servers.length ;
        }
    }else if(session && (typeof(session) == 'string' || typeof(session) == 'number')){
        index = session % servers.length ;
    }else{
        index = parseInt(Math.random() * servers.length);
    }

    data = servers[index];

    cb(null, data.id);
}

glboalErrorHandler

//錯誤碼對照表
var errCode = require('../../lib/errCode');
如下
"chat.chatHandler.send":{
	"error_code":1001 //策劃填寫錯誤提示
}

var GlobalHandler = function(app){
    this.app = app;
}
var br = require('bearcat');

GlobalHandler.prototype.globalHandler = function(err, msg, resp, session, next){
    var route = msg.route || msg.__route__ ;
    var charId = session.get('character_id');//登陸的時候 會把角色ID放到session中
    var mailModel = br.getBean('sendMailModel');
    var backendSessionService = this.app['backendSessionService'];

    if(err){
        if('no_redis_char' == err || 'redis_err' == err){
            backendSessionService.kickBySid(session.frontendId,session.id,function(err){
                if(err){
                    mailModel.sendMail(new Error('======>>>>kick char err').stack);
                }
            });
        }else if('update_redis_char' == err){
            //更新緩存錯誤
            backendSessionService.kickBySid(session.frontendId,session.id,function(err){
                if(!err){
                    global.app.rpc.redis.delete('char_' + charId ,function(err){
                        if(err){
                            mailModel.sendMail(new Error('redis del charInfo err').stack);
                        }
                    });
                }
            });
        }else if(!!errCode[route] && !!errCode[route][err]){//錯誤碼中有對應的錯誤提示
            next(null,{status : errCode[route][err]});
        }else if(!!err['condition_lack']){
            next(null,{ status : 10000 ,conditionId : err['condition_lack'].conditionId });
        }else if('redis_err' == err) {
            mailModel.sendMail(err.stack);
        }else if(!!err.status){
            next(null,err);
        }else {
            console.log('此錯誤沒有對應狀態碼!!!!!!!!',err);
            next(null,{status : 500});
        }
    }else{
        next(null,err);
    }
}

module.exports = GlobalHandler;

gate


var dispatcher = require('../../../util/dispatcher');
var pomelo = require('pomelo');
var bearcat = require('bearcat');
var async = require('async');
var util = bearcat.getBean('util');


var Handler = function() {
    this.$id = 'gateHandler';
    this.app = pomelo.app;
};

var handler = Handler.prototype;


handler.queryEntry = function(msg,session,next){
	var self = this;
	var connectors = self.app.getServersByType('connector');
    if(!connectors || connectors.length === 0) {
        next(null, {
            status: 500
        });
        return;
    }
    
    async.waterfall([
        function(cb){
        	//查看服務器是否處於停服狀態 狀態存儲在redis緩存中 設置master時,delete掉
            global.app.redis.get('gameServerStatus',function(err,data) {
                if (!err && !data) {
                    next(null, {status: 430});
                    return;
                }
                cb(err);
            });
        },
        function(cb){
            global.app.rpc.chat.chatRemote.getOnlinePlayersNumber(null,function(err,data){
                console.log('服務器同時在線人數==',data,serverLimit);
                if(!err){
                	 //查看服務器在線人數是否超過限制
                    if(data >= serverLimit){
                        next(null,{ status : 888});
                        return;
                    }
                } else {
                    next(null,{ status : 500});
                    return;
                }
                cb();
            });
        }
    ], function (err) {
    	//分配一個connector服務器 ;算法是Math.random()*connectorServers.length
        var res = dispatcher.dispatch(session.id,connectors);
        next(null, {
            status: 0,
            host: res.host,
            port: res.clientPort
        });
    });
}

實戰

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章