在上一篇文章中, 我們介紹了游戲服務器的基本架構、相關框架和Node.js開發游戲服務器的優勢。本文我們將通過聊天服務器的設計與開發,來更深入地理解pomelo開發應用的基本流程、開發思路與相關的概念。本文并不是開發聊天服務器的tutorial,如果需要tutorial和源碼可以看文章最后的參考資料。
為什么是聊天服務器?
我們目標是搭建游戲服務器,為什么從聊天開始呢?
聊天可認為是簡化的實時游戲,它與游戲服務器有著很多共通之處,如實時性、頻道、廣播等。由于游戲在場景管理、客戶端動畫等方面有一定的復雜性,并不適合作為pomelo的入門應用。聊天應用通常是Node.js入門接觸的第一個應用,因此更適合做入門教程。
Pomelo是游戲服務器框架,本質上也是高實時、可擴展、多進程的應用框架。除了在library中有一部分游戲專用的庫,其余部分框架完全可用于開發高實時web應用。而且與現在有的Node.js高實時應用框架如derby、socketstream、meteor等比起來有更好的可伸縮性。
對于大多數開發者而言,Node.js的入門應用都是一個基于socket.io開發的普通聊天室, 由于它是基于單進程的Node.js開發的, 在可擴展性上打了一定折扣。例如要擴展到類似irc那樣的多頻道聊天室, 頻道數量的增多必然會導致單進程的Node.js支撐不住。
而基于pomelo框架開發的聊天應用天生就是多進程的,可以非常容易地擴展服務器類型和數量。
從單進程到多進程,從socket.io到pomelo
一個基于socket.io的原生聊天室應用架構, 以uberchat為例。
它的應用架構如下圖所示:
服務端由單個Node.js進程組成的chat server來接收websocket請求。
它有以下缺點:
可擴展性差:只支持單進程的Node.js, 無法根據room/channel分區, 也無法將廣播的壓力與處理邏輯的壓力分開。
代碼量大:基于socket.io做了簡單封裝,服務端就寫了約430行代碼。
用pomelo來寫這個框架可完全克服以上缺點,并且代碼量只要區區100多行。
我們要搭建的pomelo聊天室具有如下的運行架構:
在這個架構里, 前端服務器也就是connector專門負責承載連接, 后端的聊天服務器則是處理具體邏輯的地方。 這樣擴展的運行架構具有如下優勢: * 負載分離:這種架構將承載連接的邏輯與后端的業務處理邏輯完全分離,這樣做是非常必要的, 尤其是廣播密集型應用(例如游戲和聊天)。密集的廣播與網絡通訊會占掉大量的資源,經過分離后業務邏輯的處理能力就不再受廣播的影響。
切換簡便:因為有了前、后端兩層的架構,用戶可以任意切換頻道或房間都不需要重連前端的websocket。
擴展性好:用戶數的擴展可以通過增加connector進程的數量來支撐。頻道的擴展可以通過哈希等算法負載均衡到多臺聊天服務器上。理論上這個架構可以實現頻道和用戶的無限擴展。
聊天服務器開發架構
game server與web server
聊天服務器項目中分生成了game-server目錄、web-server目錄與shared目錄,如下圖所示:
這樣也將應用天然地隔離成了兩個,game server與web server。
- Game server, 即游戲服務器,所有的游戲服務器邏輯都在里實現。客戶端通過websocket(0.3版會支持tcp的socket)連到game server。game-server/app.js是游戲服務器的運行入口。
- Web server,即web服務器, 也可以認為是游戲服務器的一個web客戶端, 所有客戶端的js代碼,web端的html、css資源都存放在這里,web服務端的用戶登錄、認證等功能也在這里實現。pomelo也提供了其它客戶端,包括ios、android、unity3D等。
- Shared目錄,假如客戶端是web,由于服務端和客戶端都是javascript寫的,這時Node.js的代碼重用優勢就體現出來了。shared目錄下可以存放客戶端、服務端共用的常量、算法。真正做到一遍代碼, 前后端共用。
服務器定義與應用目錄
Game server才是游戲服務器的真正入口,游戲邏輯都在里, 我們簡單看一下game-server的目錄結構,如下圖所示:
servers目錄下所有子目錄定義了各種類型的服務器,而每個servers目錄下基本都包含了handler和remote兩個目錄。 這是pomelo的創新之處,用極簡的配置實現游戲服務器的定義,后文會解釋handler和remote。
通過pomelo,游戲開發者可以自由地定義自己的服務器類型,分配和管理進程資源。在pomelo中,根據服務器的職責不同,服務器主要分為前端服務器(frontend)和后端服務器(backend)兩大類。其中,前端服務器負責承載客戶端的連接和維護session信息,所有服務器與客戶端的消息都會經過前端服務器;后端服務器負責接收前端服務器分發過來的請求,實現具體的游戲邏輯,并把消息回推給前端服務器,最后發送給客戶端。如下圖所示:
動態語言的面向對象有個基本概念叫鴨子類型。在pomelo中,服務器的抽象也同樣可以比喻為鴨子,服務器的對外接口只有兩類, 一類是接收客戶端的請求, 叫做handler, 一類是接收RPC請求, 叫做remote, handler和remote的行為決定了服務器長什么樣子。 因此開發者只需要定義好handler和remote兩類的行為, 就可以確定這個服務器的類型。 例如chat服務器目前的行為只有兩類,分別是定義在handler目錄中的chatHandler.js,和定義在remote目錄中的chatRemote.js。只要定義好這兩個類的方法,聊天服務器的對外接口就確定了。
搭建聊天服務器
準備知識
pomelo的客戶端服務器通訊
pomelo的客戶端和服務器之間的通訊可以分為三種:
request-response
pomelo中最常用的就是request-response模式,客戶端發送請求,服務器異步響應。客戶端的請求發送形式類似ajax類似:
``` pomelo.request(url, msg, function(data){}); ```
第一個參數為請求地址,完整的請求地址主要包括三個部分:服務器類型、服務端相應的文件名及對應的方法名。第二個參數是消息體,消息體為json格式,第三個參數是回調函數,請求的響應將會把結果置入這個回調函數中返回給客戶端。
notify
notify與request—response類似,唯一區別是客戶端只負責發送消息到服務器,客戶端不接收服務器的消息響應。
``` pomelo.notify(url, msg); ```
push
push則是服務器主動向客戶端進行消息推送,客戶端根據路由信息進行消息區分,轉發到后。通常游戲服務器都會發送大量的這類廣播。
``` pomelo.on(route, function(data){}); ```
以上是javascript的api, 其它客戶端的API基本與這個類型。由于API與ajax極其類似,所有web應用的開發者對此都不陌生。
session介紹
與web服務器類似,session是游戲服務器存放用戶會話的抽象。但與web不同,游戲服務器的session是基于長連接的, 一旦建立就一直保持。這反而比web中的session更直接,也更簡單。 由于長連接的session不會web應用一樣由于連接斷開重連帶來session復制之類的問題,簡單地將session保存在前端服務器的內存中是明智的選擇。
在pomelo中session也是key/value對象,其主要作用是維護當前用戶信息,例如:用戶的id,所連接的前端服務器id等。session由前端服務器維護,前端服務器在分發請求給后端服務器時,會復制session并連同請求一起發送。任何直接在session上的修改,只對本服務器進程生效,并不會影響到用戶的全局狀態信息。如需修改全局session里的狀態信息,需要調用前端服務器提供的RPC服務。
channel與廣播
廣播在游戲中是極其重要的,幾乎大部分的消息都需要通過廣播推送到客戶端,再由客戶端播放接收的消息。而channel則是服務器端向客戶端進行消息廣播的通道。 可以把channel看成一個用戶id的容器.把用戶id加入到channel中成為當中的一個成員,之后向channel推送消息,則該channel中所有的成員都會收到消息。channel只適用于服務器進程本地,即在服務器進程A創建的channel和在服務器進程B創建的channel是兩個不同的channel,相互不影響。
服務器之間RPC通訊
從之前的文章可以了解到,在pomelo中,游戲服務器其實是一個多進程相互協作的環境。各個進程之間通信,主要是通過底層統一的RPC框架來實現,服務器間的RPC調用也實現了零配置。具體RPC調用的代碼如下:
```javascript app.rpc.chat.chatRemote.add(session, uid, app.get ('serverId'), function(data){}); ```
其中app是pomelo的應用對象,app.rpc表明了是前后臺服務器的Remote rpc調用,后面的參數分別代表服務器的名稱、對應的文件名稱及方法名。為了實現這個rpc調用,則只需要在對應的chat/remote/中新建文件chatRemote.js,并實現add方法。
聊天室流程概述
下圖列出了聊天室進行聊天的完整流程:
通過以上流程, 我們可以看到pomelo的基本請求流程和用法。本文不是聊天室的tutorial,因此下面列出的代碼不是完整的,而是用極簡的代碼來說明pomelo的使用流程和api。
進入聊天室
客戶端向前端服務器發起登錄請求:
```javascript pomelo.request('connector.entryHandler.enter', {user:userInfo}, function(){}); ```
用戶進入聊天室后,服務器端首先需要完成用戶的session注冊同時綁定用戶離開事件:
```javascript session.bind(uid); session.on('closed', onUserLeave.bind(null, app)); ```
另外,服務器端需要通過調用rpc方法將用戶加入到相應的channel中;同時在rpc方法中,服務器端需要將該用戶的上線消息廣播給其他用戶,最后服務器端向客戶端返回當前channel中的用戶列表信息。
```javascript app.rpc.chat.chatRemote.add(session, uid, serverId,function(){}); ```
發起聊天
客戶端向服務端發起聊天請求,請求消息包括聊天內容,發送者和發送目標信息。消息的接收者可以聊天室里所有的用戶,也可以是某一特定用戶。
服務器端根據客戶端的發送的請求,進行不同形式的消息廣播。如果發送目標是所有用戶,服務器端首先會選擇channel中的所有用戶,然后向channel發送消息,最后前端服務器就會將消息分別發送給channel中取到的用戶;如果發送目標只是某一特定用戶,發送過程和之前完全一樣,只是服務器端首先從channel中選擇的只是一個用戶,而不是所有用戶。
```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(); }); ```
退出聊天室
用戶在退出聊天室時,必須完成一些清理工作。在session斷開連接時,通過rpc調用將用戶從channel中移除。在用戶退出前,還需要將自己下線的消息廣播給所有其他用戶。
```javascript app.rpc.chat.chatRemote.kick(session, uid, serverId, channelName, null); ```
聊天服務器的可伸縮性與擴展討論
上一講已經談到pomelo在提供了一個高可伸縮性的運行架構,對于聊天服務器同樣如此。如果想從單頻道聊天室擴展到多頻道聊天室,增加的代碼幾乎為零。大部分工作只是在進行服務器和路由的配置。對于服務器配置只需要修改json配置文件即可,而對于路由配置只需要增加一個路由算法即可。在pomelo中,開發者可以自己配置客戶端到服務器的路由規則,這樣會使得游戲開發更加靈活。
我們來看一下配置json文件對服務器運行架構的影響:
- 最簡服務器與運行架構:
- 擴展后的服務器與運行架構:
另外,在0.3版本的pomelo中增加了動態增刪服務器的功能,開發者可以在不停服務的情況下根據當前應用運行的負載情況添加新的服務器或者停止閑置的服務器,這樣可以讓服務器資源得到更充分的利用。
總結
本文通過聊天服務器的搭建過程,分析了pomelo開發應用的基本流程,基本架構與相關概念。有了這些知識我們可以輕松地使用pomelo搭建高實時應用了。 在后文中我們將分析更復雜的游戲案例,并且會對架構中的一些實現深入剖析。
相關資源: