Pomelo(柚子)是基于Node.js的高性能分布式游戏服务框架,它包含基础的开发框架和相关的扩展组件(库和工具包)。Pomelo不但适用于游戏服务器开发也可以用于开发高实时的Web应用,它的分布式架构使Pomelo比普通实时Web框架性能更好。
Pomelo是游戏服务器框架,本质上也是高实时、高扩展、多进程的应用框架,除了在提供的库部分有游戏专用的库,其余部分框架完全可用于开发高实时的应用。而且与现有的Node.js高实时应用框架如Derby、Socketstream、Meteor等相比具有更好的可伸缩性。
Pomelo为什么采用Node.js开发?
Node.js自身特点与游戏服的特性惊人的吻合,在Node.js官方定义中,fast、scalable、realtime、network这几个特性都非常符合游戏服的要求,游戏服是网络密集型的应用,对实时性要求极高,Node.js网络IO上的优势完全可以满足这点。
使用Node.js开发游戏服的优势
游戏服的运行架构
高可扩展的游戏运行架构必须是多进程的,Google的Gritsgame,Mozilla的Browserquest都采用了Node.js作为游戏服开发语言,它们都采用了单进程的Node.js服务器,由于缺乏扩展性,使它们可以支撑的在线用户数量是有限的。而多进程架构可以很好的实现游戏服的可扩展性,达到支撑较多的在线用户、降低服务器压力等要求。
典型多进程MMO运行架构
架构将游戏服做了抽象,抽象成两类:前端服务器、后端服务器
前端服务器(frontend server)
后端服务器(backend server)
游戏运行架构 & Web应用运行架构
游戏运行架构 | Web运行架构 |
---|---|
前端服务器 | Web服务器,如Apache/Nginx |
后端服务器 | 应用服务器,如Tomcat |
游戏运行架构与Web应用运行架构的区别
长连接与短连接
Web应用使用基于HTTP的短连接以达到最大的可扩展性,游戏应用采用基于Socket(WebSocket)的长连接以达到最大的实时性。
分区策略
Web应用的分区可根据负载均衡自行决定,游戏则是基于场景的分区模式,这使同场景的玩家跑在一个进程内以达到最少的跨进程调用。
有无状态
Web应用是无状态的,可以达到无限的扩展。游戏应用是有状态的,由于基于场景的分区策略,游戏的请求必须路由到指定的服务器,这也使游戏达不到Web应用同样的可扩展性。
通讯方式
Web应用基于请求响应模式,游戏应用则更为频繁的使用广播,由于玩家在游戏里的行动要求实时地通知场景中的其它玩家,必须通过广播的模式实时发送,这也使游戏在网路通信上的要求高于Web应用。
框架定位
Pomelo是一个轻量级的服务器框架,最合适的应用领域是网页游戏、社交游戏、移动游戏的服务端。不推荐将Pomelo作为大型MMORPG游戏开发,尤其是大型3D游戏,这需要像BigWorld商用引擎来支撑。
框架特性
框架组成
架构目标
框架优势
环境准备
全局安装框架
$ npm i -g pomelo
查看帮助
$ pomelo -h
用法: pomelo [选项] [命令]
命令:
init [path] 创建一个新的应用
start [options] 开启应用
list [options] 列出所有的服务器
add [options] 添加一个新的服务器
stop [options] 关闭服务器,多个服务器可使用 `pomelo stop server-id-1 server-id-2`
kill [options] 杀死应用
restart [options] 重启服务器,多个服务器可使用`pomelo restart server-id-1 server-id-2`
masterha [options] 开启主从模式下所有的从服务器
*
选项:
-h, --help 输出用法信息
-V, --version 输出版本号
创建项目
$ cd workspace
$ pomelo init ./test
默认管理员用户:
账户: admin
密码: admin
可编辑adminUser.json文件配置管理员用户
请选择底层连接器: 1 websocket(原生 socket), 2 socket.io, 3 wss, 4 socket.io(wss), 5 udp, 6 mqtt: [1]
目录结构
目录文件 | 描述 |
---|---|
game-server | 游戏服务器,以app.js文件为入口。 |
shared | 前后端、游戏服、Web服共用代码 |
web-server | Web服务器,Express框架搭建,以app.js为入口。 |
游戏服目录
目录 | 描述 |
---|---|
game-server/app | 游戏服应用开发目录,实现不同类型的服务器,添加对应Handler和Remote等。 |
game-server/config/ | 游戏服配置文件目录,配置文件以JSON格式定义。 |
game-server/logs/ | 游戏服日志目录 |
game-server/app.js | 游戏服入口文件 |
Web服目录
目录文件 | 描述 |
---|---|
web-server/bin/ | Web服应用保存目录 |
web-server/public/ | Web服静态文件保存路径 |
web-server/app.js | Web服入口文件 |
安装项目
$ cd test
$ npm-install.bat
安装脚本执行的步骤
$ cd ./game-server && npm install -d
$ cd ./web-server && npm install -d
启动项目
启动项目必须分别启动游戏服和Web服
$ cd game-server && pomelo start
启动命令
$ pomelo start [development|product] [--daemon]
命令参数 | 描述 |
---|---|
development | 默认,开发模式 |
product | 产品环境 |
--deamon | 后台运行 |
后台运行模式--daemon需forever模块支持
$ npm i -g forever
项目应用启动命令
$ pomelo start -h
用法: start [选项]
选项:
-h, --help 输出用法信息
-e, --env 指定环境
-D, --daemon 以后台守护进程模式启动
-d, --directory, 指定代码目录
-t, --type , 指定服务器类型启动
-i, --id 指定服务器ID启动
$ cd web-server && node app
访问Web服:http://127.0.0.1:3001
查看游戏服务器状态
$ cd test
$ pomelo list
try to connect 127.0.0.1:3005
serverId serverType pid rss(M) heapTotal(M) heapUsed(M) uptime(m)
connector-server-1 connector 8288 37.84 21.83 17.02 5.48
master-server-1 master 22696 35.04 19.83 14.88 5.48
游戏服务器状态信息
字段 | 含义 |
---|---|
serverId | 服务器编号,从config配置表中的ID。 |
serverType | 服务器类型,同config配置表中的type。 |
pid | 服务器对应的进程PID |
rss | - |
heapTotal | 服务器使用堆内存总大小,单位MB。 |
heapUserd | 服务器已使用堆内存大小,单位为兆(MB)。 |
uptime | 服务器启动时长,单位为分钟。 |
关闭项目
$ cd test
$ pomelo stop [id]
pomelo stop
优雅地关闭各个服务器
pomelo stop id
会关闭指定ID的服务器,关闭特定服务器会导致服务器状态信息等丢失,建议先做好服务器状态信息的维护和备份。
杀死进程
$ cd test
$ pomelo kill [--force]
pomelo kill
会直接杀死项目进程,做法比较粗暴,安全性低,开发环境下可以使用,产品环境慎用。若有残留进程杀不干净,可添加--force
参数。
动态添加服务器
$ cd test
$ pomelo add host=[host] port=[port] id=[id] serverType=[serverType]
目前只支持后端服务器的动态添加
$ apt-get install sysstat
$ git clone https://github.com/NetEase/pomelo-admin-web.git
$ cd pomelo-admin-web
$ npm i -d
$ node app
$ vim config/admin.json
{
"host": "localhost",
"port": 3005,
"username": "monitor",
"password": "monitor"
}
Chrome浏览器访问:http://127.0.0.1:7001
动态语言面向对象有鸭子类型的概念,服务器的抽象也同样可以比喻为鸭子,服务器的对外接口只有两类:
handler和remote的行为决定了服务器长什么样子,只要定义好handler和remote两类的行为,就可以确定这个服务器的类型。
gate
connector
master
master加载配置文件,通过读取配置文件,启动所配置的服务器集群,并对所有服务器进行管理。
rpc
Pomelo中使用RPC调用进行进程间通信,在Pomelo中RPC调用分为两类:使用namespace
命令空间进行区分,namespace
为sys
的是系统RPC调用,它对用户来说是透明的,目前Pomelo中系统RPC调用有:
除了系统RPC调用之外,其余的由用户自定义的RPC调用属于user namespace
的RPC调用,需要用户自己完成RPC服务端remote
的handler
代码,并由RPC客户端显式地发起调用。
router
route是用来标识一个具体服务或客户端接受服务端推送消息的位置,对服务器来说,其形式一般是:chat.chatHandler.send,其中chat是服务器类型,chatHandler是chat服务器中定义的一个Handler,send则是这个Handler中的一个handler方法。
对客户端来说,其路由形式一般为onXXX,当服务端请求到达后,前端服务器会将用户客户端请求派发到后端服务器,这种派发需要一个路由函数router,可以粗略地认为router是根据用户的session以及其请求内容做一些运算后,将其映射到一个具体的应用服务器ID。
可以通过application的route调用给某一类型的服务器配置其router。若不配置的话,Pomelo会使用一个默认的router。
Pomelo默认的路由函数是使用session里面的uid字段,计算uid字段的crc32校验码,然后用这个校验码作为key,跟同类应用服务器数目取余,得到要路由到的服务器编号。注意这里有一个陷进,如果session没有绑定uid的话,此时uid字段为undefined,可能会造成所有请求都路由到同一台服务器,所以在实际开发中还是需要自己来配置router。
session
Session会话是指一个客户端连接的抽象,Pomelo框架中有三个session会话的概念分别是Session、FrontendSession、BackendSession。
session会话字段结构
{
id://readonly
frontendId: //readonly
uid: // readonly
settings: //read and write
__socket__:
__state__:
//...
}
字段 | 权限 | 描述 |
---|---|---|
id |
只读 | 当前session会话的ID,全局唯一,自增方式来生成。 |
frontendId |
只读 | 维护当前session会话的前端服务器的ID |
uid |
只读 | 当前session会话所绑定的用户ID |
settings |
读写 | 维护key-value map用来描述session会话的自定义属性 |
__socket__ |
只读 | 底层原生socket的引用 |
__state__ |
只读 | 用来指明当前session会话的生命周期状态 |
一个session会话一旦建立,那么id
、frontendId
、uid
、__socket__
、__state__
都是确定的,都应该是只读不可写的。而settings
也不应该被随意修改。因此在前端服务器中引入了FrontendSession可将其看作是一个内部session会话在前端服务器中的傀儡。
FrontendSession的字段结构
{
id: // readonly
frontendId: // readonly
uid: // readonly
settings: // read and write
}
FrontendSession的作用
settings
字段进行设置值,然后通过调用FrontendSession的push()
方法,将设置的settings
的值同步到原始的session会话中。bind
调用可以给session绑定uidservice
Pomelo框架有两个service服务:SessionService、BackendSessionService
SessionService维护所有的原始session信息,包括不可访问的字段,绑定的uid以及用户自定义的字段。
BackendSession与FrontendSession类似,BackendSession是用于后端服务器的,可以看作是原始session的代理,其数据字段跟FrontendSession基本一致。
BackendSession是由BackendSessionService创建并维护的,在后端服务器接收到请求后,由BackendSessionService根据前端服务器RPC的参数进行创建。
对BackendSessionService的每次方法调用实际上都会生成一个远程过程调用,比如通过一个sid获取其BackendSession。同样对于BackendSession中字段的修改也不会反映到原始的session中,不过与FrontendSession一样,BackendSession也有push、bind、unbind调用,它们的作用与FrontendSession一样都是用来修改原始中的settings字段或绑定/解绑uid的,不同的是BackendSession的这些调用实际上都是namespace为sys的远程调用。
Channel
channel可以看作是玩家ID的容器,主要用于需要广播推送消息的场景。
可以把玩家加入到一个channel中,当对这个channel推送消息时,所有加入到这个channel的玩家都会受到推送过来的消息。
一个玩家的ID可能会加入多个channel中,这样玩家就会受到其加入的channel推送过来的消息。
需要注意的时channel都时服务器本地的,应用服务器A和B并不会共享channel,也就时说在服务器A上创建的channel只能 由服务器A才能给它推送消息。
request response notify push
Pomelo中有四种消息类型的消息分别是request
、response
、notify
、push
request
请求到服务器,服务器处理后返回response
响应。notify
是客户端发给服务器的通知,也就是不需要服务器给与回复的请求。push
是服务器主动给客户端推送消息的类型filter
filter
分为before
和after
两类,每个filter
都可以注册多个形成一个filter
链,所有客户端请求都会经过filter
链进行处理。
before filter
会对请求做一些前置处理,如检查当前玩家是否已经登录,打印统计日志等。after filter
是进行请求后置处理的地方,比如释放请求上下文的资源,记录请求总耗时等。after filter
中不应该再出现修改响应内容的代码,因为在进入after filter
前响应就已经被发送给客户端。handler
handler
是实现具体业务逻辑的地方,在请求处理流程中,handler
位于before filter
和after filter
之间。
handler
的接口声明:
handler.methodName = function(msg, session, next){
//...
}
after filter的参数含义和before filter类似,handler处理完毕后,如果需要返回给客户端响应,可以将返回结果封装成JavaScript对象,通过next传递给后续流程。