[egret+pomelo]实时游戏杂记(1)

[egret+pomelo]学习笔记(1)

[egret+pomelo]学习笔记(2)

[egret+pomelo]学习笔记(3)

资料

egret

pomelo

pomelo捡宝项目

准备工作

1.下载并搭建pomelo项目

2.下载pomelo捡宝项目(github上下载的,最好是看一遍git上的教程,再进行搭建会比较顺利)

3.下载的捡宝项目[Treasures] 中有简略的项目教程,可以帮助我们快速搭建和熟悉捡宝项目。

开始创建Egret项目:

 因个人比较熟悉egret引擎,在论坛中找到 egret  pomelo的第三方库

1.客户端代码:

使用egret wing 创建游戏项目,在项目src目录下,创建network文件夹,在文件夹下新建PomeloSocket类用来链接Pomelo服务端

  1 module network {
  2     /**
  3      * 链接pomelo服务端
  4      */
  5     export class PomeloSocket {
  6         public constructor() {
  7         }
  8 
  9         private pomelo: Pomelo;
 10         /**
 11          * 当前正在操作的是服务端
 12          */
 13         private currServer: network.PomeloService;
 14         /**
 15          * 服务端状态 是否开启
 16          */
 17         private running: boolean = false;
 18 
 19         init() {
 20             if (this.pomelo == null) {
 21                 this.pomelo = new Pomelo();
 22 
 23                 this.pomelo.on('server_push_message', (msg) => {
 24                     var route = msg["route"];
 25                     //根据服务端返回派发事件
 26                     {
 27                         switch (route) {
 28                             case "addEntities":
 29                                 Global.dispatchEvent(events.PomeloServerEvents.ADDENTITIES, msg);
 30                                 break;
 31                             case "rankUpdate":
 32                                 Global.dispatchEvent(events.PomeloServerEvents.RANKUPDATE, msg);
 33                                 break;
 34                             case "onUserLeave":
 35                                 Global.dispatchEvent(events.PomeloServerEvents.USERLEAVE, msg);
 36                                 break;
 37                             case "removeEntities":
 38                                 Global.dispatchEvent(events.PomeloServerEvents.REMOVEENTITIES, msg);
 39                                 break;
 40                             case "onMove":
 41                                 Global.dispatchEvent(events.PomeloServerEvents.ENTITYMOVE, msg);
 42                                 break;
 43                             case "onChangeStage":
 44                                 Global.dispatchEvent(events.PomeloServerEvents.STAGECHANGE, msg);
 45                                 break;
 46                             default:
 47                                 trace("收到新的需要处理的事件~~~~~~~~~~~~~~待处理信息为:");
 48                                 trace(msg);
 49                                 break;
 50                         }
 51                     }
 52                 });
 53 
 54                 this.pomelo.on('onKick', (msg) => {
 55                     trace("onKick");
 56                 });
 57 
 58                 this.pomelo.on('heartbeat_timeout', () => {
 59                     trace("heartbeat_timeout");
 60                 });
 61 
 62                 this.pomelo.on('close', (e: CloseEvent) => {
 63                     trace(e.currentTarget["url"] + "的链接被断开");
 64                 });
 65             }
 66         }
 67 
 68         /**
 69          * 打开服务端 
 70          * @param serverType:服务端类型
 71          * @param host:ip
 72          * @param port:端口
 73          * @param callback:回调函数
 74          * @param log:是否启用日志
 75          */
 76         open(serverType: network.PomeloService, host: string, port: number, callback?: Function, log: boolean = true) {
 77             this.pomelo.init({ host: host, port: port, log: log }, false, (succeedRes) => {
 78                 this.currServer = serverType;
 79                 this.running = true;
 80                 switch (serverType) {
 81                     case network.PomeloService.GATE:
 82                         Global.dispatchEvent(events.PomeloServerEvents.CONNECTION_GATE_SUCCEED);
 83                         break;
 84                     case network.PomeloService.CONNECTION:
 85                         Global.dispatchEvent(events.PomeloServerEvents.CONNECTION_CONNECT_SUCCEED);
 86                         break;
 87                     default:
 88                         trace("========================试图打开程序中未知服务器,请求被拒绝=========================================");
 89                         break;
 90                 }
 91             }, (errRES) => {
 92                 switch (serverType) {
 93                     case network.PomeloService.GATE:
 94                         Global.dispatchEvent(events.PomeloServerEvents.CONNECTION_GATE_ERROR);
 95                         break;
 96                     case network.PomeloService.CONNECTION:
 97                         Global.dispatchEvent(events.PomeloServerEvents.CONNECTION_CONNECT_ERROR);
 98                         break;
 99                     default:
100                         trace("========================试图打开程序中未知服务器,请求被拒绝=========================================");
101                         break;
102                 }
103             }, (closeRes) => {
104                 trace("一个服务端关闭完成。");
105             }, null);
106         }
107 
108         /**
109          * 发起请求
110          * @param route: 路由 (服务端处理函数)
111          * @param msg:内容
112          * @param callback:回调函数
113          * @param thisArg:参数
114          */
115         request(route: string, msg: any, callback: Function, thisArg?: any): void {
116             this.pomelo.request(route, msg, (response) => {
117                 callback.call(thisArg, response);
118             });
119         }
120 
121         /**
122          * 通知
123          */
124         notify(route: string, msg: any): void {
125             this.pomelo.notify(route, msg);
126         }
127 
128         /**
129          * 关闭当前服务
130          */
131         disconnect() {
132             this.pomelo.disconnect();
133             this.running = false;
134             Global.dispatchEvent(events.PomeloServerEvents.DISCONNECT_SUCCEED, { currServer: this.currServer });
135         }
136 
137         /**
138          * 获取当前的服务端
139          */
140         getCurrServer(): PomeloService {
141             return this.currServer;
142         }
143         /**
144          * 获取当前的服务端状态
145          */
146         isRunning(): boolean {
147             return this.running;
148         }
149     }
150 }
View Code

