http://www.infoq.com/cn/articles/game-server-development-2
在上一篇文章中, 我們介紹了游戲服務(wù)器的基本架構(gòu)、相關(guān)框架和Node.js開發(fā)游戲服務(wù)器的優(yōu)勢。本文我們將通過聊天服務(wù)器的設(shè)計(jì)與開發(fā),來更深入地理解pomelo開發(fā)應(yīng)用的基本流程、開發(fā)思路與相關(guān)的概念。本文并不是開發(fā)聊天服務(wù)器的tutorial,如果需要tutorial和源碼可以看文章最后的參考資料。
為什么是聊天服務(wù)器?
我們目標(biāo)是搭建游戲服務(wù)器,為什么從聊天開始呢?
聊天可認(rèn)為是簡化的實(shí)時(shí)游戲,它與游戲服務(wù)器有著很多共通之處,如實(shí)時(shí)性、頻道、廣播等。由于游戲在場景管理、客戶端動(dòng)畫等方面有一定的復(fù)雜性,并不適合作為pomelo的入門應(yīng)用。聊天應(yīng)用通常是Node.js入門接觸的第一個(gè)應(yīng)用,因此更適合做入門教程。
Pomelo是游戲服務(wù)器框架,本質(zhì)上也是高實(shí)時(shí)、可擴(kuò)展、多進(jìn)程的應(yīng)用框架。除了在library中有一部分游戲?qū)S玫膸欤溆嗖糠挚蚣芡耆捎糜陂_發(fā)高實(shí)時(shí)web應(yīng)用。而且與現(xiàn)在有的Node.js高實(shí)時(shí)應(yīng)用框架如derby、socketstream、meteor等比起來有更好的可伸縮性。
相關(guān)贊助商
QCon北京2016大會(huì),4月21-23日,北京·國際會(huì)議中心,精彩內(nèi)容邀您參與!
對于大多數(shù)開發(fā)者而言,Node.js的入門應(yīng)用都是一個(gè)基于socket.io開發(fā)的普通聊天室, 由于它是基于單進(jìn)程的Node.js開發(fā)的, 在可擴(kuò)展性上打了一定折扣。例如要擴(kuò)展到類似irc那樣的多頻道聊天室, 頻道數(shù)量的增多必然會(huì)導(dǎo)致單進(jìn)程的Node.js支撐不住。
而基于pomelo框架開發(fā)的聊天應(yīng)用天生就是多進(jìn)程的,可以非常容易地?cái)U(kuò)展服務(wù)器類型和數(shù)量。
從單進(jìn)程到多進(jìn)程,從socket.io到pomelo
一個(gè)基于socket.io的原生聊天室應(yīng)用架構(gòu), 以uberchat為例。
它的應(yīng)用架構(gòu)如下圖所示:

服務(wù)端由單個(gè)Node.js進(jìn)程組成的chat server來接收websocket請求。
它有以下缺點(diǎn):
可擴(kuò)展性差:只支持單進(jìn)程的Node.js, 無法根據(jù)room/channel分區(qū), 也無法將廣播的壓力與處理邏輯的壓力分開。
代碼量大:基于socket.io做了簡單封裝,服務(wù)端就寫了約430行代碼。
用pomelo來寫這個(gè)框架可完全克服以上缺點(diǎn),并且代碼量只要區(qū)區(qū)100多行。
我們要搭建的pomelo聊天室具有如下的運(yùn)行架構(gòu):

在這個(gè)架構(gòu)里, 前端服務(wù)器也就是connector專門負(fù)責(zé)承載連接, 后端的聊天服務(wù)器則是處理具體邏輯的地方。 這樣擴(kuò)展的運(yùn)行架構(gòu)具有如下優(yōu)勢: * 負(fù)載分離:這種架構(gòu)將承載連接的邏輯與后端的業(yè)務(wù)處理邏輯完全分離,這樣做是非常必要的, 尤其是廣播密集型應(yīng)用(例如游戲和聊天)。密集的廣播與網(wǎng)絡(luò)通訊會(huì)占掉大量的資源,經(jīng)過分離后業(yè)務(wù)邏輯的處理能力就不再受廣播的影響。
切換簡便:因?yàn)橛辛饲啊⒑蠖藘蓪拥募軜?gòu),用戶可以任意切換頻道或房間都不需要重連前端的websocket。
擴(kuò)展性好:用戶數(shù)的擴(kuò)展可以通過增加connector進(jìn)程的數(shù)量來支撐。頻道的擴(kuò)展可以通過哈希等算法負(fù)載均衡到多臺(tái)聊天服務(wù)器上。理論上這個(gè)架構(gòu)可以實(shí)現(xiàn)頻道和用戶的無限擴(kuò)展。
聊天服務(wù)器開發(fā)架構(gòu)
game server與web server
聊天服務(wù)器項(xiàng)目中分生成了game-server目錄、web-server目錄與shared目錄,如下圖所示:

