Pomelo介紹&入門
目錄
前言&介紹
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卡住不動,多數是網絡原因,重復多安幾次;或者打開FQ工具試試;也可以用淘寶鏡像 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
master.json
打開config目錄下servers.json文件,配置好各種 type 的服務器,配置如下
servers.json
解釋一下配置中的各字段:
id: 字符串類型的應用服務器ID
host:應用服務器的IP或者域名
port:RPC請求監聽的端口
clientPort: 前端服務器的客戶端請求的監聽端口
frontend:bool類型,是否是前端服務器,默認: false
可選參數:
max-connections:前端服務器最大客戶連接數
args: node/v8配置,如配置為"args": "--debug=5858 "這樣就可以啟用項目調試(沒用過,臨時問了一下谷歌,看別人是這么解釋的^_^!)
打開config目錄下adminServer.json文件,配置好各種 type 的服務器,配置如下
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”文件,處理此服務器分配邏輯
dispatcher.js
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 configures
app.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.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,代碼如下
gateHandler.js
chat服務器會接受connector的遠程調用,完成channel維護中的用戶的加入以及離開
chatRemote.js
可以看到上面代碼中的add和kick分別對應着加入和離開channel
chat服務器執行聊天邏輯,維護channel信息,一個房間就是一個channel,一個channel里有多個用戶,當有用戶發起聊天的時候,就會將其內容廣播到整個channel。
chatHandler.js
這里面是發送消息(給房間內所有人和指定用戶)
主要完成接受客戶端的請求,維護與客戶端的連接,路由客戶端的請求到chat服務器;
entryHandler.js
這里完成的主要就是RPC遠程調用chat服務器chatRemote中的實現
到此這個聊天服務器實現就完成, 打開命令行工具,執行沒有錯誤信息,基本就成功了!
cd game-server目錄 pomelo start
編寫web聊天客戶端測試
我就在web-server目錄中寫了個測試客戶端
把結構改了一下,換成了ejs模版,代碼如下
routes中index.js文件代碼
index.js
views中index.ejs文件代碼
<html>
<head><title><%= title %></title></head>
<body>
<div id="tipMsg" style="color:red; height:30px;"></div>
<div id="pnlLogin">
<h1>1.登錄(連接Gate服務器)</h1>
用戶名:<input id="txtUserName" type="text" maxlength="20" ></br>
房間號:<input id="txtRoomId" type="text" maxlength="8" >
<input id="btnLogin" type="button" value="點擊登錄" />
<br/>
</div>
<div id="pnlChat" style="display:none;">
<h1>2.聊天室</h1>
用戶名:<span id="spUserName" style="color:blue;padding-right:50px;"></span>
房間號:<span id="spRoomId" style="color:blue;padding-right:50px;"></span>
<div id="txtMessage" style="width:800px; height:400px;border:1px solid #000; overflow-y:auto; overflow-x:hidden; "></div>
<br/>
發送給:<select id="selUserList" style="width:200px;">
<option value="*">所有人</option>
</select>
<br/>
<br/>
<textarea id="txtSendMessage" type="text" style="width:690px;height:80px; overflow:auto;float:left;" ></textarea>
<input id="btnSend" type="button" value="發送" style="margin-left:10px; height:80px; width:100px;float:left;" />
</div>
</body>
</html>
<script src="js/socket.io.js"></script>
<script src="js/pomeloclient.js"></script>
<script src="js/jquery-1.11.2.min.js"></script>
<script type="text/javascript">
$(function(){
var rid = '';
var uname = '';
//監聽"onAdd", 當有新用戶加入時觸發
pomelo.on('onAdd', function(data) {
var user = data.user;
$('#txtMessage').append('<span style="color:green;">[上線提醒]:歡迎 ' + user + ' 加入聊天室<span><br/>');
//添加到用戶列表
$('#selUserList').append('<option value="' + user + '">' + user + '</option>');
});
//監聽"onLeave", 當有用戶離開聊天室時觸發
pomelo.on('onLeave', function(data) {
var user = data.user;
$('#txtMessage').append('<span style="color:green;">[離線提醒]: ' + user + ' 離開聊天室<span><br/>');
//從用戶列表移除
$('#selUserList option[value="' + user + '"]').remove();
});
// 監聽"onChat", 接收消息
pomelo.on('onChat', function(data) {
var from = data.from,
target = data.target,
msg = data.msg;
if(msg === null) return;
var name = (target == '*' ? '所有人' : target);
var time = getNowFormatDate();
$("#txtMessage").append('<span style="color:blue;">[' + time +'][' + from + '] 對 [' + name + '] 說: ' + msg + '<span><br/>');
});
//當從聊天斷開時
pomelo.on('disconnect', function(reason) {
$('#pnlLogin').show();
$('#pnlChat').hide();
});
//登錄
$('#btnLogin').on('click', function(){
var userNameReg = /^[a-zA-Z0-9]+$/,
roomIdReg = /^[0-9]+$/;
uname = $.trim($('#txtUserName').val()),
rid = $.trim($('#txtRoomId').val());
if(uname.length == 0){
alert('請輸入登錄名');
return false;
}
if(!userNameReg.test(uname)) {
alert('登錄名只能由字母或數字組成');
return false;
}
if(rid.length == 0){
alert('請輸入房間號');
return false;
}
if(!roomIdReg.test(rid)) {
alert('房間號只能是數字');
return false;
}
queryEntry(uname, function(host, port){
$('#tipMsg').append('Gate 連接成功! host:' + host + ' port:' + port + '<br/>');
//連接聊天服務器
pomelo.init({
host: host,
port: port,
log: true
}, function() {
var route = "connector.entryHandler.enter";
pomelo.request(route, {
username: uname,
rid: rid
}, function(data) {
if(data.error) {
$('#tipMsg').append('Chat 連接失敗!<br/>');
return;
}
$('#tipMsg').append('Chat 連接成功!<br/>');
$('#pnlLogin').hide();
$('#pnlChat').show();
$('#spUserName').text(uname);
$('#spRoomId').text(rid);
//加載當前聊天室 已在線用戶列表
$.each(data.users, function(i, item){
if(item != uname){
$('#selUserList').append('<option value="' + item + '">' + item + '</option>');
}
});
});
});
});
});
//發送消息
$('#btnSend').on('click', function(){
var route = "chat.chatHandler.send",
target = $("#selUserList").val(),
msg = $.trim($("#txtSendMessage").val());
if(msg.length == 0){
alert('不能發送空消息!');
return;
}
pomelo.request(route, {
rid: rid,
content: msg,
from: uname,
target: target
}, function(data) {
$("#txtSendMessage").val('');
if(from == uname) {
var name = (target == '*' ? '所有人' : target);
var time = getNowFormatDate();
$("#txtMessage").append('<span style="color:blue;">[' + time +'][' + from + '] 對 [' + name + '] 說: ' + msg + '<span><br/>');
}
});
});
})
//連接Gate服務器
function queryEntry(uid, callback) {
var route = 'gate.gateHandler.queryEntry';
pomelo.init({
host: '127.0.0.1',
port: 15014,
log: true
}, function() {
pomelo.request(route, {
uid: uid
}, function(data) {
pomelo.disconnect();
if(data.code === 500) {
alert('用戶名在此房間中已存在,請重新輸入新的用戶名!');
return;
}
callback(data.host, data.port);
});
});
};
function getNowFormatDate() {
var date = new Date();
var seperator1 = "-";
var seperator2 = ":";
var month = date.getMonth() + 1;
var strDate = date.getDate();
if (month >= 1 && month <= 9) {
month = "0" + month;
}
if (strDate >= 0 && strDate <= 9) {
strDate = "0" + strDate;
}
var currentdate = date.getFullYear() + seperator1 + month + seperator1 + strDate
+ " " + date.getHours() + seperator2 + date.getMinutes()
+ seperator2 + date.getSeconds();
return currentdate;
}
</script>
app.js代碼如下:
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 (感覺現在活躍度也比較低^_^!)