在文件夹下新建PomeloService类用来链接Pomelo服务端

 1 module network {
 2     /**
 3      * 服务端模块列表
 4      */
 5     export class PomeloService {
 6         public constructor() {
 7         }
 8         /**
 9          * Gate模块
10          */
11         public static GATE: string = "PomeloService_GATE";
12         /**
13          * Connect 模块操作
14          */
15         public static CONNECTION: string = "PomeloService_CONNECTION";
16     }
17 }
View Code

在项目src目录下创建pomeloTest文件,链接pomelo相应的服务端

class PomeloTest {

    private connectIp: string;
    private connectPort: number;

    public constructor() {
        Global.addEventListener(events.PomeloServerEvents.CONNECTION_GATE_SUCCEED, this.onGateSucceed, this);
        Global.addEventListener(events.PomeloServerEvents.CONNECTION_GATE_ERROR, this.onGateError, this);
        Global.addEventListener(events.PomeloServerEvents.CONNECTION_CONNECT_SUCCEED, this.onConnectSucceed, this);
        Global.addEventListener(events.PomeloServerEvents.CONNECTION_CONNECT_ERROR, this.onConnectError, this);
    }

    connectGate() {
        config.Config.pomelo.init();
        config.Config.pomelo.open(network.PomeloService.GATE, config.Config.gateServer.ip, config.Config.gateServer.port);
    }

    private onGateSucceed() {
        Global.addEventListener(events.PomeloServerEvents.DISCONNECT_SUCCEED, this.onGateClosed, this);

        config.Config.pomelo.request("gate.gateHandler.queryEntry", { uid: config.Config.player.name }, this.onGateMsg);

        trace("Gate服务端链接成功");
    }

    private onGateError() {
        trace("Gate服务端链接失败");
    }

    private onGateMsg(gate_data) {
        this.connectIp = gate_data.host;
        this.connectPort = gate_data.port;
        config.Config.pomelo.disconnect();
        trace("正在尝试链接connect服务端...");
        config.Config.pomelo.open(network.PomeloService.CONNECTION, this.connectIp, this.connectPort);
    }

    private onGateClosed() {
        trace("Gate服务端成功断开链接");
        // trace("正在尝试链接connect服务端...");
        // config.global.pomelo.open(network.PomeloService.CONNECTION, this.connectIp, this.connectPort);
    }

    private onConnectSucceed() {
        trace("CONNECT服务端链接成功");
        trace("开始注册服务端信息...");
        config.Config.pomelo.request('connector.entryHandler.entry', { name: config.Config.player.name }, this.onEntryMsg);
    }

    private onConnectError() {
        trace("CONNECT服务端链接失败...");
    }

    private onEntryMsg(entry_data) {
        if (entry_data.code === 200) {
            trace("注册信息成功");
            trace("开始申请进入游戏...");
            config.Config.pomelo.request('area.playerHandler.enterScene', { name: config.Config.player.name, playerId: entry_data.playerId }, (respose) => {
                Global.dispatchEvent(events.PomeloServerEvents.MAPMSG, respose);
                trace("进入游戏成功");
                trace("开始解析地图信息");
            });
        } else {
            trace("注册服务端信息出现问题,请检查提交信息");
        }
    }

    move(x: number, y: number, targetId: string) {
        config.Config.pomelo.notify('area.playerHandler.move', { targetPos: { x: x, y: y }, target: targetId });
    }

    changeStage(s: string) {
        config.Config.pomelo.notify('area.playerHandler.changeStage', { S: s });
    }
}
View Code

 完整源码下载

 

2.服务端代码

以上步骤都是准备工作,各语言间的链接方式和代码都不相同,如果没有使用egret可以使用pomelo项目中自带的web-server项目就可以轻松搭建起来的,接下来,就是服务端中的代码说明,因为本人的代码写的并不是很好,所以既然想做个好点的游戏,一步一步剖析pomelo的运行方式是很重要的一步。

2.1代码执行流程

在game-server文件夹下 直接使用pomelo命令启动app.js

pomelo start

出现这样的界面,就证明pomelo服务端启动成功了。那首先的一步 就是查看app文件中的代码

var bearcat = require('bearcat');
var pomelo = require('pomelo');

/**
 * Init app for client.
 */
var app = pomelo.createApp();

当代码执行到 var app = pomelo.createApp(); 这句时,将执行game-server/node_modules/pomelo/pomelo.js 文件中的 createApp方法

 
var application = require('./application');

/*
* * Create an pomelo application. * * @return {Application} * @memberOf Pomelo * @api public */ Pomelo.createApp = function (opts) { var app = application;
//初始化 app.init(opts); self.app
= app; return app;
};

这个方法中一共四行执行代码,第一行是引用了pomelo.js同级目录下的application.js文件,对pomelo文件中的application对象进行初始化,并将初始化的对方返回给调用该方法的app.js中的app对象。下面我们看一下,这个app.init(opts);这句话具体做了些什么呢? 我们进入application文件看一下。

 

 前面标红的地方,是对当前application文件中的信息进行一个初始化,因为createApp()调用时并未对其传递相关的opts参数,所以,这里涉及到opts变量相关的应该是“undefined”。

appUtil.defaultConfiguration(this);是做什么的呢?我们转到【 var appUtil = require('./util/appUtil');】  game-server/node_modules/pomelo/util/appUtil.js文件查找defaultConfiguration方法。

 setupEnv (环境配置)

var setupEnv = function (app, args) {
  app.set(Constants.RESERVED.ENV, args.env || process.env.NODE_ENV || Constants.RESERVED.ENV_DEV, true);
};

app.set方法:(设置配置文件,并返回设置的值)

/**
 * Assign `setting` to `val`, or return `setting`'s value.
 *
 * Example:
 *
 *  app.set('key1', 'value1');
 *  app.get('key1');  // 'value1'
 *  app.key1;         // undefined
 *
 *  app.set('key2', 'value2', true);
 *  app.get('key2');  // 'value2'
 *  app.key2;         // 'value2'
 *
 * @param {String} setting the setting of application
 * @param {String} val the setting's value
 * @param {Boolean} attach whether attach the settings to application
 * @return {Server|Mixed} for chaining, or the setting value
 * @memberOf Application
 */
Application.set = function (setting, val, attach) {
  if (arguments.length === 1) {
    return this.settings[setting];
  }
  this.settings[setting] = val;
  if(attach) {
    this[setting] = val;
  }
  return this;
};

loadMaster(加载master json文件)

