pomelo 架构概览
pomelo 之所以简单易用、功能全面,并且具有高可扩展性、可伸缩性等特点,这与它的技术选型和方案设计是密不可分的。在研究大量游戏引擎设计思路基础上,结合以往游戏开发的经验,确定了pomelo框架的设计方案。1、pomelo为什么采用 node.js 开发
node.js 自身特点与游戏服务器的特性惊人的吻合。 在 node.js 的官方定义中, fast、scalable、realtime、network 这几个特性都非常符合游戏服务器的要求。游戏服务器是个网络密集型的应用,对实时性要求极高,而 node.js 在网络 io 上的优势也完全可以满足这点。使用 node.js 开发游戏服务器的优势总结:
- io 与可伸缩性的优势。io 密集型的应用采用 node.js 是最合适的, 可达到最好的可伸缩性;
- 多进程单线程的应用架构。node.js 天生采用单线程, 使它在处理复杂逻辑的时候无需考虑线程同步、锁、死锁等一系列问题, 减少了很多逻辑错误。 由多进程 node.js 组成的服务器群是最理想的应用架构;
- 语言优势。使用 javascript 开发可以实现快速迭代,如果客户端使用 html5,更可实现代码共用。
2、游戏服务器的运行架构
一个真正高可扩展的游戏运行架构必须是多进程的。google 的 gritsgame, mozilla 的 browserquest 都采用了 node.js 作为游戏服务器开发语言, 但它们都采用了单进程的 node.js 服务器,缺乏扩展性,这使它们可以支撑的在线用户数量是很有限的(这两个游戏主要是作为 HTML5 游戏的 demo )。而多进程的架构可以很好的实现游戏服务器的的扩展性,达到支撑较多在线用户、降低服务器压力等要求。
一个典型的多进程 MMO 运行架构, 如下图所示:

