(十四)nodejs循序渐进-高性能游戏服务器框架pomelo之开发Treasures游戏

#Tutorial 2 -- Treasures ##描述 Treasures 游戏是从 LordOfPomelo 中抽取出来,去掉了大量的游戏逻辑,用以更好的展示 Pomelo 框架的用法以及运作机制。

Treasures 很简单,输入一个用户名后,会随机得到一个游戏角色,进入游戏场景。在游戏场景中地上会散落一些宝物,每个宝物都有分数,玩家操作游戏人物去捡起地上的宝物,然后就能得到相应的分数。

##安装和运行 安装 pomelo

npm install -g pomelo

获取源码

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

安装 npm 依赖包(先进入项目目录)

sh npm-install.sh

启动 web-server (先进入web-server目录)

node app.js

启动 game-server (先进入game-server目录)

pomelo start

在浏览器中访问 http://localhost:3001 进入游戏

Pomelo自带的demo只Treasures运行时报错解决方案:

Pomelo自带demo之Treasures,下载源码后进入web-server目录,先输入命令 npm install -d 安装第三方模块,之后运行会报错,错误提示是:TypeError: mime.lookup is not a function,web-server\node_modules\connect\lib\middleware\static.js:144。原因是mime模块从2.x版本开始把lookup方法改为了getType,而作者编写时的mime模块是1.x版本。如果把lookup改为getType,虽能解决眼前这个错误,但接下来还会有其它地方报错,因为2.x版本不仅仅修改了这一个方法。更简单的解决办法是直接使用mime的1.x版本。方法有2种:

1、在web-server目录下运行命令:npm install mime
2、打开web-server目录的package.json,在dependencies字段中增加一行:"mime": "^1",意思是安装mime的1.x版本。之后运行命令:npm install -d

第1种方法虽简单,但如果把源码clone到别处,问题会重复出现,所以推荐使用第2种方法,在package.json明确好版本,不管clone到哪,只要npm install -d就好了。

 

##架构 Treasures 分为 web-Server 和 game-Server 两部分。

  • web-server 是用 Express 建立的最一个基础的 http 服务,用来支撑浏览器页面的访问。

  • game-server 是 WebSocket 服务器,用来运行整个游戏的逻辑。

首先,通过配置文件,来看 game-server 的具体架构 game-server/config/server.json

{"development": {"connector": [{"id": "connector-server-1", "host": "127.0.0.1", "port": 3150, "clientPort": 3010, "frontend": true},{"id": "connector-server-2", "host": "127.0.0.1", "port": 3151, "clientPort": 3011, "frontend": true}],"area": [{"id": "area-server-1", "host": "127.0.0.1", "port": 3250, "areaId": 1}],"gate": [{"id": "gate-server-1", "host": "127.0.0.1", "clientPort": 3014, "frontend": true}]}
}

可以看出,服务端是由以下几个部分构成:

  • 2 个 connector 服务器,主要用于接受和发送消息。
  • 1 个 gate 服务器,主要用于负载均衡,将来自客户端的连接分散到两个 connector 服务器上。
  • 1 个 area 服务器,主要用于驱动游戏场景,和游戏逻辑

##源码分析 通过游戏流程来分析代码。

1. 连接服务器 客户端 web-server/public/js/main.js 中 entry 方法中

pomelo.request('gate.gateHandler.queryEntry', {uid: name}, function(data) {//...
});

服务端 game-server/app/servers/gate/handler/gateHandler.js 中

Handler.prototype.queryEntry = function(msg, session, next) {// ...// 返回要连接的 connector 服务器的 host 和 portnext(null, {code: Code.OK, host: res.host, port: res.wsPort});
};

这样客户端就能连接到分配的 connector 服务器上。

2. 进入游戏 在与 connector 服务器建立连接之后,开始进入游戏

pomelo.request('connector.entryHandler.entry', {name: name}, function(data) {// ...
});

在客户端第一次向 connector 服务器发送请求时,服务器会将 session 信息进行初始化和绑定

// session 与 playerId 绑定
session.bind(playerId);
// 设置玩家 areaId
session.set('areaId', 1);

进入游戏场景,客户端向服务端发起进入场景请求:

pomelo.request("area.playerHandler.enterScene", {name: name, playerId: data.playerId}, function(data) {// ...
});

客户端向服务端发送请求后,先到达 connector 服务器,然后 connector 服务器根据 game-server/app/util/routeUtil.js 中转发规则,将请求路由到相应的 area 服务器(本例子中只有一个area服务器),area 服务器中的 playerHandler 再处理相应的请求。这样玩家就加入到游戏场景中了。

在一个玩家加入到游戏场景之后,其他玩家必须能即时的看到这个玩家的加入,所以服务端必须将消息广播到在此游戏场景中的所有玩家。 建立 channel,所有加入此游戏场景的玩家都会加入到这个 channel 中

// 获取 channel,如果没有就创建一个
channel = pomelo.app.get('channelService').getChannel('area_' + id, true);
// 将玩家加入 channel
channel.add(e.id, e.serverId);