var loadMaster = function (app) {
  app.loadConfigBaseApp(Constants.RESERVED.MASTER, Constants.FILEPATH.MASTER);
  app.master = app.get(Constants.RESERVED.MASTER);
};

app.loadConfigBaseApp方法:(递归方式 加载json配置文件)

/**
 * Load Configure json file to settings.(support different enviroment directory & compatible for old path)
 *
 * @param {String} key environment key
 * @param {String} val environment value
 * @param {Boolean} reload whether reload after change default false
 * @return {Server|Mixed} for chaining, or the setting value
 * @memberOf Application
 */
Application.loadConfigBaseApp = function (key, val, reload) {
  var self = this;
  var env = this.get(Constants.RESERVED.ENV);
  var originPath = path.join(Application.getBase(), val);
  var presentPath = path.join(Application.getBase(), Constants.FILEPATH.CONFIG_DIR, env, path.basename(val));
  var realPath;
  if(fs.existsSync(originPath)) {
     realPath = originPath;
     var file = require(originPath);
     if (file[env]) {
       file = file[env];
     }
     this.set(key, file);
  } else if(fs.existsSync(presentPath)) {
    realPath = presentPath;
    var pfile = require(presentPath);
    this.set(key, pfile);
  } else {
    logger.error('invalid configuration with file path: %s', key);
  }

  if(!!realPath && !!reload) {
    fs.watch(realPath, function (event, filename) {
      if(event === 'change') {
        delete require.cache[require.resolve(realPath)];
        self.loadConfigBaseApp(key, val);
      }
    });
  }
};

master 的json文件加载完成了,下一步就是加载server的json文件

/**
 * Load server info from config/servers.json.
 */
var loadServers = function (app) {
  app.loadConfigBaseApp(Constants.RESERVED.SERVERS, Constants.FILEPATH.SERVER);
  var servers = app.get(Constants.RESERVED.SERVERS);
  var serverMap = {}, slist, i, l, server;
  for (var serverType in servers) {
    slist = servers[serverType];
    for (i = 0, l = slist.length; i < l; i++) {
      server = slist[i];
      server.serverType = serverType;
      if (server[Constants.RESERVED.CLUSTER_COUNT]) {
        utils.loadCluster(app, server, serverMap);
        continue;
      }
      serverMap[server.id] = server;
      if (server.wsPort) {
        logger.warn('wsPort is deprecated, use clientPort in frontend server instead, server: %j', server);
      }
    }
  }
  app.set(Constants.KEYWORDS.SERVER_MAP, serverMap);
};

 

首先加载server的json文件并存储于app中,遍历读取到的servers的serverType,通过servers[serverType]可获取到对应的服务端配置组,使用utils.loadCluster(app, server, serverMap);方法操作, game-server/node_modules/pomelo/util/util.js,下面来看一下loadCluster方法是来做什么的。

/**
 * Load cluster server.
 *
 */
utils.loadCluster = function(app, server, serverMap) {
  var increaseFields = {};
  var host = server.host;
  var count = parseInt(server[Constants.RESERVED.CLUSTER_COUNT]);
  var seq = app.clusterSeq[server.serverType];
  if(!seq) {
    seq = 0;
    app.clusterSeq[server.serverType] = count;
  } else {
    app.clusterSeq[server.serverType] = seq + count;
  }

  for(var key in server) {
    var value = server[key].toString();
    if(value.indexOf(Constants.RESERVED.CLUSTER_SIGNAL) > 0) {
      var base = server[key].slice(0, -2);
      increaseFields[key] = base;
    }
  }

  var clone = function(src) {
    var rs = {};
    for(var key in src) {
      rs[key] = src[key];
    }
    return rs;
  };
  for(var i=0, l=seq; i<count; i++,l++) {
    var cserver = clone(server);
    cserver.id = Constants.RESERVED.CLUSTER_PREFIX + server.serverType + '-' + l;
    for(var k in increaseFields) {
      var v = parseInt(increaseFields[k]);
      cserver[k] = v + i;
    }
    serverMap[cserver.id] = cserver;
  }
};

 

这个方法可以看出,是用来做集群间的负载均衡,将设置app中的clusterSeq 属性值,以用来存储集群的ID。