说明: 上图中的方块表示进程, 定义上等同于“服务器”
运行架构说明:
- 客户端通过 websocket 长连接连到 connector 服务器群;
- connector 负责承载连接,并把请求转发到后端的服务器群;
- 后端的服务器群主要包括按场景分区的场景服务器 (area) 、聊天服务器 (chat) 和状态服务器等 (status), 这些服务器负责各自的业务逻辑。真实的案例中还会有各种其它类型的服务器;
- 后端服务器处理完逻辑后把结果返回给 connector, 再由 connector 广播回给客户端;
- master 负责统一管理这些服务器,包括各服务器的启动、监控和关闭等功能。
游戏运行架构与 web 应用运行架构的区别:
该游戏运行架构表面上看与 web 应用运行架构很类似,connector 类似于 web 应用的 apache/nginx 等 web 服务器,后端的服务器群类似于 web 应用中的应用服务器(如 tomcat ),但实际上存在着很大的差别:- 长连接与短连接。web 应用使用基于 http 的短连接以达到最大的可扩展性,游戏应用采用基于 socket(websocket) 的长连接,以达到最大的实时性;
- 分区策略不同。web 应用的分区可以根据负载均衡自由决定, 而游戏则是基于场景 (area) 的分区模式, 这使同场景的玩家跑在一个进程内, 以达到最少的跨进程调用;
- 有状态和无状态。web 应用是无状态的, 可以达到无限的扩展。 而游戏应用则是有状态的, 由于基于场景的分区策略,它的请求必须路由到指定的服务器, 这也使游戏达不到 web 应用同样的可扩展性;
- 广播模式和 request/response 模式。web 应用采用了基于 request/response 的请求响应模式。而游戏应用则更频繁地使用广播, 由于玩家在游戏里的行动要实时地通知场景中的其它玩家, 必须通过广播的模式实时发送。这也使游戏在网络通信上的要求高于 web 应用。
复杂的运行架构需要一个框架来简化开发
游戏的运行架构很复杂,要想支撑起如此复杂的运行架构,必须要有一个框架来简化开发。 pomelo 正是这样一个框架,它使我们用最少的代码, 最清晰的结构来实现复杂的运行架构。3、pomelo 的框架介绍
pomelo framework 的组成架构如图所示:
- server management, pomelo 是个真正多进程、分布式的游戏服务器。因此各游戏server(进程)的管理是pomelo很重要的部分,框架通过抽象使服务器的管理非常容易;
- network, 请求、响应、广播、RPC、session 管理等构成了整个游戏框架的脉络,所有游戏流程都构建在这个脉络上;
- application, 应用的定义、component 管理,上下文配置, 这些使 pomelo framework 的对外接口很简单, 并且具有松耦合、可插拔架构。
3.1、pomelo 的架构设计目标
- 服务器(进程)的抽象与扩展:在 web 应用中, 每个服务器是无状态、对等的, 开发者无需通过框架或容器来管理服务器。 但游戏应用不同, 游戏可能需要包含多种不同类型的服务器,每类服务器在数量上也可能有不同的需求。这就需要框架对服务器进行抽象和解耦,支持服务器类型和数量上的扩展;
- 客户端的请求、响应、广播:客户端的请求、响应与 web 应用是类似的, 但框架是基于长连接的, 实现模式与 http 请求有一定差别。 广播是游戏服务器最频繁的操作, 需要方便的 API, 并且在性能上达到极致;
- 服务器间的通讯、调用:尽管框架尽量避免跨进程调用,但进程间的通讯是不可避免的, 因此需要一个方便好用的 RPC 框架来支撑;
- 松耦合、可插拔的应用架构:应用的扩展性很重要, pomelo framework 支持以 component 的形式插入任何第三方组件, 也支持加入自定义的路由规则, 自定义的 filter 等。
3.2、服务器(进程)的抽象与扩展介绍
服务器的抽象与分类
该架构把游戏服务器做了抽象, 抽象成为两类:前端服务器和后端服务器, 如图:
前端服务器 (frontend) 的职责:
- 负责承载客户端请求的连接;
- 维护 session 信息;
- 把请求转发到后端;
- 把后端需要广播的消息发到前端。
- 处理业务逻辑, 包括 RPC 和前端请求的逻辑;
- 把消息推送回前端。
服务器的鸭子类型
动态语言的面向对象有个基本概念叫鸭子类型。 服务器的抽象也同样可以比喻为鸭子, 服务器的对外接口只有两类, 一类是接收客户端的请求, 叫做 handler, 一类是接收 RPC 请求, 叫做 remote, handler 和 remote 的行为决定了服务器长什么样子。 因此我们只要定义好 handler 和 remote 两类的行为, 就可以确定这个服务器的类型。服务器抽象的实现
利用目录结构与服务器对应的形式, 可以快速实现服务器的抽象。以下是示例图:
图中的 connector, area, chat 三个目录代表三类服务器类型, 每个目录下的 handler 与 remote 决定了这个服务器的行为(对外接口)。 开发者只要往 handler 与 remote 目录填代码, 就可以实现某一类的服务器。这让服务器实现起来非常方便。 让服务器动起来, 只要填一份配置文件 servers.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, "area": 1},
{"id": "area-server-2", "host": "127.0.0.1", "port": 3251, "area": 2},
{"id": "area-server-3", "host": "127.0.0.1", "port": 3252, "area": 3}
],
"chat":[
{"id":"chat-server-1","host":"127.0.0.1","port":3450}
]
}
}
3.3、客户端请求与响应、广播的抽象介绍
所有的 web 应用框架都实现了请求与响应的抽象。尽管游戏应用是基于长连接的, 但请求与响应的抽象跟 web 应用很类似。 下图的代码是一个 request 请求示例:
上图的 remote 目录里定义了一个 RPC 接口:chatRemote.js,它的接口定义如下:
chatRemote.kick = function(uid, player, cb) {
}
其它服务器(RPC 客户端)只要通过以下接口就可以实现 RPC 调用:
app.rpc.chat.chatRemote.kick(session, uid, player, function(data){
});
这个调用会根据特定的路由规则转发到特定的服务器。(如场景服务的请求会根据玩家在哪个场景直接转发到对应的 server )。 RPC 框架目前在底层采用 socket.io 作为通讯协议,但协议对上层是透明的,以后可以替换成任意的协议。
3.4、pomelo 支持可插拔的 component 扩展架构
component 是 pomelo 自定义组件,开发者可自加载自定义的 component。 component 在 pomelo 框架参考将有更深入的讨论。 以下是 component 的生命周期图:
用户只要实现component相关的接口: start, afterStart, stop, 就可以加载自定义的组件:
app.load([name], comp, [opts])