Nodejs学习笔记(十六)--- Pomelo介绍入门
目录
- 前言&介绍
- 安装Pomelo
- 创建项目并启动
- 创建项目
- 项目结构说明
- 启动
- 测试连接
- 聊天服务器
- 新建gate和chat服务器
- 配置master.json
- 配置servers.json
- 配置adminServer.json
- 解决服务器分配问题
- 实现gate.gateHandler.queryEntry
- 实现chat服务器chatRemote.js
- 实现chat服务器chatHandler.js
- 实现connector中entryHandler.js
- 运行
- 编写web聊天客户端测试
- 写在之后
前言&介绍
Pomelo:一个快速、可扩展、Node.js分布式游戏服务器框架
从三四年前接触Node.js开始就接触到了Pomelo,从Pomelo最初的版本到现在,总的来说网易出品还算不错,但是发展不算快;用它做过一些项目和小游戏表现还不错。
用它的主要好处:
1. 入门简单,有比较丰富的文档和示例(虽然现在看版本也比较老了,但是入门没什么问题)
2.分布式多进程且扩展简单(单进程多线程,每个服务器都是一个Node进程,通过配置文件就可以管理集群)
3.可以不去关注底层和网络相关逻辑,聚焦业务逻辑的处理,对于有Web服务器开发经验却没有游戏服务器开发经验来说还是比较友好的
4.提供了很多工具和客户端支持(像IOS、Android & Java、Javascript、C、Cocos2d-x、U3D等)
......
入门参考链接
https://github.com/NetEase/pomelo/wiki/Home-in-Chinese
其它链接:
https://github.com/NetEase/pomelo
https://www.npmjs.com/package/pomelo
安装Pomelo
安装要求
Windows下安装要求环境
Python (2.5 < 版本 < 3)
VC++编译器
PS: Windows新环境自已检查一下,我本机环境已经装好python2.7,Visaul Studio也安装了所以也有VC++编译器
其它操作系统应该问题不大
官方安装介绍文档:https://github.com/NetEase/pomelo/wiki/%E5%AE%89%E8%A3%85pomelo
全局安装Pomelo
npm install pomelo -g
安装成功后如下图,可以看到现在最新版本为2.2.5

说明:Pomelo光是安装可能出现各种失败
1. 回头去检查一下,Python和VC++编辑器是否有问题
2.如果以前全局安装过Pomelo,最好删除掉 “C:\Users\当前用户\AppData\Roaming\npm\node_modules”目录下Pomelo文件夹和“C:\Users\当前用户\AppData\Roaming\npm-cache”目录下Pomelo开头的文件夹
3.如果并不报错,npm卡住不动,多数是网络原因,重复多安几次;或者打开翻墙工具试试;也可以用淘宝镜像 cnpm 安装
创建项目并启动
pomelo init 项目名
执行创建项目命令后,出现如下图选择项(Please select underly connector, 1 for websocket(native socket), 2 for socket.io, 3 for wss, 4 for socket.io(wss), 5 for udp, 6 for mqtt: [1])

这是让你选择connector的协议,除了5 for udp,其它都是长连接,我们接下来选择 2 for socket.io
在上图cmd中输入2,并回车,选择socket.io继续安装
这里connector协议可以通过app.js配置进行修改// app configuration
app.configure('production|development', 'connector', function(){app.set('connectorConfig',{connector : pomelo.connectors.sioconnector,
...
});
}); 成功后,转到项目根目录,执行安装项目执行 npm-install.bat 依赖项 (其它平台执行npm-install.sh)
cd 项目目录
npm-install.bat 项目创建完成后,目录如下图

game-server : 游戏服务器,所有游戏服务器功能和逻辑都在此目录下
game-server/app.js:入口文件
game-server/app: 存放游戏逻辑和功能相关代码都这个子目录下,servers目录下可以新建多个目录来创建不同类型的服务器,在pomelo中,使用路径来区分服务器类型
game-server/config:存放游戏服务器配置文件目录,像日志、服务器、数据库等几乎所有配置文件都可以存放到此目录下
game-server/logs:日志目录,存放游戏服务器所有日志文件
web-server: web服务器(如果你是个H5游戏,这里就是Web客户端,如果是IOS、Andriod客户端,这目录就没什么用)
shared:公共代码存放处,这里要以放一些共用代码
启动game-server
cd game-server
pomelo start 启动命令执行成功后,出现如下图错误提示

