之前已經介紹過, skynet 只是一個輕量框架,不是一個開箱即用的引擎 。能不能用好它,取決於使用者是否清楚知道自己要乾什麼,如果是用 skynet 做網路游戲伺服器,那麼就必須先知道網路游戲伺服器應該如何設計。 在 skynet 發佈版中帶的 example 中,有類似 gate watchdo ...
之前已經介紹過, skynet 只是一個輕量框架,不是一個開箱即用的引擎 。能不能用好它,取決於使用者是否清楚知道自己要乾什麼,如果是用 skynet 做網路游戲伺服器,那麼就必須先知道網路游戲伺服器應該如何設計。
在 skynet 發佈版中帶的 example 中,有類似 gate watchdog agent 之類的服務,它們並不是唯一的用 skynet 構建游戲伺服器的模式。我想另外寫一個範例,示範依舊基於 skynet 但用不同的模式構建游戲伺服器的方法。
我花了兩天時間寫了這麼一個 sample ,放在 github 上 。
在這個範例中,我主要想展示這樣一些東西:
GateServer 並不是唯一的管理連接的模式。在 skynet 中,也可以自定義其它的方式來管理大量外部連接。這個例子中使用了前段時間我實現的另一個模塊 ,這個模塊並沒有放在 skynet 發佈版中。
在這個範例中,實現了一個 hub 服務,類似 gate 的作用。但是它僅僅監聽埠,並把新建連接交給合適的服務處理。按範例中的流程,每個新連接都直接轉交 auth 服務;只有 auth 服務認可了連接,再轉給 manager 服務。
這裡 auth 和 manager 都是單一服務。如果實際使用的時候有性能問題,auth 服務可以擴展為多個,做負載均衡。如果有必要,還可以加一個排隊服務的環節。
manager 拿到連接的身份後,會根據身份分派 agent 服務代理這個連接上的請求。註意:這裡的代碼並沒有簡單的為已認證身份的連接啟動一個新的 agent 。這也是很多對 skynet 缺乏瞭解的同學普遍的誤解——skynet 一定會為每個鏈接創建一個獨立的 lua vm 。
manager 管理若幹 agent 的原則是,如果系統中沒有為特定用戶服務的 agent 存在,則啟動一個新的。但即使這個用戶連接斷開,也不一定及時退出 agent 服務。agent 是否退出,是由 agent 自己決定的。manager 只負責將用戶關聯到活著的 agent 服務上。這個關聯關係面向用戶而不是面向連接的,多個連接可以同時通過 auth 認證,一起關聯到同一個 agent 服務上(比如多客戶端同時以不同連接接入)。
manager 服務目前實現的還很簡陋,但是稍加改造,就可以支持把多個用戶關聯到同一個 agent 。比如,做棋牌伺服器時,你可能讓同一個牌桌的用戶在一起會更好。
agent 服務可以用來處理業務邏輯。目前的範例中僅能處理 login 和 ping 請求。我們區分了 signin 和 login 。signin 表示用戶已經通過了認證進入系統,但未必可以進行業務請求;而 login 表示被 agent 接受。這個範例里,如果一個用戶 login 成功,在他的連接斷開前,這個用戶無法再次 login ;當然你可以稍微改造,變成後login 的用戶頂掉前一個;或是讓他們可以同存。
這個範例還提供了一個不同於 snax 另一個簡單封裝。展示如何不用 skynet 早期提供的具名服務方式,而使用 skynet.uniqueservice 來取代它們。
在這個範例封裝中,只需要聲明服務依賴的其它服務的名稱就可以以正確的次序啟動它們了。
封裝層把 skynet.dispatch skynet.info_func 等在編寫 skynet 服務時的繁瑣步驟簡化了,它的工作原理理解起來可能比 snax 要簡單一些,用起來也很容易。
這次的客戶端使用了一個開源的 lsocket 庫,而不是 skynet 發佈版中那個簡陋的 clientsocket 模塊。這能更好的暫時怎麼編寫 skynet 的客戶端。
同時,客戶端中使用 sproto 協議的代碼也更清晰一點,稍微做了一些封裝,讓代碼比 skynet 自帶的 example 更易讀。
伺服器部分和客戶端交互的部分也有對應的封裝模塊。
關於客戶端部分,我比較推崇只使用請求/回應模式,而不支持伺服器推送數據。如果需要推送,可以用 long polling 解決。
在客戶端,和伺服器不同,它要同時面對用戶 UI 的交互、圖像渲染、以及網路請求回應。所以我覺得不適合把伺服器的那套 rpc 機制直接搬到客戶端。所以在範例中,我也並沒有使用 coroutine 來做 rpc 調用。
callback 模式可能更適合客戶端的工作。但 callback 並不是 rpc_call(request, cb) 這種。而是把 request (只可以從客戶端發起,伺服器永遠只響應客戶端的請求,而沒有反向請求)的回應處理方法註冊在一張表中。
比如,有一個叫做 ping 的請求,客戶端先定義好:
function ping(req, resp, session)
然後在需要 ping 伺服器(對於客戶端來說通常是由用戶 UI 操作引起的)時,local session = request("ping", req) 就可以了。當收到伺服器的 ping 回應時,上面的 ping 函數被回調,可以接收到當初 request 時發起的 req 數據,以及伺服器傳回的 resp ,和 session 。
如果 ping 操作是無狀態的,那麼 session 多半可以忽略掉。在回調函數中,我們可以拿到提起請求時的內容 req ,也就不必再依賴其它狀態了。
有部分流程,可能依賴多次和伺服器交互。這種帶上下文的交互,或許我們應該用 coroutine 封裝一下這類 RPC 調用?但目前最常見的多次交互只出現在登錄認證流程中,似乎不必為它單獨做太複雜的東西。