当 area 中有玩家加入,或其他状态发生改变时,这些信息都会被推送到在这个 channel 中的每个玩家。比如有玩家加入时:

channel.pushMessage({route: 'addEntities', entities: added});

这些消息都是通过 connector 服务器发送到客户端。而 area 中的消息是通过 session.frontendId 来决定是由哪个 connector 服务器发出去。

客户端接受消息:

// 当有新玩家加入时,服务端会广播消息给所有玩家。客户端通过这个路由绑定,来获取消息
pomelo.on('addEntities', function(data) {// ...
});

3. Area 服务器 area 服务器是一个由 tick 驱动的游戏场景。每个 tick 都会对场景中的 entity 的状态进行更新,如果状态有发生改变,这些改变会被推送到客户端。

function tick() {//run all the actionarea.actionManager().update();// update entitiesarea.entityUpdate();// update rankarea.rankUpdate();
}

比如玩家发起一个 move 动作:

客户端

// 向服务端发送 move 请求通知
pomelo.notify('area.playerHandler.move', {targetPos: {x: entity.x, y: entity.y}, target: targetId});

服务端 playerHandler 接受请求:

handler.move = function(msg, session, next) {// ...// 产生一个 move actionvar action = new Move({entity: player,endPos: endPos,});
});

然后这个 action 会在每个 tick 中更新。

###4. 客户端发送和接受消息 客户端和服务端的通讯有以下几种方式:

  • Request - Response 方式
// 向 connector 发送请求,参数 {name: name}
pomelo.request('connector.entryHandler.entry', {name: name}, function(data) {// 回调函数得到请求返回结果// do something
});
  • Notify (向服务端发送通知)
// 向服务端发送 move 请求通知
pomelo.notify('area.playerHandler.move', {targetPos: {x: entity.x, y: entity.y}, target: targetId});
  • Push (服务端主动发送消息到客户端)
// 当有新玩家加入时,服务端会广播消息给所有玩家。客户端通过这个路由绑定,来获取消息
pomelo.on('addEntities', function(data) {// ...
});

###5. 离开游戏 就是在玩家离开游戏时,connector 服务器会先收到断开的消息,这时,它需要在 area 服务器中将用户剔除,并广播消息给其他在线玩家。 因为服务器之间的进程都是独立的,所以这就涉及到一个 RPC 调用,好在 Pomelo 框架对 RPC 做了很好的封装,做法如下: area 服务器想要提供一系列的 Remote 接口供其他服务器进程调用,只需要在 servers/area 目录下,创建一个 remote 目录,在这个目录下的文件暴露出的接口,都可以作为 RPC 调用接口。 比如,玩家离开:

// connector 中对 session 绑定事件,当 session 关闭时,触发事件
session.on('closed', onUserLeave.bind(null, self.app));var onUserLeave = function (app, session, reason) {if (session && session.uid) {// rpc 调用app.rpc.area.playerRemote.playerLeave(session, {playerId: session.get('playerId'), areaId: session.get('areaId')}, null);}
};

对应的 area/remote/playerRemote.js 中 playerLeave 方法

exports.playerLeave = function(args, cb) {// 发出通知area.getChannel().pushMessage({route: 'onUserLeave', code: consts.MESSAGE.RES, playerId: playerId});// ...
};

这样就轻易的完成了一个跨进程的调用 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/444492.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

leetcode243. 最短单词距离(vip题)好像挺简单?

给定一个单词列表和两个单词 word1 和 word2,返回列表中这两个单词之间的最短距离。 示例: 假设 words ["practice", "makes", "perfect", "coding", "makes"] 输入: word1 “coding”, word2 “practice”…

谈谈苹果应用内支付(IAP)的坑

一、请求商品 下面是请求商品的代码: - (void)validateProductIdentifier:(NSArray *)productIdentifier {SKProductsRequest *productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIdentifier]];self.request = productRe…

leetcode204. 计数质数(vip题)