[2017-11-23 11:54:42.226] [ERROR] console - Option path is not valid. Please refer to the README. [2017-11-23 11:54:42.226] [ERROR] console - Option close timeout is not valid. Please refer to the README. [2017-11-23 11:54:42.226] [ERROR] console - Option heartbeats is not valid. Please refer to the README. [2017-11-23 11:54:42.226] [ERROR] console - Option log level is not valid. Please refer to the README.
问题原因和解决方式
原因:新版的socket.io用法不正确的导致的,官方早已修复,就是没有publish到npm包中
修复方式:把node_modules目录下的pomelo中sioconnector.js(../game-server/node_modules/pomelo/lib/connectors/sioconnector.js)
替换为 https://github.com/NetEase/pomelo/blob/master/lib/connectors/sioconnector.js
替换后再启动game-server,就没有这些错误提示了^_^!
1.启动web-server
cd web-server
node app 启动后如下图

会发些有一些提示,这是express写法问题,可以打开web-server根目录下app.js,按如下修改
//var app = express.createServer(); 注释掉这一行代码,替换为下面这一行代码 var app = express();
再启动时无express用法提示^_^!
2.打开http://localhost:3001

如上图,点击“Test Game Server”按钮,提示“game server is ok.”,则此项目game-server可用^_^!
聊天服务器
上面大体了解了pomelo,要入门还是以一个聊天服务器为入门示例最好,其它逻辑相对简单,入门学习不会因其它游戏逻辑影响。
官方有个非常好的示例:https://github.com/NetEase/chatofpomelo 官方也有很多说明
网上也有很多文章分析讲解这项目,我就不完全解释些项目了,接下来我就在上面新建的好的“PomeloDemo”的基础上改成一个聊天服务器
在app/servers目录下新建gate和chat服务器,新建好后目录如下

