Pomelo框架參考

翻譯:死月,校對:葉岬,原文連接

Pomelo(柚子)是一個可以讓開發(fā)者便捷開發(fā)的游戲服務(wù)端框架。下面是其一些Pomelo的主要設(shè)計(jì)思想。

概述

在這一節(jié)中我們會展示Pomelo是什么。

在游戲服務(wù)端中通常有著各種各樣的任務(wù),形如客戶端連接的管理、游戲世界狀態(tài)的管理以及計(jì)算游戲的邏輯。不同的任務(wù)可能會又不同的資源需求,比如IO開銷或者CPU開銷等。但是在一個進(jìn)程中處理所有的作業(yè)顯然是非常不切實(shí)際的。所以將一個游戲服務(wù)端分割成一些簡小服務(wù)端進(jìn)程是一種趨勢。每個服務(wù)端只需處理一種單一的服務(wù),比如說連接服務(wù)、場景服務(wù)或者聊天服務(wù)。所有這些服務(wù)進(jìn)程之間都有著互相的聯(lián)系,從而構(gòu)成整個大千游戲世界。

要從頭開發(fā)一個基于上述模型的游戲服務(wù)端的話,必須消耗非常多的時間,而且處理起那些形如服務(wù)端資源規(guī)劃、建立以及管理網(wǎng)絡(luò)連接、進(jìn)程間發(fā)送和接收消息等非常冗雜的作業(yè)也會顯得非常力不從心。更糟糕的是,這些作業(yè)在開始一個新游戲的時候又得不斷重復(fù)。所以就需要有一個救世主來把你從繁雜冗余重復(fù)的工作中拯救出來,比如說BigWorld引擎(非常著名和給力,同時也非常貴和復(fù)雜)。現(xiàn)在Pomelo給你提供來另一個選擇(開源,簡單,迅捷)來達(dá)到你的要求。

Pomelo框架包括以下的功能模塊:

Pomelo框架
Pomelo框架

<center><small>Pomelo 結(jié)構(gòu)</small></center>

  • 服務(wù)端管理模塊負(fù)責(zé)定義服務(wù)端類型,建立和監(jiān)控所有服務(wù)進(jìn)程。
  • 網(wǎng)絡(luò)模塊是進(jìn)程間通信的基礎(chǔ),也提供了RPC和頻道(Channel)來隱藏所有的底層詳情。
  • 應(yīng)用模塊代表了關(guān)注配置和生命周期管理相關(guān)的服務(wù)流程的進(jìn)程上下文環(huán)境。(求大神翻譯這句)

服務(wù)器類型

Pomelo在為一個游戲服務(wù)器群集可定制所有服務(wù)端模板的基礎(chǔ)上提供一個靈活的服務(wù)端類型系統(tǒng)。它將服務(wù)端類型分為兩大類——前端服務(wù)端和后端服務(wù)端。

Pomelo服務(wù)端類型
Pomelo服務(wù)端類型

<center><small>Pomelo服務(wù)端類型</small></center>

總而言之,前端服務(wù)端是負(fù)責(zé)與客戶端的通信然后把這些請求轉(zhuǎn)發(fā)給后端服務(wù)端,而后端服務(wù)端就是實(shí)現(xiàn)游戲邏輯的戰(zhàn)場了。

本質(zhì)上,前端和后端服務(wù)端都是服務(wù)端容器。基于服務(wù)端容器,開發(fā)者可以根據(jù)自己的意愿自由定制他們的服務(wù)端類型。開發(fā)者擁有全部的權(quán)限來決定一個服務(wù)端需要提供哪種類型的服務(wù)。比如說,你如果在后端服務(wù)端碼了聊天服務(wù)的代碼那么你就可以弄出一個聊天服務(wù)端,又比如你在后端服務(wù)端碼了狀態(tài)服務(wù)的代碼你也可以搞出一個狀態(tài)服務(wù)端。

所以啊開發(fā)者們只需要在Pomelo上面掐指一算來決定需要多少前段服務(wù)端和后端服務(wù)端來堆成一坨游戲服務(wù)端,然后為這些服務(wù)端結(jié)點(diǎn)碼上應(yīng)有的代碼。接著Pomelo就會開始滾動然后能滾起所有的服務(wù)進(jìn)程。

服務(wù)端容器
服務(wù)端容器

<center><small>服務(wù)端容器</small></center>

客戶端請求機(jī)制

這一節(jié)我們會講一下服務(wù)端是如何處理客戶端請求的。

請求和響應(yīng)

在Pomelo里,消息將為氛圍兩類——請求和通知。這兩者的不同點(diǎn)如下所示:

請求和通知
請求和通知

<center><small>請求和通知</small></center>

請求是雙向消息,也就是說服務(wù)端接收到一個客戶端消息的時候就得發(fā)回客戶端一個響應(yīng)消息。這個時候Pomelo就會射出一個與請求相關(guān)聯(lián)的回調(diào)??蛻舳巳缦屡鲆粋€請求:

pomleo.request('connector.helloHandler.ask',
    {
        msg: 'What is your name?'
    }, function(resp) 
    {
        // We can get the name from response
    }
);

然后通知就是單向消息,服務(wù)端不需要提供相應(yīng)??蛻舳诵枞缦屡鲆粋€請求:

pomelo.notify('connector.helloHandler.sayHi',
    {
        msg: 'Hi'
    }
);

如何處理客戶端消息

Pomelo將進(jìn)程流*分為兩部分:處理機(jī)和過濾器。處理機(jī)負(fù)責(zé)提供游戲邏輯,過濾器需要做前置和后續(xù)工作,比如日志和超時處理等。兩者分工如下圖所示:

客戶端請求處理
客戶端請求處理

前置過濾器

客戶端而來的消息需要通過前置過濾器鏈來做一些前置處理,比如說驗(yàn)證當(dāng)前玩家登錄狀態(tài)以及日志記錄。前置過濾器的接口如下:

filter.before = function(msg, session, next)

msg是從客戶端接收的消息對象;session是當(dāng)前玩家的會話對象;next是接下去的流程的回調(diào)函數(shù)。

前置過濾器需要調(diào)用next函數(shù)來跑到前置過濾器鏈中的下一個前置過濾器。當(dāng)它穿過了整條前置過濾器鏈時,它將最終撲進(jìn)處理機(jī)的懷抱。如果next函數(shù)的第一個參數(shù)傳入錯誤,那么說明蹦跶出了一個錯誤,就需要停止這個進(jìn)程流,比如說這個玩家不自覺還沒有登錄。這時這個消息就需要被傳進(jìn)一個全局錯誤處理機(jī)(稍后再做解釋)。

處理機(jī)

處理機(jī)是實(shí)現(xiàn)游戲邏輯的天堂。它的接口如下:

handler.methodName = function(msg, session, next)

參數(shù)跟前置過濾器差不多。處理一個請求消息時,處理機(jī)需要傳進(jìn)響應(yīng)對象,這是一個簡單的Json對象,將作為下一個回調(diào)函數(shù)的第二個參數(shù)。對于一個通知消息來說,只需要將第二個參數(shù)留空即可。

如果在處理的過程中又蹦跶出了一個錯誤,只需要傳入一個錯誤對象給next函數(shù)的第一個參數(shù)即可,跟之前的前置過濾器一樣。

錯誤處理機(jī)

錯誤處理機(jī)是一個可選項(xiàng),它將處理全局錯誤——比如說未知錯誤的處理以及錯誤的報告。錯誤處理機(jī)如下設(shè)置:

app.set('errorHandler', handleFunc);

一個錯誤處理機(jī)的接口又該如下申明:

errorHandler = function(err, msg, resp, session, next)

err是前置過濾器或者處理機(jī)傳給的錯誤對象;resp是處理機(jī)本來將要傳給客戶端的響應(yīng)消息。剩下的參數(shù)跟之前講的一樣。

后續(xù)過濾器

前面講的進(jìn)程流最終會由后續(xù)過濾器來擦屁股。后續(xù)過濾器將負(fù)責(zé)后續(xù)的處理,比如釋放請求上下文資源,記錄該請求的處理時間。不過它不要修改響應(yīng)消息,因?yàn)槠鋵?shí)在進(jìn)入這個后續(xù)過濾器鏈中之前,消息就已經(jīng)被一股腦發(fā)送到客戶端了。

后續(xù)過濾器接口聲明如下:

filter.after = function(err, msg, resp, session, next)

所有的參數(shù)都跟之前說的一樣。

會話

會話是一個保持玩家狀態(tài)的鍵對對象,它可以是以這個玩家id作為鍵名。Pomelo擁有兩種類型的會話:全局會話和本地會話。

全局會話是由與客戶端直連的前端服務(wù)端生成的并且就位于前端服務(wù)端。這是存儲玩家信息的全局地方。傳客戶端消息的時候它將弄出一個拷貝和客戶端消息一起傳給后端服務(wù)端。這時后端服務(wù)端就會得到一份會話拷貝,也就是本地會話了。

本地會話應(yīng)該是一個只讀的對象,至少只在本地是可讀寫的*。在本地會話的修改并不會影響到全局上的那位。如果你想將本地會話同步到全局會話的話就需要調(diào)用推送本地會話的方法。更多細(xì)節(jié)請參考API文檔中的本地會話服務(wù)。

頻道和廣播

在這一節(jié)我們會搞一下服務(wù)器是如何把消息推送給客戶端的。

頻道

屎一樣多的消息將要在游戲服務(wù)端被推送,比如當(dāng)一個玩家在一個場景中從A點(diǎn)移動到B點(diǎn)的時候,服務(wù)器就需要推送一個AOI(Area of Interest)消息給周圍的玩家。頻道就是推送消息的這么個工具。

頻道是一個玩家ID的合集。你可以把一個玩家ID加入到某個頻道,同樣也可以從中移除。如果你通過頻道來推送消息,那么該頻道中的所有成員都將收到這條消息。你可以創(chuàng)建任意多個頻道來自定義消息推送區(qū)域,這些頻道互相之間是獨(dú)立的。

頻道分類

Pomelo有兩種頻道:命名頻道和匿名頻道。

創(chuàng)建一個命名頻道時,你需要給它指定一個頻道名,這樣它才會返回一個頻道實(shí)例。命名頻道并不會自動釋放,你需要記得調(diào)用 channel.destroy 方法來釋放它。命名頻道通常用來保持長時間關(guān)系的訂閱,比如說聊天服務(wù)。

匿名頻道通過 channelService.pushMessageByUids 來使用的,沒有頻道名也沒有頻道實(shí)例返回。匿名頻道用于那些頻道成員變動頻繁的或者推送的都是臨時消息的時候,比如AOI消息。

頻道的更多用法請參見API文檔。

這兩種頻道本質(zhì)上都是相同的,即使他們看起來是兩個媽生的。首先,頻道需要將連接至它們的前端服務(wù)端分組。然后它需要將消息連通玩家ID通過分組一起推送到各自的前端服務(wù)端,最后天女散花至各個客戶端。

頻道廣播
頻道廣播

<center><small>頻道廣播</samll></center>

RPC框架

在這一節(jié),我們將解決各服務(wù)端之間通信不打架的問題。

RPC的用途

進(jìn)程間需要互相配合合作,它們之間的通信是非常重要且復(fù)雜的。Pomelo的RPC(Remote Procedure Call,遠(yuǎn)程過程調(diào)用)框架就是一個將進(jìn)程們?nèi)嗄笤谝黄鸬挠行侄巍?/p>

下面就是Pomelo的RPC框架需要思考的一些點(diǎn)。

  • 路由規(guī)則。路由規(guī)則決定一條消息應(yīng)該傳給哪個進(jìn)程。路由規(guī)則與消息類型不同,不過它也同樣會被當(dāng)前玩家狀態(tài)或者其它一些東東影響。比如客戶端的一個簡單移動請求,當(dāng)當(dāng)前玩家處于場景1中,它就需要被路由給管理場景1的進(jìn)程。如果玩家被傳送到場景2,那么這之后的所有移動請求都需要被路由給場景2的進(jìn)程中。而且不同的游戲這個規(guī)則是不同的。所以這里就需要一個開放的機(jī)制來讓開發(fā)者好定義路由規(guī)則。
  • 協(xié)議。不同游戲的服務(wù)端進(jìn)程間的請求通信協(xié)議也是不同的。比如說有些服務(wù)端想要TCP協(xié)議,而有些則對UDP欲求不滿。

Pomelo的RPC框架引入抽象層來簡化和解決以上所述的問題。

RPC客戶端

RPC客戶端層體系結(jié)構(gòu)如下:

RPC客戶端體系結(jié)構(gòu)
RPC客戶端體系結(jié)構(gòu)

<center><small>RPC客戶端體系結(jié)構(gòu)</small></center>