统计所有小于非负整数 n 的质数的数量。 示例: 输入: 10 输出: 4 解释: 小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。 思路:筛法,见代码。 class Solution {public int countPrimes(int n) {// 1. 给数加上标记byte[] nums new byte[n];for (i…

如何使得客户端和服务器端完美配合做IOS应用内付费

配置Developer.apple.com 登录到Developer.apple.com,然后进行以下步骤: 为应用建立建立一个不带通配符的App ID用该App ID生成和安装相应的Provisioning Profile文件。配置iTunes Connect 登录到iTunes Connet,然后进行以下步骤: 用该App ID创建一个新的应用。在该应用中…

IOS内购流程从0-1手把手教会

苹果掌握着可能是全球最重要的APP分发渠道,然而30%的抽成近年来也被人批评,现在苹果似乎也看到反对意见了,从2021年1月1日开始,部分小型企业的分成费用降低到15%。 据报道,苹果将于2021年1月1日启动App Store小企业项目,会降低他们的抽成费用。针对年收入不足100万美元的…

leetcode217. 存在重复元素(vip题)超简单

给定一个整数数组,判断是否存在重复元素。 如果任何值在数组中出现至少两次,函数返回 true。如果数组中每个元素都不相同,则返回 false。 示例 1: 输入: [1,2,3,1] 输出: true 示例 2: 输入: [1,2,3,4] 输出: false 示例 3: 输入: [1,1,…

订单数据持久化和验证相关解决方案

订单数据持久化 有时候苹果支付在支付完成后,从苹果服务器返回收据的过程中可能会掉单(可能是网络问题,可能是苹果BUG,也有一部分是开发者自身埋的坑),因此我们需要一个订单持久化的机制来保障。 首先根据内购商品ID(此商品ID是在苹果后台建好的内购商品)、用户信息(…

IOS iap处理逻辑流程图再次梳理

序言: 本文补全一下iOS iap处理逻辑。 iap处理逻辑 苹果退单wiki:https://developer.apple.com/documentation/storekit/in-app_purchase/handling_refund_notifications 一、上图主要处理了以下业务: 普通购买 自动续订订阅 补单处理 预防黑产 退单处理 二、除了上述业…

(十七)深入浅出TCPIP之HTTP和HTTPS

超文本传输协议HTTP协议被用于在Web浏览器和网站服务器之间传递信息,HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息,因此&…

leetcode283. 移动零 比官方更好的解法。

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 示例: 输入: [0,1,0,3,12] 输出: [1,3,12,0,0] 说明: 必须在原数组上操作,不能拷贝额外的数组。 尽量减少操作次数。 思路:记录0的个…

C++:15---异常机制

1.概念:异常处理是一种允许两个独立开发的程序组件在程序执行时遇到不正常的情况相互通信的工具 2.异常检测和异常处理的方式throw表达式:程序遇到了错误或者无法处理的问题,使用throw引发异常try、catch语句块:以关键字tyr开始,并以一个或多个catch子句结束。它们也被称为…

Redis:08---字符串对象

一、字符串对象概述字符串类型是Redis最基础的数据结构。首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的,所以字符串类型能为其他四种数据结构的学习奠定基础字符串就是一个由字节组成的序列如下图所示,字符串类型的…

leetcode252. 会议室

给定一个会议时间安排的数组&#xff0c;每个会议时间都会包括开始和结束的时间 [[s1,e1],[s2,e2],...] (si < ei)&#xff0c;请你判断一个人是否能够参加这里面的全部会议。 示例 1: 输入: [[0,30],[5,10],[15,20]] 输出: false 示例 2: 输入: [[7,10],[2,4]] 输出: tr…

(十八)深入浅出TCPIP之epoll的一些思考

Epoll基本介绍在linux的网络编程中&#xff0c;很长的时间都在使用select来做事件触发。在linux新的内核中&#xff0c;有了一种替换它的机制&#xff0c;就是epoll。相比于 select&#xff0c;epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select…

leetcode292. Nim 游戏

你和你的朋友&#xff0c;两个人一起玩 Nim 游戏&#xff1a;桌子上有一堆石头&#xff0c;每次你们轮流拿掉 1 - 3 块石头。 拿掉最后一块石头的人就是获胜者。你作为先手。 你们是聪明人&#xff0c;每一步都是最优解。 编写一个函数&#xff0c;来判断你是否可以在给定石头…

C++:16---强制类型转换和类型转换

旧式的强制类型转换 在早期C/C++中,显式地进行强制类型的转换有以下两种形式:type (expr) ; //函数形式的强制类型转换(type) expr; //C语言风格的强制类型转换比如: char c = 12; int b = (int)c; float f = float(b); C++的新式强制类型转换…

Nginx不停机优雅升级

最近线上运行的游戏越来越多,云服务商也给我推送提示系统升级,漏洞补丁升级,也有nginx更新的。 有一些比较关键性的系统补丁需要立即更新处理,有一些可以换一换不用升级,但此nginx升级的需求比较迫切,但更新可能需要重启nginx。 这将会影响到我们这样的一个登录业务逻辑…

leetcode186. 翻转字符串里的单词 II

给定一个字符串&#xff0c;逐个翻转字符串中的每个单词。 示例&#xff1a; 输入: ["t","h","e"," ","s","k","y"," ","i","s"," ","b","l…

Nginx大规模并发原理

Nginx在主流硬件上的并发数为十万,网络处理方面的领先地位,归功于突破性的事件驱动架构。 Nginx在每颗内核上创建一个工作进程,有效利用硬件资源。 在单个工作进程中交替处理多个连接,应对突如其来的网络流量。 Nginx资源管理 Nginx使用状态机管理流量。 非阻塞事件…

使用 CXF 做 webservice 简单例子

转&#xff1a;http://www.cnblogs.com/frankliiu-java/articles/1641949.html Apache CXF 是一个开放源代码框架&#xff0c;提供了用于方便地构建和开发 Web 服务的可靠基础架构。它允许创建高性能和可扩展的服务&#xff0c;您可以将这样的服务部署在 Tomcat 和基于 Spring …