gate服务器:
在一般情况下用户量一台机器就可以支撑,但用户量多了就得扩充服务器,gate服务器的作用就相当于前端负载均衡服务器;
客户端向gate服务器发出请求,gate服务器会给客户端分配一个connector服务器;
分配策略是根据客户端的某一个key做hash得到connector的id,这样就可以实现各个connector服务器的负载均衡。这个一会儿会实现
connector服务器:
接受客户端请求,并将其路由到chat服务器,以及维护客户端的链接;
同时,接收客户端对后端服务器的请求,按照用户配置的路由策略,将请求路由给具体的后端服务器。当后端服务器处理完请求或者需要给客户端推送消息的时候,connector服务器同样会扮演一个中间角色,完成对客户端的消息发送;
connector服务器会同时拥有clientPort和port,其中clientPort用来监听客户端的连接,port端口用来给后端提供服务;
chat服务器:
handler和remote决定了服务器的行为;
handler接收用户发送过来的send请求,remote由connector RPC发起远程调用时调用;
在remote里由于涉及到用户的加入和退出,所以会有对channel的操作。
其实也可以提前了解一些Pomelo中的术语,不分别解释,可以提前看看:https://github.com/NetEase/pomelo/wiki/%E6%9C%AF%E8%AF%AD%E8%A7%A3%E9%87%8A
{"development": {"id": "master-server-1", "host": "127.0.0.1", "port": 15005},"production": {"id": "master-server-1", "host": "127.0.0.1", "port": 15005}
} master.json
打开config目录下servers.json文件,配置好各种 type 的服务器,配置如下
{"development":{"connector":[{"id":"connector-server-1", "host":"127.0.0.1", "port":14050, "clientPort": 13050, "frontend": true},{"id":"connector-server-2", "host":"127.0.0.1", "port":14051, "clientPort": 13051, "frontend": true},{"id":"connector-server-3", "host":"127.0.0.1", "port":14052, "clientPort": 13052, "frontend": true}],"chat":[{"id":"chat-server-1", "host":"127.0.0.1", "port":16050},{"id":"chat-server-2", "host":"127.0.0.1", "port":16051},{"id":"chat-server-3", "host":"127.0.0.1", "port":16052}],"gate":[{"id": "gate-server-1", "host": "127.0.0.1", "clientPort": 15014, "frontend": true}]},"production":{"connector":[{"id":"connector-server-1", "host":"127.0.0.1", "port":14050, "clientPort": 13050, "frontend": true},{"id":"connector-server-2", "host":"127.0.0.1", "port":14051, "clientPort": 13051, "frontend": true},{"id":"connector-server-3", "host":"127.0.0.1", "port":14052, "clientPort": 13052, "frontend": true}],"chat":[{"id":"chat-server-1", "host":"127.0.0.1", "port":16050},{"id":"chat-server-2", "host":"127.0.0.1", "port":16051},{"id":"chat-server-3", "host":"127.0.0.1", "port":16052}],"gate":[{"id": "gate-server-1", "host": "127.0.0.1", "clientPort": 15014, "frontend": true}]}
} servers.json 解释一下配置中的各字段:
id: 字符串类型的应用服务器ID
host:应用服务器的IP或者域名
port:RPC请求监听的端口
clientPort: 前端服务器的客户端请求的监听端口
frontend:bool类型,是否是前端服务器,默认: false
可选参数:
max-connections:前端服务器最大客户连接数
args: node/v8配置,如配置为"args": "--debug=5858 "这样就可以启用项目调试(没用过,临时问了一下谷歌,看别人是这么解释的^_^!)
打开config目录下adminServer.json文件,配置好各种 type 的服务器,配置如下
[{"type": "connector","token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
}, {"type": "chat","token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
},{"type": "gate","token": "agarxhqb98rpajloaxn34ga8xrunpagkjwlaw3ruxnpaagl29w4rxn"
}] adminServer.json 它有什么作用,可以看一下以下2个链接
http://blog.csdn.net/nynyvkhhiiii/article/details/49249915
https://github.com/NetEase/pomelo-admin#server-master-auth
从上面的servers.json配置的修改可以看出与最开始创建出来的项目一个服务器相比,connector和chat我都配置了三个服务器
这就要解决客户端请求服务器分配问题
解决思路:用户访问gate服务器,使用用户的uid的crc32的校验码与connector服务器的个数取余,从而得到一个connector服务器,把这个connector服务器分配给请求用户
在app目录下新建util目录,目录下新建“dispatcher.js”和 “routeUtil.js”文件,处理此服务器分配逻辑
var crc = require('crc');module.exports.dispatch = function(uid, connectors) {var index = Math.abs(crc.crc32(uid)) % connectors.length;return connectors[index]; };dispatcher.js
var exp = module.exports; var dispatcher = require('./dispatcher');exp.chat = function(session, msg, app, cb) {var chatServers = app.getServersByType('chat');if(!chatServers || chatServers.length === 0) {cb(new Error('can not find chat servers.'));return;}var res = dispatcher.dispatch(session.get('rid'), chatServers);cb(null, res.id); };routeUtil.js
准备好这些文件后,在game-server服务器入口文件app.js中添加配配置
var pomelo = require('pomelo'); var routeUtil = require('./app/util/routeUtil');/*** Init app for client.*/ var app = pomelo.createApp(); app.set('name', 'PomeloDemo');// app configuration // app.configure('production|development', 'connector', function(){ app.configure('production|development', function(){// route configuresapp.route('chat', routeUtil.chat);app.set('connectorConfig',{connector : pomelo.connectors.sioconnector,// 'websocket', 'polling-xhr', 'polling-jsonp', 'polling'transports : ['websocket', 'polling'],heartbeats : true,closeTimeout : 60 * 1000,heartbeatTimeout : 60 * 1000,heartbeatInterval : 25 * 1000});// filter configures app.filter(pomelo.timeout()); });// start app app.start();process.on('uncaughtException', function (err) {console.error(' Caught exception: ' + err.stack); });app.filter(pomelo.timeout()); 过滤器,pomelo内置了一些过滤器,可以自行去了解一下,也可以根据自已的需求去自定义!
注意:
app.configure('production|development', 'connector', function(){
修改为
app.configure('production|development', function(){
这个如果不修改,在启动调用时会遇到 engine.io 中报错 TypeError: Cannot read property 'indexOf' of undefined at Server.verify !
6.实现 gate.gateHandler.queryEntry
作用:用户连接gate服务器,返回分配的connector
在gate目录下handler下新建gateHandler.js,代码如下
var dispatcher = require('../../../util/dispatcher');module.exports = function(app) {return new Handler(app); };var Handler = function(app) {this.app = app; };var handler = Handler.prototype;/*** Gate handler that dispatch user to connectors.** @param {Object} msg message from client* @param {Object} session* @param {Function} next next stemp callback**/ handler.queryEntry = function(msg, session, next) {var uid = msg.uid;if(!uid) {next(null, {code: 500});return;}// get all connectorsvar connectors = this.app.getServersByType('connector');if(!connectors || connectors.length === 0) {next(null, {code: 500});return;}// select connectorvar res = dispatcher.dispatch(uid, connectors);next(null, {code: 200,host: res.host,port: res.clientPort}); };gateHandler.js
chat服务器会接受connector的远程调用,完成channel维护中的用户的加入以及离开
module.exports = function(app) {return new ChatRemote(app); };var ChatRemote = function(app) {this.app = app;this.channelService = app.get('channelService'); };/*** Add user into chat channel.** @param {String} uid unique id for user* @param {String} sid server id* @param {String} name channel name* @param {boolean} flag channel parameter**/ ChatRemote.prototype.add = function(uid, sid, name, flag, cb) {var channel = this.channelService.getChannel(name, flag);var username = uid.split('*')[0];var param = {route: 'onAdd',user: username};channel.pushMessage(param);if( !! channel) {channel.add(uid, sid);}cb(this.get(name, flag)); };/*** Get user from chat channel.** @param {Object} opts parameters for request* @param {String} name channel name* @param {boolean} flag channel parameter* @return {Array} users uids in channel**/ ChatRemote.prototype.get = function(name, flag) {var users = [];var channel = this.channelService.getChannel(name, flag);if( !! channel) {users = channel.getMembers();}for(var i = 0; i < users.length; i++) {users[i] = users[i].split('*')[0];}return users; };/*** Kick user out chat channel.** @param {String} uid unique id for user* @param {String} sid server id* @param {String} name channel name**/ ChatRemote.prototype.kick = function(uid, sid, name, cb) {var channel = this.channelService.getChannel(name, false);// leave channelif( !! channel) {channel.leave(uid, sid);}var username = uid.split('*')[0];var param = {route: 'onLeave',user: username};channel.pushMessage(param);cb(); };chatRemote.js
可以看到上面代码中的add和kick分别对应着加入和离开channel
chat服务器执行聊天逻辑,维护channel信息,一个房间就是一个channel,一个channel里有多个用户,当有用户发起聊天的时候,就会将其内容广播到整个channel。
var chatRemote = require('../remote/chatRemote');module.exports = function(app) {return new Handler(app); };var Handler = function(app) {this.app = app; };var handler = Handler.prototype;/*** Send messages to users** @param {Object} msg message from client* @param {Object} session* @param {Function} next next stemp callback**/ handler.send = function(msg, session, next) {var rid = session.get('rid');var username = session.uid.split('*')[0];var channelService = this.app.get('channelService');var param = {route: 'onChat',msg: msg.content,from: username,target: msg.target};channel = channelService.getChannel(rid, false);//the target is all usersif(msg.target == '*') {channel.pushMessage(param);}//the target is specific userelse {var tuid = msg.target + '*' + rid;var tsid = channel.getMember(tuid)['sid'];channelService.pushMessageByUids(param, [{uid: tuid,sid: tsid}]);}next(null, {route: msg.route}); };chatHandler.js
这里面是发送消息(给房间内所有人和指定用户)
主要完成接受客户端的请求,维护与客户端的连接,路由客户端的请求到chat服务器;
module.exports = function(app) {return new Handler(app); };var Handler = function(app) {this.app = app; };var handler = Handler.prototype;/*** New client entry chat server.** @param {Object} msg request message* @param {Object} session current session object* @param {Function} next next stemp callback* @return {Void}*/ handler.enter = function(msg, session, next) {var self = this;var rid = msg.rid;var uid = msg.username + '*' + ridvar sessionService = self.app.get('sessionService');//duplicate log inif( !! sessionService.getByUid(uid)) {next(null, {code: 500,error: true });return;}session.bind(uid);session.set('rid', rid);session.push('rid', function(err) {if(err) {console.error('set rid for session service failed! error is : %j', err.stack);}});session.on('closed', onUserLeave.bind(null, self.app));//put user into channelself.app.rpc.chat.chatRemote.add(session, uid, self.app.get('serverId'), rid, true, function(users){next(null, {users:users});}); };/*** User log out handler** @param {Object} app current application* @param {Object} session current session object**/ var onUserLeave = function(app, session) {if(!session || !session.uid) {return;}app.rpc.chat.chatRemote.kick(session, session.uid, app.get('serverId'), session.get('rid'), null); };entryHandler.js
这里完成的主要就是RPC远程调用chat服务器chatRemote中的实现
到此这个聊天服务器实现就完成, 打开命令行工具,执行没有错误信息,基本就成功了!
cd game-server目录
pomelo start
编写web聊天客户端测试
我就在web-server目录中写了个测试客户端
把结构改了一下,换成了ejs模版,代码如下
routes中index.js文件代码
var express = require('express'); var router = express.Router();router.get('/', function (req, res, next) {res.render('index', { title: 'Nodejs学习笔记(十六)--- Pomelo介绍&入门' }); });module.exports = router;index.js
views中index.ejs文件代码
<%= title %> 1.登录(连接Gate服务器)
用户名:房间号:
2.聊天室
用户名:房间号:
发送给:
app.js代码如下:
var express = require('express'); var path = require('path'); var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser');var app = express();var index = require('./routers/index.js');// views engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs');app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public')));app.use('/', index);app.use(function(req, res, next) {var err = new Error('Not Found');err.status = 404;res.render('404'); });if (app.get('env') === 'development') {app.use(function(err, req, res, next) {console.log(err);res.status(err.status || 500);res.render('500', {message: err.message,error: err});}); }app.use(function(err, req, res, next) {console.log(err);res.status(err.status || 500);res.render('500', {message: err.message,error: {}}); });app.listen(10111, function () {console.log("You can debug your app test with http://127.0.0.1:10111"); });module.exports = app;app.js
运行起来后,测试结果如下图:

写在之后
Pomelo学习入门不算复杂,写一篇感觉讲不全,写多篇感觉太散,大家将就着看,有些东西不认识的还是去看一下API文档 http://pomelo.netease.com/api.html (也是低水准的官方API^_^!)
或者问一下google啥的...
可以参考这两个例子来学习:
https://github.com/NetEase/chatofpomelo
https://github.com/NetEase/lordofpomelo
入门建议从chatofpomelo开始
看之前可以提前看看一些pomelo术语,有个大体了解,再边看代码边理解:https://github.com/NetEase/pomelo/wiki/%E6%9C%AF%E8%AF%AD%E8%A7%A3%E9%87%8A
主要参考资料:
https://github.com/NetEase/pomelo/wiki/Home-in-Chinese (比较杂乱,可能官方大神都忙着搞赚钱的项目,将就着看,有很多东西对入门来说还是很有用的)
如果有些问题解决不了,可以去社区问一下:http://nodejs.netease.com/tag/pomelo (感觉现在活跃度也比较低^_^!)
转载于:https://www.cnblogs.com/zhongweiv/p/nodejs_pomelo.html
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