上圖中RPC消息是一個文本消息,包含一個RPC請求的描述,它包括RPC請求類型、參數(shù)以及其它一些東西。Session是啟動RPC請求的玩家狀態(tài)的一個合集。

  • 郵箱層——郵箱層解決了通信協(xié)議的問題。一個郵箱代表一個遠(yuǎn)程服務(wù)端,并且使用遠(yuǎn)程服務(wù)端ID作為該郵箱的ID,這樣就能通過服務(wù)端ID非常方便地找到相關(guān)聯(lián)的郵箱實(shí)例。郵箱實(shí)例涵蓋了當(dāng)前服務(wù)端與遠(yuǎn)程服務(wù)端的所有通信細(xì)節(jié),比如說如何建立連接,該用何種協(xié)議,如何關(guān)閉連接等等。你可以實(shí)現(xiàn)不同的郵箱來讓它們支持不同的協(xié)議,而且它們的通信協(xié)議能很方便地進(jìn)行切換,因?yàn)槟阒恍枰卩]箱層選擇合適的郵箱就可以了。
  • 郵局層——郵局層維持了當(dāng)前進(jìn)程的所有郵箱實(shí)例。它會將RPC消息從上層通過郵箱ID傳送給對應(yīng)的郵箱實(shí)例。郵局層接收到一個決定對應(yīng)一個遠(yuǎn)程服務(wù)端將要生成何種類型的郵箱的郵箱工廠函數(shù),然后返回一個相關(guān)聯(lián)的郵箱實(shí)例。對于一個服務(wù)端第一次嘗試連接遠(yuǎn)程服務(wù)端時,它將詢問郵箱工廠以生成一個郵箱實(shí)例。所以開發(fā)者需要通過郵箱工廠函數(shù)來自定義通信機(jī)制。
  • 路由層——路由層用來提供路由規(guī)則。它接受一個路由函數(shù),然后用它來通過上層的RPC消息和會話決定目標(biāo)進(jìn)程ID。然后這個ID就會通過郵局層,正如上文所述。
  • 代理層——代理層提供了使調(diào)用遠(yuǎn)程方法看起來就像在調(diào)用本地方法并且隱藏所有RPC細(xì)節(jié)的本地代理實(shí)例。本地代理方法和遠(yuǎn)程方法的唯一區(qū)別在于它多了個會話(session)參數(shù),包含了當(dāng)前玩家的狀態(tài),這個參數(shù)在參數(shù)槽的第一個位置。下面是一個簡單的例子:

遠(yuǎn)程服務(wù):

remote.echo = function(msg, cb) {
  // …
};

本地代理:

proxy.echo = function(session, msg, cb) {
  // …
};

還有一個好辦法來調(diào)用遠(yuǎn)程函數(shù),那就是如果目標(biāo)服務(wù)端ID直接可用,那么可以通過調(diào)用rpcInvoke*函數(shù)來實(shí)現(xiàn)。

RPC服務(wù)端

RPC服務(wù)端的層次如下:

RPC服務(wù)端結(jié)構(gòu)體系
RPC服務(wù)端結(jié)構(gòu)體系

<center><small>RPC服務(wù)端結(jié)構(gòu)體系</small></center>

  • 接收器層(受體層)——接收器層通過網(wǎng)絡(luò)將遠(yuǎn)程服務(wù)導(dǎo)出。它將會監(jiān)聽一個端口,通過指定協(xié)議接收以及解析RPC消息。需要指出的是,受體得與遠(yuǎn)程的同行們的郵箱層進(jìn)行合作,也就是說它們得用同樣的協(xié)議來使它們與各自能進(jìn)行無障礙通信。接收器同樣是被接收器工廠函數(shù)定制的。而且接收器得將RPC消息傳往上層。
  • 調(diào)度層——調(diào)度層解析RPC消息,導(dǎo)出消息類型以及RPC參數(shù),然后將RPC請求轉(zhuǎn)發(fā)至目標(biāo)遠(yuǎn)程服務(wù)。
  • 遠(yuǎn)程服務(wù)層——遠(yuǎn)程服務(wù)層實(shí)現(xiàn)了開發(fā)者提供的服務(wù)邏輯,由Pomelo框架自動載入。

服務(wù)端擴(kuò)展

這一節(jié),灑家一起討論討論如何擴(kuò)展服務(wù)端進(jìn)程的能力。

正如之前提到的,我們創(chuàng)建了很多類型的服務(wù)端,每個服務(wù)端都有自己的牛叉之處。比如說,前段服務(wù)端有著從客戶端接收消息的能力,后端服務(wù)端有著從前段服務(wù)端進(jìn)貢的消息。那么,我們又該如何維護(hù)和重用這些厲害的地方呢?此外,我們該如何以非常優(yōu)雅和靈動的方式來擴(kuò)展這些進(jìn)程的能力呢?

“合體(組合)”將會是一個非常合適的途徑。Pomelo有著一個組件系統(tǒng)來達(dá)到這個目標(biāo)。

組件

組件是啥

