1. 程式人生 > >回合制遊戲網路通訊協議及心跳機制調研

回合制遊戲網路通訊協議及心跳機制調研

回合制策略遊戲

回合制策略遊戲是策略遊戲的一種子型別,所有的玩家輪流自己的回合,只有自己的回合,才能夠進行操縱。早期的戰略由於硬體運算能力有限,在考量遊戲樂趣的情況下,多半採取這種型式。

主要分類
戰棋類遊戲
SLG:角色扮演因素較少,戰鬥以整體策略為主。
SRPG:角色扮演因素為主,戰鬥為回合制,通常己方人員較少,特別依靠培養系統鑄造的強人。
半即時制
使用行動點數系統,行動點數基於角色行動需要而設定的耗時系統或者行動速度的差異。半即時回合制是按照人物角色的速度來決定行動的先後順序,速度快的角色可能一個回合可以攻擊多次,是回合制遊戲的發展趨勢。

遊戲後臺網路通訊

網路通訊本身是非常複雜的事情,目前的開發環境已經提供了相對來講簡單得多的程式設計介面,但是網路程式還是需要處理很多的問題。
兩種處理方式:一種是跟遊戲伺服器耦合帶一起,遊戲伺服器既處理問落接入相關的邏輯,也處理遊戲邏輯。一種是把網路通訊部分剝離住來,向遊戲伺服器提供一種以訊息為單位的、非阻塞的、有Qos能力的中間服務,遊戲伺服器看不到網路的細節。
我們選擇第二種好處,是基於這樣的考慮,首先簡化了遊戲伺服器的處理邏輯,降低了程式設計的難度。更易於提升後臺整體的處理效能,不同部分可以獨立的優化,因為它可以不斷的優化和在不同專案裡面去繼承的。
這是非常簡單的一張示意圖,我們會有一個介面,在客戶端和伺服器端給它不同的介面,會有不同的形式,看不到網路的問題,而且都是非阻塞的形式,面向訊息的服務,類似於有保障的、可持續的服務。

遊戲通訊協議

協議分兩大類:文字協議和二進位制協議,這是兩個非常典型的例子,一是UDP,非常高效。一是文字協議,都是文字。文字協議直觀,版本相容性好,但是效率低。其實對於遊戲來講,我們最好能夠做到不同的版本都可以玩,不強調所有的客戶端都去升級,這對運營商有非常強的週期,這對運營商來說有很大的挑戰。版本相容性問題其實是我們在通訊協議的時候,都需要重點考慮的。二進位制協議效率高,但是不直觀,版本相容性處理相對複雜。
Web相關的遊戲根據與瀏覽器互動的方式可能採用文字協議,基於效率原因,C/S型別的遊戲通常採用二進位制協議。

遊戲伺服器是時鐘訊息和網路訊息驅動的,大部分程式碼都是接受訊息,或者接納,其實有很多的解碼、編碼、程式碼佔了相當大的比例。我們可以把協議做一個區分,就變成一個訊息的協議描述,然後生成工具,然後得到網路協議處理程式碼。

從遊戲來講,所有的線上遊戲通常使用資料庫來儲存使用者資料。通常MMO使用關係型資料庫來儲存資料,後面主要針對MMO進行儲存方式的討論。會有兩種方式:一種是把遊戲的每一個數據物件的屬性看成一個單獨欄位,遵循RDBMS的要求來設計資料庫表和索引,儘量符合3NF。以MMO為例,有帳號表、角色基本資訊表、物品表、裝備表等等,這是一種方式。
還有一種方式更具體,角色的列表類資料儘量採用blob來儲存而不是另一個表。原則是這些列表資料只被角色自身所擁有,就是這個玩家所擁有,其他玩家不會擁有個資料,它的生命週期跟玩家是一致的,不存在其他的交叉擁有情況,技能、物品、裝備、任務、好友等等都屬於這種情況。
優點是儲存表結構簡單,通常幾張表就可以玩一個遊戲,不超過10個。存取互動簡單,角色登入或者推出時通常只需要存取一到二條記錄。同一個角色的資料易於保持一致,易於多版本資料共存。我們把這些資料存到資料庫的時候,會把編碼存到資料庫裡面。所以在資料庫裡面做完的資料可能會不一樣,不過不會影響,它會共存。
這種方式也會有缺點,資料維護工具、客服工具實現相對複雜,需要提供特殊的API來操作資料。如果手上工具是通用的,可能比以前要直白一點。某些型別的統計相對要麻煩一些,有些常用的資料,比如說角色的等級,在這方面可以用一些方式解決你的問題。
舉個例子,在MMOG這塊,儲存角色的概要資訊,包括名字、基本屬性等,用於顯示角色列表和防止重名。還有儲存角色的詳細資訊,儲存帳號的倉庫資訊,儲存公會的資訊。
新趨勢的影響,就是Nosql資料庫,效能高,存在好的開源實現,遊戲的資料訪問多為唯一鍵訪問,很少複雜的Query,符合Nosql資料庫的特點。後面在遊戲應用上,可能也會涉及到。

網路同步

網路同步面臨的主要問題:第一如何減少網路波動對同步的影響;第二如何減少外掛對同步的破壞。如果沒有外掛,網路幾大問題沒有伺服器去執行。這兩個問題單獨都好解決,但是在一起比較難解決。我們解決這兩個問題,會遵循幾個原則:第一網路條件好的玩家獲得好的體驗;第二網路條件差的玩家儘可能獲得好一些的體驗,但不能拖累其他玩家的體驗;第三外掛不能在網路同步方面獲得持續的好處。對外掛方面,玩一個遊戲是一個人,或者說非人類不清楚,所以說外掛不能在網路同步獲得持續的好處。
為了解決問題我們有一些基本方法:首先要探測玩家的網路質量;第二在玩家機器與伺服器之間進行時鐘同步;第三基於遊戲特點,設計合理的同步機制。像競技類的遊戲,都是根據它的某些特點決定的,這是需要我們權衡考慮的。這裡強調一點,在外掛獲得好處,跟玩家體驗時間做一個折中,你要保證外掛持續得到源源不斷的好處,這樣外掛就會上去。對於探測、時鐘同步都需要控制好。

TCP / UDP

UDP:使用者資料報協議:主要用在實時性要求比較高的以及對質量相對較弱的地方.但是面對現在高質量的線路不會容易丟包,除非是一些擁塞條件下,如流媒體;
TCP:傳輸控制協議:是面連線的那麼執行環境必然要求其可靠性不可丟包,有良好的擁塞控制機制如 http ftp telnet等;

TCP UDP
傳送 安全送達
接收與建立連線 是(三次握手)
資料大小 無限制
可靠性 可靠
速度 慢(三次握手才能完成連線)
應用 流媒體

“心跳”機制

1.原理

心跳機制是定時傳送一個自定義的結構體(心跳包),讓對方知道自己還活著,以確保連結的有效性的機制。

發包方:可以是客戶端也可以是服務端,看哪邊實現方便合理。一般是客戶端

在TCP的機制裡面,本身是存在有心跳包的機制的,也就是TCP的選項(SO_KEEPALIVE)。系統預設是設定的是2小時的心跳頻率,探測次數為5次,如果要使用TCP的KeepAlive保活機制,需要手工開啟KeepAlive功能並設定合理的KeepAlive引數。但是它檢查不到機器斷電、網線拔出、防火牆這些斷線。而且邏輯層處理斷線可能也不是那麼好處理。一般,如果只是用於保活還是可以的。心跳包一般來說都是在邏輯層傳送空的包來實現的。下一個定時器,在一定時間間隔下發送一個空包給客戶端,然後客戶端反饋一個同樣的空包回來,伺服器如果在一定時間內收不到客戶端傳送過來的反饋包,那就只有認定說掉線了。只需要send或者recv一下,如果結果為零,則為掉線。

TCP的KeepAlive保活的缺陷
TCP協議自身先天就有KeepAlive機制,因為開啟KeepAlive功能需要消耗額外的寬頻和流量,所以TCP協議層預設並不開啟KeepAlive功能,另一方面,KeepAlive設定不合理時可能會 因為短暫的網路波動而斷開健康的TCP連線.

TCP仍然需要心跳包
在長連線下,有可能很長一段時間都沒有資料往來。理論上說,這個連線是一直保持連線的,但是實際情況中,如果中間節點出現什麼故障是難以知道的。更要命的是,有的節點(防火牆)會自動把一定時間之內沒有資料互動的連線給斷掉。在這個時候,就需要我們的心跳包了,用於維持長連線,保活。在獲知了斷線之後,伺服器邏輯可能需要做一些事情,比如斷線後的資料清理,重新連線,這個自然是要由邏輯層根據需求去做了。總的來說,心跳包主要也就是用於長連線的保活和斷線處理。一般的應用下,判定時間在30-40秒比較不錯。如果實在要求高,那就在6-9秒

   心跳機制的原理很簡單:客戶端每隔N秒向服務端傳送一個心跳訊息,服務端收到心跳訊息後,回覆同樣的心跳訊息給客戶端。如果服務端或客戶端在M秒(M>N)內都沒有收到包括心跳訊息在內的任何訊息,即心跳超時,我們就認為目標TCP連線已經斷開了。
   由於不同的應用程式對感知TCP掉線的靈敏度不一樣,所以,N和M的值就可以設定的不一樣。靈敏度要求越高,N和M就要越小;靈敏度要求越低,N和M就可以越大。而要求靈敏度越高,也是有代價的,那就是需要更頻繁地傳送心跳訊息,如果有幾千個連線同時頻繁地傳送心跳訊息,那麼其所消耗的資源也是不能忽略的。
   當然,網路環境(如延遲的大小)的好壞,也對會對N和M的值的設定產生影響,比如,網路延遲較大,那麼N與M之間的差值也應該越大(比如,M是N的3倍)。否則,可能會產生誤判 -- 即TCP連線沒有斷開,只是因為網路延遲大才及時沒收到心跳訊息,我們卻認為連線已經斷開了。

2.心跳檢測步驟

1.客戶端每隔一個時間間隔發生一個探測包給伺服器
2.客戶端發包時啟動一個超時定時器
3.伺服器端接收到檢測包,應該回應一個包
4.如果客戶機收到伺服器的應答包,則說明伺服器正常,刪除超時定時器
5.如果客戶端的超時定時器超時,依然沒有收到應答包,則說明伺服器掛了

3.應用層傳送心跳包的技術

由應用程式自己傳送心跳包來檢測連線是否正常,大致的方法是:
伺服器在一個Timer事件中定時向客戶端傳送一個短小精悍的資料包,然後啟動一個低級別的執行緒,在該執行緒中不斷檢測客戶端的迴應,如果在一定時間內沒有收到客戶端的迴應,即認為客戶端已經掉線;同樣,如果客戶端在一定時間內沒有收到伺服器的心跳包,則認為連線不可用。
缺陷:程式碼較多,稍顯複雜

4.要關閉掉線的TCP連線

無論是普通掉線(立即感知)還是心跳超時掉線(非立即感知),都需要關閉對應的TCP連線以釋放系統資源。

5.UDP與"心跳"

由於UDP是無連線的協議,所以,當使用UDP引擎時,幾乎肯定是需要配備心跳機制的,使用心跳訊息確認客戶端還線上,以保證服務端不會過早釋放對應的Session或長期保留已失效的Session.

心跳包和輪詢的區別

心跳包和輪詢看起來類似, 都是客戶端主動聯絡伺服器, 但是區別很大:

輪詢是為了獲取資料, 而心跳是為了保活TCP連線。
輪詢得越頻繁, 獲取資料就越及時, 心跳的頻繁與否和資料是否及時沒有直接關係
輪詢比心跳能耗更高, 因為一次輪詢需要經過TCP三次握手, 四次揮手, 單次心跳不需要建立和拆除TCP連線.

客戶端如何快速感知自己掉線?

在某些客戶端電腦上,比如拔掉網線,或斷開wifi,程式可能需要幾秒到幾分鐘才能感知到自己掉線,不同的電腦這個感受的時間不一樣。那麼如何才能讓客戶端儘可能快地得到掉線通知了?
可以利用socket寫超時的機制,像下面這樣做:
(1)將Socket傳送緩衝區的大小設定為0。 對應IRapidPassiveEngine的Advanced屬性的SocketSendBuffSize屬性。
(2)設定寫超時為一個較小的值,如30秒。 對應IRapidPassiveEngine的Advanced屬性的WriteTimeoutInSecs屬性。
這樣,結合上面的心跳傳送機制(如每隔5秒傳送一個心跳),則當網路斷開後,在傳送心跳訊息,最多再過30秒,程式就會得到掉線通知了。

HOW

應用層心跳的確是檢測連線有效性,雙方是否存活的最佳實踐,那麼剩下的問題就是怎麼實現。

最簡單粗暴做法當然是定時心跳,如每隔 30 秒心跳一次,15 秒內沒有收到心跳回包則認為當前連線已失效,斷開連線並進行重連。這種做法最直接,實現也簡單。唯一的問題是比較耗電和耗流量。以一個協議包 5 個位元組計算,一天收發 2880 個心跳包,一個月就是 5 * 2 * 2880 * 30 = 0.8 M 的流量,如果手機上多裝幾個 IM 軟體,每個月光心跳就好幾兆流量沒了,更不用說頻繁的心跳帶來的電量損耗。

既然頻繁心跳會帶來耗電和耗流量的弊端,改進的方向自然是減少心跳頻率,但也不能過於影響連線檢測的實時性。基於這個需求,一般可以將心跳間隔根據程式狀態進行調整,當程式在後臺時(這裡主要考慮安卓),儘量拉長心跳間隔,5 分鐘,甚至 10 分鐘都可以。而當 App 在前臺時則按照原來規則操作。連線可靠性的判斷也可以放寬,避免一次心跳超時就認為連線無效的情況,使用錯誤積累,只在心跳超時 n 次後才判定當前連線不可用。當然還有一些小 trick 比如從收到的最後一個指令包進行心跳包週期計時而不是固定時間,這樣也能夠一定程度減少心跳次數。

參考: