天天看點

深入淺出Node.js遊戲伺服器開發--分布式聊天伺服器搭建

在上一篇文章中, 我們介紹了遊戲伺服器的基本架構、相關架構和 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遊戲伺服器開發--分布式聊天伺服器搭建

服務端由單個 Node.js 程序組成的 chat server 來接收 websocket 請求。

它有以下缺點:

  1. 可擴充性差:隻支援單程序的 Node.js, 無法根據 room/channel 分區, 也無法将廣播的壓力與處理邏輯的壓力分開。
  2. 代碼量大:基于 socket.io 做了簡單封裝,服務端就寫了約 430 行代碼。

用 pomelo 來寫這個架構可完全克服以上缺點,并且代碼量隻要區區 100 多行。

我們要搭建的 pomelo 聊天室具有如下的運作架構:

深入淺出Node.js遊戲伺服器開發--分布式聊天伺服器搭建

在這個架構裡, 前端伺服器也就是 connector 專門負責承載連接配接, 後端的聊天伺服器則是處理具體邏輯的地方。 這樣擴充的運作架構具有如下優勢: * 負載分離:這種架構将承載連接配接的邏輯與後端的業務處理邏輯完全分離,這樣做是非常必要的, 尤其是廣播密集型應用(例如遊戲和聊天)。密集的廣播與網絡通訊會占掉大量的資源,經過分離後業務邏輯的處理能力就不再受廣播的影響。

  • 切換簡便:因為有了前、後端兩層的架構,使用者可以任意切換頻道或房間都不需要重連前端的 websocket。
  • 擴充性好:使用者數的擴充可以通過增加 connector 程序的數量來支撐。頻道的擴充可以通過哈希等算法負載均衡到多台聊天伺服器上。理論上這個架構可以實作頻道和使用者的無限擴充。

聊天伺服器開發架構

game server 與 web server

聊天伺服器項目中分生成了 game-server 目錄、web-server 目錄與 shared 目錄,如下圖所示:

深入淺出Node.js遊戲伺服器開發--分布式聊天伺服器搭建

這樣也将應用天然地隔離成了兩個,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 的目錄結構,如下圖所示:

深入淺出Node.js遊戲伺服器開發--分布式聊天伺服器搭建

servers 目錄下所有子目錄定義了各種類型的伺服器,而每個 servers 目錄下基本都包含了 handler 和 remote 兩個目錄。 這是 pomelo 的創新之處,用極簡的配置實作遊戲伺服器的定義,後文會解釋 handler 和 remote。

通過 pomelo,遊戲開發者可以自由地定義自己的伺服器類型,配置設定和管理程序資源。在 pomelo 中,根據伺服器的職責不同,伺服器主要分為前端伺服器 (frontend) 和後端伺服器 (backend) 兩大類。其中,前端伺服器負責承載用戶端的連接配接和維護 session 資訊,所有伺服器與用戶端的消息都會經過前端伺服器;後端伺服器負責接收前端伺服器分發過來的請求,實作具體的遊戲邏輯,并把消息回推給前端伺服器,最後發送給用戶端。如下圖所示:

深入淺出Node.js遊戲伺服器開發--分布式聊天伺服器搭建

動态語言的面向對象有個基本概念叫鴨子類型。在 pomelo 中,伺服器的抽象也同樣可以比喻為鴨子,伺服器的對外接口隻有兩類, 一類是接收用戶端的請求, 叫做 handler, 一類是接收 RPC 請求, 叫做 remote, handler 和 remote 的行為決定了伺服器長什麼樣子。 是以開發者隻需要定義好 handler 和 remote 兩類的行為, 就可以确定這個伺服器的類型。 例如 chat 伺服器目前的行為隻有兩類,分别是定義在 handler 目錄中的 chatHandler.js,和定義在 remote 目錄中的 chatRemote.js。隻要定義好這兩個類的方法,聊天伺服器的對外接口就确定了。

搭建聊天伺服器

準備知識

pomelo 的用戶端伺服器通訊

pomelo 的用戶端和伺服器之間的通訊可以分為三種:

深入淺出Node.js遊戲伺服器開發--分布式聊天伺服器搭建
  • 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 調用的代碼如下:複制代碼

app.rpc.chat.chatRemote.add(session, uid, app.get

('serverId'), function(data){});

