通過我們上一篇的介紹,大家應該對Threerings這個引擎有了一個初步的認識。在引擎的核心框架之一的Narya中,主要包括了presents,crowd和bureau三個package,而presents則包括了今天我們要介紹的DObject部分。
presents這個包或者說框架是對底層網絡通訊的一層封裝,將底層的網絡通訊的實現細節抽象成對象與事件(object和event),以供建造在其上的遊戲應用能夠方便的調用。在深入討論presents框架對構造在其上的應用所提供的服務之前,我們不妨先來看一下下面所列的幾點,對於一個mmorpg遊戲來說需要一種什麼樣的網絡通訊機制。
1)單一的服務器上連接有大量的客戶端。
2)通訊不僅發生在服務端與客戶端,還發生在客戶端與客戶端之間,但客戶端與客戶端的通訊也必須通過服務器。
3)前面已經討論過,客戶端無權直接修改共享的敏感數據,而只有向服務端發出請求,服務端驗證後纔會修改該數據。
而presents框架正是通過分佈式對象(distributed objects)即我們所謂的DObject機制來滿足上述需求的。presents框架所提供的服務允許構造在其上的遊戲應用通過這種DObject機制來訪問需要共享的信息。DObject對象由服務端來維護,客戶端通過訂閱DObject對象獲得一個該對象的本地代理,當狀態發生變化時客戶端通過接收來自服務端的“事件”來更新本地的代理對象。
客戶端無法直接修改本地的代理對象,而是通過將請求封裝在“事件”中,通過調用本地代理對象的setter方法將該請求發送到服務端供進一步處理。服務端在驗證了該請求的合法性之後會把封裝了該請求的“事件”應用到維護在服務端的主DObject中,隨後會把該“事件”分發給所有訂閱了該DObject的所有客戶端。包括髮起請求的客戶端之內的所有客戶端再將該“事件”應用到本地的代理對象上。正是通過這樣的機制所有的客戶端可以維護到最新的對象數據。
定義DObject對象
定義一個DObject對象就如同定義一個普通的java對象一樣,只是在定義完之後需要通過ant運行一個該引擎自帶的代碼生成工具,該工具會自動生成並插入運行DObject系統所必須的一些方法和常量。一個剛定義好的DObject對象看起來是這樣的:
public class CageObject extends DObject |
在DObject中所有的非transient並且是public修飾符的屬性都會被該引擎自帶的代碼生成工具捕獲,並生成用於DObject系統相應的方法和常量。所有非public或者有transient修飾符的屬性會被忽略,也就是說當訂閱者通過網絡從服務器接收一個該DObject對象的本地代理時這些屬性並不會通過網絡被接收到。
當我們運行完代碼生成工具後,我們定義的對象會變成這樣:
public class CageObject extends DObject /** The field name of the owner field. */
/** The number of monkeys in the cage. */
/** The name of the owner of this cage. */
// AUTO-GENERATED: METHODS START /** |
黑體部分的代碼就是工具爲我們所生成的,其中包括屬性的setter方法和常量的定義,當我們從服務器接收到屬性更新的事件後,就是通過這些常量來區別具體是哪個屬性的狀態被更新。而且只要AUTO-GENERATED塊中的內容不要去手工修改的話,你可以重複添加或修改對象的屬性,代碼生成工具會自動幫你生成該屬性所對應的setter方法和常量定義,而所有在AUTO-GENERATED塊之外的內容都會保持不變。
看的仔細的讀者也許會發現,在setter方法中,在發出屬性更新的請求後,新的值會被立即寫入到該DObject對象的本地副本中。這是因爲有網絡延遲,客戶端往往還沒有等到服務器傳來的屬性更新“事件”就假設屬性已經被成功修改,從而導致運行時產生錯誤。Threerings小組在多次經歷這樣的問題之後不得不對框架做出了修改,通過將新的值立即寫入到DObject對象的本地副本的屬性中來解決這些問題。但是反過來也說明,在一般情況下,DObject對象屬性的更新並不是直接由客戶端觸發,而是由服務端在接收到從客戶端發來的可以導致DObject對象屬性發生變化的請求時觸發的。
創建DObject對象
你可以象創建一個普通的java對象一樣創建一個DObject對象,然後通過DObject Manager來註冊它。但要注意的是你必須在server端使用RootDObjectManager對象來註冊它,因爲它的父類 DObjectManager中並沒有registerObject這個方法,如果在客戶端的話,你是無法創建DObject對象的(準確的說你也可以創建,但它僅僅只是一個普通的java對象而已,並不具備框架所賦予它的DObject對象的機制),而只能通過調用服務端的服務來實現(通過 InvocationService,會在下一篇中詳細介紹)。
public class ServerEntity { public void init (RootDObjectManager omgr) { _object = omgr.registerObject(new CageObject()); } protected CageObject _object; } |
訂閱DObject對象
客戶端通過訂閱來獲取一個DObject對象的本地代理
public class ObjectUser implements Subscriber { public void init (Client client, int objectId) { client.getDObjectManager().subscribeToObject(objectId, this); } // inherited from interface Subscriber public void objectAvailable (DObject object) { // yay! we got our object _object = (CageObject)object; } // inherited from interface Subscriber public void requestFailed (int oid, ObjectAccessException cause) { // oh the humanity, we failed to subscribe } protected CageObject _object; } |
之後可以通過類似的機制來解除對該DObject對象的訂閱
public class ObjectUser implements Subscriber { // ... public void shutdown (Client client) { client.getDObjectManager().unsubscribeFromObject( _object.getOid(), this); _object = null; } // ... } |
既然說到這裏,我們也不妨再多說幾句,即在一個異步的分佈式環境中,並不能保證在ObjectUser中,對DObject對象的訂閱請求一定會在你調用shutdown之前被處理,如果出現這種情況,那麼在前面的例子中,你會得到一個null pointer異常,更糟糕的情況是當你以爲你的DObject對象已經被解除訂閱了,而實際上卻沒有。爲了解決這個問題,框架引入了一個SafeSubscriber類。
public class ObjectUser implements Subscriber { // inherited from interface Subscriber // inherited from interface Subscriber public void shutdown (Client client) { protected SafeSubscriber _safesub; |
SafeSubscriber類中的subscribe以及unsubscribe方法是對DObjectManager中的subscribeToObject以及unsubscribeFromObject兩個方法的封裝,使用SafeSubscriber類,它能保證在解除訂閱之前訂閱請求一定會先行被處理,甚至在先行的訂閱請求失敗等複雜情況下仍然能保證請求能夠被正確處理。
事件偵聽
當DObject對象被成功訂閱後,所有有關於此DObject對象的事件都會被派送到位於客戶端的本地代理對象上。如果需要動態的對不同事件作出相應的反應,那麼可以使用偵聽器(listener)。在客戶端上可以註冊任意數目的偵聽器,當DObject對象被解除訂閱後,所有註冊的偵聽器也即隨之而去。
AttributeChangeListener最常見的偵聽器之一,當我們所偵聽的DObject屬性發生變化的時候即會被通知到,還是來看一下下面的例子。
public class ObjectUser // inherited from interface Subscriber // inherited from interface Subscriber // inherited from interface AttributeChangeListener public void shutdown (Client client) { protected SafeSubscriber _safesub; |
在當前的分佈式系統中,當有任何一方調用了CageObject的setter方法後都會生成一個attributeChange事件併發送到服務器,服務器處理完後會重新派發此事件到所有的CageObject對象的訂閱者,若客戶端註冊了attributeChage事件的偵聽器後,此事件就會被該偵聽器所捕獲,並調用attributeChanged()方法。在這裏大家不要把subscriber和listener兩個概念搞混,最關鍵的區別是不管有沒有註冊某個DObject偵聽器,只要訂閱了該DObject那麼所有的事件都會通過Presents系統被派送並應用到該DObject的代理對象上。另外還需要注意的一點是不僅在客戶端上可以註冊事件偵聽器,在服務端同樣可以,一旦事件通過網絡被派發就會被立即通知到。
其次還需要知道的一點是偵聽器是在事件被應用到對象之後纔會被通知到。之前的屬性值可以通過AttributeChangedEvent.getOldValue()方法來得到,不過在實際當中好像很少需要知道這個oldValue。
分佈式集合屬性
在前面的例子中我們一直使用primitive類型作爲DObject的分佈式屬性,但在某些情況下我們還是需要引入更復雜的數據結構來作爲DObject的分佈式屬性。接下來我們會介紹Presents框架所支持的兩種集合類型,即sets和arrays來作爲我們的DObject對象的分佈式屬性,通過框架提供的機制,使用起來就如同是primitive屬性一般。
分佈式數組
在DObject中使用元素爲primitive類型的數組,在使用代碼生成工具時會被偵測到,而其自動生成的代碼提供了一種既可以更新整個數組也可以一次只更新數組中單個元素的機制。
public class ChessObject extends DObject /** Used to track our board state. */ // AUTO-GENERATED: METHODS START /** |
針對數組單個元素的更新,我們可以使用ElementUpdateListener偵聽器來偵聽。當數組中的單個元素更新後,實現了這個接口的偵聽器會自動被通知。不過當調用setState()方法更新整個數組後,我們還是要使用普通的AttributeChangeListener偵聽器來偵聽。
在使用數組作爲分佈式對象還要注意下數組的界標,比如當發出請求更新數組索引爲9的元素的時候,請確保你的數組至少有10個元素,否則會拋出一個數組下標越界的異常。實際上在使用數組的時候並非只能使用primitive作爲元素類型,而可以是任何實現Streamable接口的對象,對於Streamable接口我們在接下來的一節中會詳細介紹。
Streamable接口和SimpleStreamableObject對象
Streamable接口是用來標記那些可以通過網絡來傳輸的對象,並且可以使用在DObject中作爲分佈式數組中的元素。同java中的Serializable接口類似,底層的對象序列化是通過反射來實現的。在對象序列化的時候,只要沒有標記成transient的屬性都會被序列化。請看下面的例子:
public class Player implements Streamable /** This player's rating. */ public class ChessObject extends DObject |
自動生成的代碼在這裏就省略了,不過你可以想象一下象這樣兩個方法setPlayers(Player[] value)和setPlayersAt(Player value, int index)會被生成幷包含該方法中應有的代碼。在這裏需要指出的是實現了streamable接口的對象在網絡上傳輸的時候是整個的對象在傳輸,而不只是更新了的單個屬性,估計是因爲這樣做太複雜了而且用處也不大。如果帶寬是首要考慮目標,你可以自己從DEvent類繼承並定製一個專門的event類來控制什麼需要被傳輸,什麼不需要,這裏就不展開討論了。SimpleStreamableObject類是Streamable接口的一個簡單實現,它使用反射默認實現了toString()方法,可以打印出屬性的實際值(在調試和日誌輸出的時候比較有用)。
Distributed Sets
如果是開發一個分佈式系統的話,經常碰到的一個情況是需要一個可以自由添加元素的分佈式的對象集合,而這個集合中元素的排列順序通常卻並不重要。爲了滿足這種需求,框架爲我們提供了分佈式的集合類DSet。一個DSet對象往往包含了多個元素,我們把它叫做entry,每個entry必須要實現DSet.Entry接口,而一旦實現這個接口也即自動實現了Streamable接口;實現DSet.Entry接口的元素還必須提供一個Comparable key用來區別DSet中的其他元素(通過key還可以使用效率比較高的二分查詢算法)。若在DObject中使用了DSet的話,除了set之外自動生成的代碼還會包括addTo,update以及removeFrom三個方法,比如我們來看下面的這個例子:
public class Monkey implements DSet.Entry /** The monkey's age. */ // documentation inherited from interface DSet.Entry public class CageObject extends DObject /** A collection of monkeys. */ // AUTO-GENERATED: METHODS START /** /** /** |
當然了,我們可以直接更新整個set的值,但更多的僅僅只是往set中增加新的entry,更新set中entry的值,或者刪除某個entry。與DSet配套的自然有它相應的偵聽器(SetListener),使用這個偵聽器的話,當一個distributed set被更改的時候會自動被通知到。這個偵聽器具體使用與前面介紹其他兩個偵聽器非常類似,這裏就不再舉例說明了。