昨天和人閑扯,談到了 MMORPG 客戶端的網(wǎng)絡(luò)消息應(yīng)該基于怎樣的模型。依稀記得很早寫過我的觀點,但是 blog 上卻找不到。那么今天補(bǔ)上這么一篇吧。
我認(rèn)為,MMO 類游戲,服務(wù)器扮演的角色是虛擬的世界,一切的狀態(tài)變化都是在游戲服務(wù)器仲裁和演化的。而客戶端的角色本質(zhì)上是一個狀態(tài)呈現(xiàn)器,把玩家視角看到的虛擬世界的狀態(tài),通過網(wǎng)絡(luò)消息呈現(xiàn)出來。所以、在設(shè)計客戶端的網(wǎng)絡(luò)消息分發(fā)框架時,應(yīng)圍繞這個職責(zé)來設(shè)計。
客戶端發(fā)起的請求分兩種:一種是通知服務(wù)器,我扮演的角色狀態(tài)發(fā)生了改變,請求服務(wù)器仲裁;另一種是,我期望獲取服務(wù)器對象的最新狀態(tài)。后一種有時以服務(wù)器主動推送來解決,也可以用主動請求,兩者主要的區(qū)別在于流量控制。
其實、對于客戶端作為狀態(tài)呈現(xiàn)這個角色而言,請求和回應(yīng)之間的關(guān)系并沒有什么緊密聯(lián)系,并不適合使用基于 RPC 的網(wǎng)絡(luò)模型。這個無關(guān)乎 RPC 的實現(xiàn)是用 callback 還是用 coroutine 。
這是因為,當(dāng)服務(wù)器的狀態(tài)改變時,客戶端關(guān)心的應(yīng)該是這類事情發(fā)生時,我應(yīng)該如何呈現(xiàn);而不是具體到某個請求發(fā)出后,收到回應(yīng)我應(yīng)該怎么處理。RPC 把請求和回應(yīng)緊密耦合在一起,讓回應(yīng)的處理流程強(qiáng)依賴請求時的上下文,這樣容易引起不必要的狀態(tài)管理。
比如,我看到我們公司的一個項目中曾經(jīng)出過這樣的 bug :UI 界面上點擊一個按鈕,用 callback 的形式發(fā)起了一次 RPC 調(diào)用;在回應(yīng)的 callback 函數(shù)中對 UI 界面上的元素做了一系列的修改。可是在回應(yīng)的網(wǎng)絡(luò)消息收到時, UI 界面已經(jīng)關(guān)閉了,由于處于節(jié)約內(nèi)存的考慮,還觸發(fā)了一些對象銷毀流程,結(jié)果因為操控不存在的對象造成了 bug 。
你可以從別的角度來看待這個 bug 。比如說應(yīng)該建立一個更穩(wěn)固的 UI 框架,做更嚴(yán)謹(jǐn)?shù)纳诠芾怼5艺J(rèn)為,根源問題就是沒有把請求和回應(yīng)解耦造成的。
我們應(yīng)該這樣看待按鈕點擊事件:它只是觸發(fā)了一個事件在服務(wù)器上運行,這個事件導(dǎo)致了某個服務(wù)器上的對象狀態(tài)改變了,而回應(yīng)包只是通知了狀態(tài)改變的結(jié)果。客戶端真正要做的是怎樣正確呈現(xiàn)這個結(jié)果。
如果我們把客戶端處理網(wǎng)絡(luò)包的流程都?xì)w納成類似的模型,統(tǒng)一成一個簡單的框架就很容易了。
客戶端根據(jù)收到的網(wǎng)絡(luò)包進(jìn)行相應(yīng)的處理,無論這個網(wǎng)絡(luò)包是服務(wù)器的推送、還是對之前客戶端發(fā)起請求的回應(yīng)。在這個框架下,只關(guān)心來了一個網(wǎng)絡(luò)包后的類別,根據(jù)這個類別來決定要做哪類事情。處理流程是關(guān)聯(lián)在消息類別上的,而不是關(guān)聯(lián)在單個請求上的。
對于請求回應(yīng)的處理,其實和推送的處理并沒有本質(zhì)的不同。只是實現(xiàn)上略有差別。對應(yīng)回應(yīng)包的處理,處理流程得到的參數(shù)不僅僅是網(wǎng)絡(luò)包,還需要有對應(yīng)的請求數(shù)據(jù)(也就是客戶端當(dāng)初自己發(fā)起請求的參數(shù))和這個事件綁定的客戶端本地對象。
通常我們會用一個 session 號來關(guān)聯(lián)回應(yīng)包和請求包,在發(fā)起請求時,不僅把請求內(nèi)容打包發(fā)給服務(wù)器,同時還把它記錄在本地,用 session 關(guān)聯(lián)起來;這樣在收到請求時,可以根據(jù) session 找回當(dāng)初請求的參數(shù),以及請求的類型,這樣就可以不必讓服務(wù)器推送完整的狀態(tài)值,客戶端可以自己找到匹配的內(nèi)容。
在大部分情況下,我們還需要在發(fā)起請求時,給這個 session 綁定一個本地的對象。雖然請求本身肯定有這個信息(否則服務(wù)器就不知道該請求到底操作呢什么東西),但額外的一個本地對象使用起來更方便,可以用來攜帶少量本地狀態(tài)信息。在回應(yīng)抵達(dá)時,直接操作這個綁定對象寫起來更方便。
如果用 lua 來實現(xiàn),看起來大致是這樣的:
send_request(obj, req_type, req_data)
-- 發(fā)起一個請求,請求類型為 req type ,參數(shù)是 req data ,綁定對象為 obj 。
function net.req_type(obj, req_data, resp_data)
-- 定義一個函數(shù)來處理 req_type 這種類型的消息,可以獲得發(fā)起請求時的 obj 和 req data 以及服務(wù)器的回應(yīng) resp data 。