其中 app 是 pomelo 的應用對象,app.rpc 表明了是前背景伺服器的 Remote rpc 調用,後面的參數分别代表伺服器的名稱、對應的檔案名稱及方法名。為了實作這個 rpc 調用,則隻需要在對應的 chat/remote/ 中建立檔案 chatRemote.js,并實作 add 方法。

聊天室流程概述

下圖列出了聊天室進行聊天的完整流程:

深入淺出Node.js遊戲伺服器開發--分布式聊天伺服器搭建

通過以上流程, 我們可以看到 pomelo 的基本請求流程和用法。本文不是聊天室的 tutorial,是以下面列出的代碼不是完整的,而是用極簡的代碼來說明 pomelo 的使用流程和 api。

進入聊天室

用戶端向前端伺服器發起登入請求:

pomelo.request('connector.entryHandler.enter',

{user:userInfo}, function(){});

使用者進入聊天室後,伺服器端首先需要完成使用者的 session 注冊同時綁定使用者離開事件:

session.bind(uid);

session.on('closed', onUserLeave.bind(null, app));

另外,伺服器端需要通過調用 rpc 方法将使用者加入到相應的 channel 中;同時在 rpc 方法中,伺服器端需要将該使用者的上線消息廣播給其他使用者,最後伺服器端向用戶端傳回目前 channel 中的使用者清單資訊。

app.rpc.chat.chatRemote.add(session, uid,

serverId,function(){});

發起聊天

用戶端向服務端發起聊天請求,請求消息包括聊天内容,發送者和發送目标資訊。消息的接收者可以聊天室裡所有的使用者,也可以是某一特定使用者。

伺服器端根據用戶端的發送的請求,進行不同形式的消息廣播。如果發送目标是所有使用者,伺服器端首先會選擇 channel 中的所有使用者,然後向 channel 發送消息,最後前端伺服器就會将消息分别發送給 channel 中取到的使用者;如果發送目标隻是某一特定使用者,發送過程和之前完全一樣,隻是伺服器端首先從 channel 中選擇的隻是一個使用者,而不是所有使用者。

if(msg.target == '*')

channel.pushMessage(param);

else

channelService.pushMessageByUids(param,

[{uid:uid, sid:sid}]);

接收聊天消息

用戶端接收廣播消息,并将消息并顯示即可。

pomelo.on('onChat', function() {

addMessage(data.from, data.target, data.msg);

$("#chatHistory").show();

});

退出聊天室

使用者在退出聊天室時,必須完成一些清理工作。在 session 斷開連接配接時,通過 rpc 調用将使用者從 channel 中移除。在使用者退出前,還需要将自己下線的消息廣播給所有其他使用者。

app.rpc.chat.chatRemote.kick(session, uid, serverId,

channelName, null);

聊天伺服器的可伸縮性與擴充讨論

上一講已經談到 pomelo 在提供了一個高可伸縮性的運作架構,對于聊天伺服器同樣如此。如果想從單頻道聊天室擴充到多頻道聊天室,增加的代碼幾乎為零。大部分工作隻是在進行伺服器和路由的配置。對于伺服器配置隻需要修改 json 配置檔案即可,而對于路由配置隻需要增加一個路由算法即可。在 pomelo 中,開發者可以自己配置用戶端到伺服器的路由規則,這樣會使得遊戲開發更加靈活。

我們來看一下配置 json 檔案對伺服器運作架構的影響:

  • 最簡伺服器與運作架構:
深入淺出Node.js遊戲伺服器開發--分布式聊天伺服器搭建
  • 擴充後的伺服器與運作架構:

另外,在 0.3 版本的 pomelo 中增加了動态增删伺服器的功能,開發者可以在不停服務的情況下根據目前應用運作的負載情況添加新的伺服器或者停止閑置的伺服器,這樣可以讓伺服器資源得到更充分的利用。

總結

本文通過聊天伺服器的搭建過程,分析了 pomelo 開發應用的基本流程,基本架構與相關概念。有了這些知識我們可以輕松地使用 pomelo 搭建高實時應用了。 在後文中我們将分析更複雜的遊戲案例,并且會對架構中的一些實作深入剖析。

相關資源:

  • pomelo 建構聊天伺服器 tutorial
  • 聊天伺服器的源碼
  • 線上聊天伺服器 demo