processArgs(创建进程启动服务端)

/**
 * Process server start command
 */
var processArgs = function (app, args) {
  var serverType = args.serverType || Constants.RESERVED.MASTER;
  var serverId = args.id || app.getMaster().id;
  var mode = args.mode || Constants.RESERVED.CLUSTER;
  var masterha = args.masterha || 'false';
  var type = args.type || Constants.RESERVED.ALL;
  var startId = args.startId;

  app.set(Constants.RESERVED.MAIN, args.main, true);
  app.set(Constants.RESERVED.SERVER_TYPE, serverType, true);
  app.set(Constants.RESERVED.SERVER_ID, serverId, true);
  app.set(Constants.RESERVED.MODE, mode, true);
  app.set(Constants.RESERVED.TYPE, type, true);
  if (!!startId) {
    app.set(Constants.RESERVED.STARTID, startId, true);
  }

  if (masterha === 'true') {
    app.master = args;
    app.set(Constants.RESERVED.CURRENT_SERVER, args, true);
  } else if (serverType !== Constants.RESERVED.MASTER) {
    app.set(Constants.RESERVED.CURRENT_SERVER, args, true);
  } else {
    app.set(Constants.RESERVED.CURRENT_SERVER, app.getMaster(), true);
  }
};

 

这个方法设置了app的一些属性参数值,后两步的日志文件和生命周期,放到后面的章节再研究,现在app的信息已经完善,至此,appUtil.appdefaultConfiguration方法执行完成,Pomelo.createApp执行完成,并将app返回给app.js文件中的app对象,思路回到app.js中,代码继续向下走

var bearcat = require('bearcat');
var pomelo = require('pomelo');

/**
 * Init app for client.
 */
var app = pomelo.createApp();

var Configure = function() {
  app.set('name', 'treasures');

  app.configure('production|development', 'gate', function() {
    app.set('connectorConfig', {
      connector: pomelo.connectors.hybridconnector
    });
  });

  app.configure('production|development', 'connector', function() {
    app.set('connectorConfig', {
      connector: pomelo.connectors.hybridconnector,
      heartbeat: 100,
      useDict: true,
      useProtobuf: true
    });
  });

  app.configure('production|development', 'area', function() {
    var areaId = app.get('curServer').areaId;
    if (!areaId || areaId < 0) {
      throw new Error('load area config failed');
    }

    var areaService = bearcat.getBean('areaService');
    var dataApiUtil = bearcat.getBean('dataApiUtil');
    areaService.init(dataApiUtil.area().findById(areaId));
  });
}

 app赋值完成之后,声明了Configure对象,这里貌似是接收消息使用的,下面来去到application.configure。

function load(path, name) {
  if (name) {
    return require(path + name);
  }
  return require(path);
}

/**
 * connectors
 */
Pomelo.connectors = {};
Pomelo.connectors.__defineGetter__('sioconnector', load.bind(null, './connectors/sioconnector'));
Pomelo.connectors.__defineGetter__('hybridconnector', load.bind(null, './connectors/hybridconnector'));
Pomelo.connectors.__defineGetter__('udpconnector', load.bind(null, './connectors/udpconnector'));
Pomelo.connectors.__defineGetter__('mqttconnector', load.bind(null, './connectors/mqttconnector'));

Pomelo.connectors.__defineGetter__('hybridconnector', load.bind(null, './connectors/hybridconnector')); 将 game-server/node_modules/pomelo/connectors/hybridconnector.js的引用赋值给app中的connectorConfig属性设置,这个connector可以看做是一个链接的控制器,后续的操作将围绕着这个connector对象来开展。

至此,app的创建准备工作便完成了。

下面是重要的一步,程序开始,通过start方法启动服务端

 

关于更多请关注 bearcat 的介绍 

 

其它

从代码中可以看出这个app已经启动完成,在这个期间有还有一个在application文件中的对象Constants,constants文件是记录程序中的一些基础的配置。

module.exports = {
  KEYWORDS: {
    BEFORE_FILTER: '__befores__',
    AFTER_FILTER: '__afters__',
    GLOBAL_BEFORE_FILTER: '__globalBefores__',
    GLOBAL_AFTER_FILTER: '__globalAfters__',
    ROUTE: '__routes__',
    BEFORE_STOP_HOOK: '__beforeStopHook__',
    MODULE: '__modules__',
    SERVER_MAP: '__serverMap__',
    RPC_BEFORE_FILTER: '__rpcBefores__',
    RPC_AFTER_FILTER: '__rpcAfters__',
    MASTER_WATCHER: '__masterwatcher__',
    MONITOR_WATCHER: '__monitorwatcher__'
 },

  FILEPATH: {
    MASTER: '/config/master.json',
    SERVER: '/config/servers.json',
    CRON: '/config/crons.json',
    LOG: '/config/log4js.json',
    SERVER_PROTOS: '/config/serverProtos.json',
    CLIENT_PROTOS: '/config/clientProtos.json',
    MASTER_HA: '/config/masterha.json',
    LIFECYCLE: '/lifecycle.js',
    SERVER_DIR: '/app/servers/',
    CONFIG_DIR: '/config'
  },

  DIR: {
    HANDLER: 'handler',
    REMOTE: 'remote',
    CRON: 'cron',
    LOG: 'logs',
    SCRIPT: 'scripts',
    EVENT: 'events',
    COMPONENT: 'components'
  },

  RESERVED: {
    BASE: 'base',
    MAIN: 'main',
    MASTER: 'master',
    SERVERS: 'servers',
    ENV: 'env',
    CPU: 'cpu',
    ENV_DEV: 'development',
    ENV_PRO: 'production',
    ALL: 'all',
    SERVER_TYPE: 'serverType',
    SERVER_ID: 'serverId',
    CURRENT_SERVER: 'curServer',
    MODE: 'mode',
    TYPE: 'type',
    CLUSTER: 'clusters',
    STAND_ALONE: 'stand-alone',
    START: 'start',
    AFTER_START: 'afterStart',
    CRONS: 'crons',
    ERROR_HANDLER: 'errorHandler',
    GLOBAL_ERROR_HANDLER: 'globalErrorHandler',
    AUTO_RESTART: 'auto-restart',
    RESTART_FORCE: 'restart-force',
    CLUSTER_COUNT: 'clusterCount',
    CLUSTER_PREFIX: 'cluster-server-',
    CLUSTER_SIGNAL: '++',
    RPC_ERROR_HANDLER: 'rpcErrorHandler',
    SERVER: 'server',
    CLIENT: 'client',
    STARTID: 'startId',
    STOP_SERVERS: 'stop_servers',
    SSH_CONFIG_PARAMS: 'ssh_config_params'
  },

  COMMAND: {
    TASKSET: 'taskset',
    KILL: 'kill',
    TASKKILL: 'taskkill',
    SSH: 'ssh'
  },

  PLATFORM: {
    WIN: 'win32',
    LINUX: 'linux'
  },

  LIFECYCLE: {
    BEFORE_STARTUP: 'beforeStartup',
    BEFORE_SHUTDOWN: 'beforeShutdown',
    AFTER_STARTUP: 'afterStartup',
    AFTER_STARTALL: 'afterStartAll'
  },

  SIGNAL: {
    FAIL: 0,
    OK: 1
  },

 TIME: {
   TIME_WAIT_STOP: 3 * 1000,
   TIME_WAIT_KILL: 5 * 1000,
   TIME_WAIT_RESTART: 5 * 1000,
   TIME_WAIT_COUNTDOWN: 10 * 1000,
   TIME_WAIT_MASTER_KILL: 2 * 60 * 1000,
   TIME_WAIT_MONITOR_KILL: 2 * 1000,
   TIME_WAIT_PING: 30 * 1000,
   TIME_WAIT_MAX_PING: 5 * 60 * 1000,
   DEFAULT_UDP_HEARTBEAT_TIME: 20 * 1000,
   DEFAULT_UDP_HEARTBEAT_TIMEOUT: 100 * 1000,
   DEFAULT_MQTT_HEARTBEAT_TIMEOUT: 90 * 1000
 }
};
View Code

 

由于基础太差需要好好吸收一下,本次就学到这,下章继续~

 

posted @ 2017-12-25 16:43  枫之戊  阅读(2121)  评论(0编辑  收藏  举报