這樣也將應(yīng)用天然地隔離成了兩個(gè),game server與web server。
- Game server, 即游戲服務(wù)器,所有的游戲服務(wù)器邏輯都在里實(shí)現(xiàn)。客戶端通過websocket(0.3版會(huì)支持tcp的socket)連到game server。game-server/app.js是游戲服務(wù)器的運(yùn)行入口。
- Web server,即web服務(wù)器, 也可以認(rèn)為是游戲服務(wù)器的一個(gè)web客戶端, 所有客戶端的js代碼,web端的html、css資源都存放在這里,web服務(wù)端的用戶登錄、認(rèn)證等功能也在這里實(shí)現(xiàn)。pomelo也提供了其它客戶端,包括ios、android、unity3D等。
- Shared目錄,假如客戶端是web,由于服務(wù)端和客戶端都是javascript寫的,這時(shí)Node.js的代碼重用優(yōu)勢就體現(xiàn)出來了。shared目錄下可以存放客戶端、服務(wù)端共用的常量、算法。真正做到一遍代碼, 前后端共用。
服務(wù)器定義與應(yīng)用目錄
Game server才是游戲服務(wù)器的真正入口,游戲邏輯都在里, 我們簡單看一下game-server的目錄結(jié)構(gòu),如下圖所示:

servers目錄下所有子目錄定義了各種類型的服務(wù)器,而每個(gè)servers目錄下基本都包含了handler和remote兩個(gè)目錄。 這是pomelo的創(chuàng)新之處,用極簡的配置實(shí)現(xiàn)游戲服務(wù)器的定義,后文會(huì)解釋handler和remote。
通過pomelo,游戲開發(fā)者可以自由地定義自己的服務(wù)器類型,分配和管理進(jìn)程資源。在pomelo中,根據(jù)服務(wù)器的職責(zé)不同,服務(wù)器主要分為前端服務(wù)器(frontend)和后端服務(wù)器(backend)兩大類。其中,前端服務(wù)器負(fù)責(zé)承載客戶端的連接和維護(hù)session信息,所有服務(wù)器與客戶端的消息都會(huì)經(jīng)過前端服務(wù)器;后端服務(wù)器負(fù)責(zé)接收前端服務(wù)器分發(fā)過來的請求,實(shí)現(xiàn)具體的游戲邏輯,并把消息回推給前端服務(wù)器,最后發(fā)送給客戶端。如下圖所示:

動(dòng)態(tài)語言的面向?qū)ο笥袀€(gè)基本概念叫鴨子類型。在pomelo中,服務(wù)器的抽象也同樣可以比喻為鴨子,服務(wù)器的對外接口只有兩類, 一類是接收客戶端的請求, 叫做handler, 一類是接收RPC請求, 叫做remote, handler和remote的行為決定了服務(wù)器長什么樣子。 因此開發(fā)者只需要定義好handler和remote兩類的行為, 就可以確定這個(gè)服務(wù)器的類型。 例如chat服務(wù)器目前的行為只有兩類,分別是定義在handler目錄中的chatHandler.js,和定義在remote目錄中的chatRemote.js。只要定義好這兩個(gè)類的方法,聊天服務(wù)器的對外接口就確定了。
搭建聊天服務(wù)器
準(zhǔn)備知識
pomelo的客戶端服務(wù)器通訊
pomelo的客戶端和服務(wù)器之間的通訊可以分為三種:
request-response
pomelo中最常用的就是request-response模式,客戶端發(fā)送請求,服務(wù)器異步響應(yīng)。客戶端的請求發(fā)送形式類似ajax類似:
``` pomelo.request(url, msg, function(data){}); ```
第一個(gè)參數(shù)為請求地址,完整的請求地址主要包括三個(gè)部分:服務(wù)器類型、服務(wù)端相應(yīng)的文件名及對應(yīng)的方法名。第二個(gè)參數(shù)是消息體,消息體為json格式,第三個(gè)參數(shù)是回調(diào)函數(shù),請求的響應(yīng)將會(huì)把結(jié)果置入這個(gè)回調(diào)函數(shù)中返回給客戶端。
notify
notify與request—response類似,唯一區(qū)別是客戶端只負(fù)責(zé)發(fā)送消息到服務(wù)器,客戶端不接收服務(wù)器的消息響應(yīng)。
``` pomelo.notify(url, msg); ```
push
push則是服務(wù)器主動(dòng)向客戶端進(jìn)行消息推送,客戶端根據(jù)路由信息進(jìn)行消息區(qū)分,轉(zhuǎn)發(fā)到后。通常游戲服務(wù)器都會(huì)發(fā)送大量的這類廣播。
``` pomelo.on(route, function(data){}); ```
以上是javascript的api, 其它客戶端的API基本與這個(gè)類型。由于API與ajax極其類似,所有web應(yīng)用的開發(fā)者對此都不陌生。
session介紹
與web服務(wù)器類似,session是游戲服務(wù)器存放用戶會(huì)話的抽象。但與web不同,游戲服務(wù)器的session是基于長連接的, 一旦建立就一直保持。這反而比web中的session更直接,也更簡單。 由于長連接的session不會(huì)web應(yīng)用一樣由于連接斷開重連帶來session復(fù)制之類的問題,簡單地將session保存在前端服務(wù)器的內(nèi)存中是明智的選擇。
在pomelo中session也是key/value對象,其主要作用是維護(hù)當(dāng)前用戶信息,例如:用戶的id,所連接的前端服務(wù)器id等。session由前端服務(wù)器維護(hù),前端服務(wù)器在分發(fā)請求給后端服務(wù)器時(shí),會(huì)復(fù)制session并連同請求一起發(fā)送。任何直接在session上的修改,只對本服務(wù)器進(jìn)程生效,并不會(huì)影響到用戶的全局狀態(tài)信息。如需修改全局session里的狀態(tài)信息,需要調(diào)用前端服務(wù)器提供的RPC服務(wù)。
channel與廣播
廣播在游戲中是極其重要的,幾乎大部分的消息都需要通過廣播推送到客戶端,再由客戶端播放接收的消息。而channel則是服務(wù)器端向客戶端進(jìn)行消息廣播的通道。 可以把channel看成一個(gè)用戶id的容器.把用戶id加入到channel中成為當(dāng)中的一個(gè)成員,之后向channel推送消息,則該channel中所有的成員都會(huì)收到消息。channel只適用于服務(wù)器進(jìn)程本地,即在服務(wù)器進(jìn)程A創(chuàng)建的channel和在服務(wù)器進(jìn)程B創(chuàng)建的channel是兩個(gè)不同的channel,相互不影響。
服務(wù)器之間RPC通訊
從之前的文章可以了解到,在pomelo中,游戲服務(wù)器其實(shí)是一個(gè)多進(jìn)程相互協(xié)作的環(huán)境。各個(gè)進(jìn)程之間通信,主要是通過底層統(tǒng)一的RPC框架來實(shí)現(xiàn),服務(wù)器間的RPC調(diào)用也實(shí)現(xiàn)了零配置。具體RPC調(diào)用的代碼如下:
```javascript app.rpc.chat.chatRemote.add(session, uid, app.get ('serverId'), function(data){}); ```
其中app是pomelo的應(yīng)用對象,app.rpc表明了是前后臺(tái)服務(wù)器的Remote rpc調(diào)用,后面的參數(shù)分別代表服務(wù)器的名稱、對應(yīng)的文件名稱及方法名。為了實(shí)現(xiàn)這個(gè)rpc調(diào)用,則只需要在對應(yīng)的chat/remote/中新建文件chatRemote.js,并實(shí)現(xiàn)add方法。
聊天室流程概述
下圖列出了聊天室進(jìn)行聊天的完整流程:

