[egret+pomelo]实时游戏杂记(1)
资料
准备工作
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 }
在文件夹下新建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 }
在项目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 }); } }
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 } };
由于基础太差需要好好吸收一下,本次就学到这,下章继续~