在Pomelo里面,組件是可重用的服務(wù)單位。一個組件實(shí)例提供了若干種服務(wù)。比如說處理機(jī)組件載入處理機(jī)代碼后將會將客戶端消息傳給請求處理機(jī)。

一個組件實(shí)例要被注冊進(jìn)進(jìn)程上下文(被稱為應(yīng)用),這樣后者就會獲得該實(shí)例提供的能力。組件實(shí)例可以通過應(yīng)用與其它組件進(jìn)行交互合作。比方說一個連接組件接收到一個客戶端請求然后將它發(fā)送給應(yīng)用,那么一個處理機(jī)組件等下就有可能從應(yīng)用中獲取這條消息。

組件系統(tǒng)模型如下所示:

組件系統(tǒng)
組件系統(tǒng)

<center><small>組件系統(tǒng)</small></center>

在代碼里面,組件是一個非常簡單的類,實(shí)現(xiàn)了一些必須的生命周期接口,應(yīng)用需要觸發(fā)生命周期每個階段每個組件所需的回調(diào)函數(shù)。

組件的生命周期
組件的生命周期

<center><small>組件的生命周期</small></center>

  • start(cb)——服務(wù)端在啟動階段被調(diào)用的開始生命周期。注意:各組件需要調(diào)用cb函數(shù)來繼續(xù)接下去的步驟。組件也可以傳入一個粗偶的參數(shù)給cb來表示當(dāng)前組件啟動失敗,以此來使應(yīng)用結(jié)束這個進(jìn)程。
  • afterStart(cb)——服務(wù)端在啟動之后的生命周期階段回調(diào),需要在當(dāng)前進(jìn)程所有被注冊組件啟動之后調(diào)用。它給這些組件進(jìn)行一些協(xié)作間初始化的機(jī)會。
  • stop(force, cb)——服務(wù)端在停止生命周期階段的回調(diào),當(dāng)服務(wù)端將要停止的時候調(diào)用。組件可以做一些清理作業(yè),比如沖刷此周期中的數(shù)據(jù)到數(shù)據(jù)庫。force參數(shù)如果為true的話表示所有的組件都需要被立即停止。

Pomelo的抽象級

基于組件系統(tǒng),應(yīng)用實(shí)際上是進(jìn)程的骨干。它載入了所有的注冊組件,鞭策它們穿越了整個生命周期。但是應(yīng)用不能涉及到各組件的細(xì)節(jié)。所有的定制一個服務(wù)端進(jìn)程的作業(yè)僅僅只是挑選必須的組件構(gòu)成一個應(yīng)用。所以應(yīng)用是非常干凈和靈活的,而組件的可重用性非常高。此外,組件系統(tǒng)最終將所有服務(wù)端類型弄進(jìn)一個統(tǒng)一進(jìn)程。

Pomelo的抽象級
Pomelo的抽象級

<center><small>Pomelo的抽象級</small></center>

怎么注冊一個組件

如下:

app.load([name], comp, [opts])
  • name——可選的組件名。命名的組件實(shí)例可以在被載入后通過app.components.name來訪問。
  • comp——組件實(shí)例或者組件工廠函數(shù)。如果comp是一個函數(shù),那么應(yīng)用將會把它當(dāng)做一個工廠函數(shù),并且讓它返回一個組件實(shí)例。工廠函數(shù)有兩個參數(shù)appopts(看下文),并且它將返回一個組件實(shí)例。
  • opts——可選項(xiàng),它將被傳入至組件工廠函數(shù)的第二個參數(shù)。

總結(jié)

我們將一整個服務(wù)端分尸成一些小塊服務(wù)端,以此來清晰化結(jié)構(gòu)體系和提高游戲服務(wù)端的可擴(kuò)展性。然后我們將所有的服務(wù)類型綜合成兩種服務(wù)端容器即前段和后端服務(wù)端,通過這樣的方式來簡化模型。我們討論了客戶端到服務(wù)端、服務(wù)端到客戶端以及服務(wù)端之間的消息流。最后,我們介紹了組件系統(tǒng),把上面的所有物件合體構(gòu)成一個有機(jī)的整體??偟目磥?,Pomelo提供了一個可擴(kuò)展的靈活的框架來支持游戲服務(wù)端開發(fā),并且趕走了所有的“躁點(diǎn)”和復(fù)雜的作業(yè)。

享受Pomelo的游戲開發(fā)過程吧,親們!

更多信息請參見API文檔,快速開始手冊結(jié)構(gòu)體系概述。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容