通過以上流程, 我們可以看到pomelo的基本請求流程和用法。本文不是聊天室的tutorial,因此下面列出的代碼不是完整的,而是用極簡的代碼來說明pomelo的使用流程和api。
進(jìn)入聊天室
客戶端向前端服務(wù)器發(fā)起登錄請求:
```javascript pomelo.request('connector.entryHandler.enter', {user:userInfo}, function(){}); ```
用戶進(jìn)入聊天室后,服務(wù)器端首先需要完成用戶的session注冊同時(shí)綁定用戶離開事件:
```javascript session.bind(uid); session.on('closed', onUserLeave.bind(null, app)); ```
另外,服務(wù)器端需要通過調(diào)用rpc方法將用戶加入到相應(yīng)的channel中;同時(shí)在rpc方法中,服務(wù)器端需要將該用戶的上線消息廣播給其他用戶,最后服務(wù)器端向客戶端返回當(dāng)前channel中的用戶列表信息。
```javascript app.rpc.chat.chatRemote.add(session, uid, serverId,function(){}); ```
發(fā)起聊天
客戶端向服務(wù)端發(fā)起聊天請求,請求消息包括聊天內(nèi)容,發(fā)送者和發(fā)送目標(biāo)信息。消息的接收者可以聊天室里所有的用戶,也可以是某一特定用戶。
服務(wù)器端根據(jù)客戶端的發(fā)送的請求,進(jìn)行不同形式的消息廣播。如果發(fā)送目標(biāo)是所有用戶,服務(wù)器端首先會(huì)選擇channel中的所有用戶,然后向channel發(fā)送消息,最后前端服務(wù)器就會(huì)將消息分別發(fā)送給channel中取到的用戶;如果發(fā)送目標(biāo)只是某一特定用戶,發(fā)送過程和之前完全一樣,只是服務(wù)器端首先從channel中選擇的只是一個(gè)用戶,而不是所有用戶。
```javascript if(msg.target == '*') channel.pushMessage(param); else channelService.pushMessageByUids(param, [{uid:uid, sid:sid}]); ```
接收聊天消息
客戶端接收廣播消息,并將消息并顯示即可。
```javascript pomelo.on('onChat', function() { addMessage(data.from, data.target, data.msg); $("#chatHistory").show(); }); ```
退出聊天室
用戶在退出聊天室時(shí),必須完成一些清理工作。在session斷開連接時(shí),通過rpc調(diào)用將用戶從channel中移除。在用戶退出前,還需要將自己下線的消息廣播給所有其他用戶。
```javascript app.rpc.chat.chatRemote.kick(session, uid, serverId, channelName, null); ```
聊天服務(wù)器的可伸縮性與擴(kuò)展討論
上一講已經(jīng)談到pomelo在提供了一個(gè)高可伸縮性的運(yùn)行架構(gòu),對于聊天服務(wù)器同樣如此。如果想從單頻道聊天室擴(kuò)展到多頻道聊天室,增加的代碼幾乎為零。大部分工作只是在進(jìn)行服務(wù)器和路由的配置。對于服務(wù)器配置只需要修改json配置文件即可,而對于路由配置只需要增加一個(gè)路由算法即可。在pomelo中,開發(fā)者可以自己配置客戶端到服務(wù)器的路由規(guī)則,這樣會(huì)使得游戲開發(fā)更加靈活。
我們來看一下配置json文件對服務(wù)器運(yùn)行架構(gòu)的影響:
- 最簡服務(wù)器與運(yùn)行架構(gòu):

- 擴(kuò)展后的服務(wù)器與運(yùn)行架構(gòu):

另外,在0.3版本的pomelo中增加了動(dòng)態(tài)增刪服務(wù)器的功能,開發(fā)者可以在不停服務(wù)的情況下根據(jù)當(dāng)前應(yīng)用運(yùn)行的負(fù)載情況添加新的服務(wù)器或者停止閑置的服務(wù)器,這樣可以讓服務(wù)器資源得到更充分的利用。
總結(jié)
本文通過聊天服務(wù)器的搭建過程,分析了pomelo開發(fā)應(yīng)用的基本流程,基本架構(gòu)與相關(guān)概念。有了這些知識我們可以輕松地使用pomelo搭建高實(shí)時(shí)應(yīng)用了。 在后文中我們將分析更復(fù)雜的游戲案例,并且會(huì)對架構(gòu)中的一些實(shí)現(xiàn)深入剖析。
相關(guān)資源: