動作手遊實時PVP幀同步方案(圖解客戶端)
1、概述
1.1、基於UDP的幀同步方案
在技術選型方面,之所以選擇幀同步方案,在Kevin的一篇介紹PVP幀同步後臺實現的文章中已經做了詳細敘述,這裡簡單摘要如下:
高一致性。如果每一幀的輸入都同步了,在同樣的上下文中,計算得出的結果應該也是同步的。
低流量消耗。除了幀同步,其它方案(比如狀態同步)想做到高一致性,需要同步非常大量的資料。無論是對於行動網路,還是固絡都是不合適的。
伺服器邏輯簡化。採用幀同步方案,伺服器只需要做簡單的幀同步,不需要關心太多的業務細節。有利於客戶端功能的擴充套件和伺服器的穩定和效能。
反作弊。客戶端只需要在適當機時上報校驗資料給伺服器,伺服器對2個客戶端上報的資料進行對比,就可以快速識別是否有人作弊。然後通過無收益的方式間接防止作弊。
那麼,為什麼選擇UDP而不是TCP呢?主要有2點原因:
弱網路環境。
實時性要求。
我們通過一個測試APP,在WIFI和4G環境下,採用TCP和UDP兩種方式連線同一個伺服器,分別獲得對應的RTT進行對比。
我們可以發現,在弱網路環境下,UDP的RTT幾乎不受影響。而TCP的RTT波動比較大,特別是受丟包率影響比較明顯。
1.2、基於UDP的FSP協議棧
由於UDP具有不可靠性,所以在UDP的基礎上實現一個自定義的協議棧:FSP,即FrameSyncProtocol。
FSP的基本原理就是防照TCP的ACK/SEQ重傳機制,實現了傳輸的可靠性,同時還採用冗餘換速度的方式,又保證了傳輸的**速率。在幀同步方案中一舉兩得。
2、技術原理
2.1、幀同步技術原理
如下圖所示,客戶端A的操作A1與客戶端B的操作B1封裝成OperateCmd資料傳送給PVP伺服器。PVP伺服器每66MS產生一個邏輯幀,在該幀所在時間段內收到A1和B1後,生成一個Frame資料塊,在該幀時間結束時,將Frame傳送給客戶端A和B。Frame資料塊內有該幀的幀號。客戶端A和B收到Frame資料後,便知道該幀內,客戶端A和B都做了什麼操作。然後根據收到的操作A1和B1進行遊戲表現,最終呈現給玩家A和B的結果是一致的。從而實現客戶端A與B的資料同步。
圖1 幀同步技術原理
2.2、FSP協議棧原理
如下圖所示,傳送者維持一個傳送佇列,對每一次傳送進行編號。每一次傳送時,會將待發送的資料寫入佇列。然後將佇列裡的資料+編號傳送給接收者。
接收者收到資料後,會將該編號回送給傳送者以確認。傳送者收到確認編號後,會將該編號對應的資料包從佇列中刪除,否則該資料仍儲存在傳送佇列中。
下次傳送時,會有新的資料進入佇列。然後將佇列中的資料+最新的編號傳送給接收者。以此迴圈反覆。
圖2 FSP協議棧原理
上圖解析:
第1次傳送,在傳送佇列裡只有Data1,於是將Data1和編號1(Seq=1)傳送給接收者。收到確認編號1(Ack=1)後,將Data1從佇列中刪除。
第4到7次傳送,由於從第4次傳送開始就沒有收到確認編號,於是佇列中包含了Data4到Data7。第7次傳送後,收到確認編號6,於是將Data4至Data6從佇列中刪除。
第8次傳送,佇列中包含Data7和Data8。傳送後收到確認編號8,從而將Data7和Data8從佇列中刪除。
以上的關鍵點是,傳送者未收到確認編號,並不一直等待,而是會繼續下一次傳送。結合圖1:
如果傳送者是伺服器,則會每隔66MS會將一個Frame資料寫入傳送佇列,然後將該佇列裡的所有Frame資料一起傳送給客戶端 。
如果傳送者是客戶端,則會在玩家有操作時,將玩家的每一個OperateCmd資料寫入傳送佇列,然後將該佇列裡的所有OperateCmd資料一起傳送給伺服器 。如果傳送佇列不為空,則每隔99MS重複傳送。如果傳送佇列為空,則不再發送。直到玩家下一次操作。
由於伺服器和客戶端即是傳送者,又是接收者。則伺服器和客戶端的每一次傳送,除了會帶上該次傳送的編號,還會帶上對對方傳送編號的確認 。
3、技術實現
3.1、整體框架
圖3 PVP通訊模組整體框架
這是一個典型的手遊PVP通訊模組的整體框架。這裡主要分享一下FSP模組和幀同步模組的技術實現。
3.2、FSP模組
FSP模組主要用來實現FSP協議棧。其協議格式定義如下。
FSP上行協議定義:
Seq
Ack
SomeData
OperateCmd List
CheckSum
FSP下行協議定義:
Seq
Ack
Frame List
CheckSum
如下圖所示,是FSP模組的接收邏輯流程。
圖4 FSP模組接收邏輯流程
其中關鍵點是:
對Recv New Ack判斷,對曾經發送過的Operate進行確認刪除。
對Recv New Seq判斷,過濾掉因為網路問題造成亂序的包。
上圖中,接收到的Frame最終都儲存在RecvQueue中。我們將接收邏輯放在子執行緒中。所以只需要在主執行緒中需要Recv的時刻從RecvQueue中讀取FremeList即可。
如下圖所示,是FSP模組的傳送邏輯流程。傳送邏輯同樣放在子執行緒中。傳送邏輯有2種觸發方式:
業務層主動呼叫傳送
每隔指定時間觸發一次(在WIFI和4G下使用不同的時間,可以減少伺服器收到的純確認包比例,有利於提高通訊效能)
圖5 FSP模組主動傳送邏輯流程
圖6 FSP模組定時傳送邏輯流程
3.3、幀同步模組
下圖是幀同步模組的實現框架。
圖7 幀同步模組實現框架
按照上圖箭頭編號描述如下:
(1)負責接收來自FSP模組的FrameList。
(2)將FrameList裡的每1幀都存入FrameQueue。
(3)同時將FrameList的每1幀的幀號進行變換後,得到客戶端幀號。同時,在等下1個伺服器幀到來之前,需要將客戶端的幀鎖定在下1個伺服器幀的前一幀(LockFrameIndex)。然後 將FrameIndex和LockFrameIndex傳入FrameBuffer。
(4)客戶端每1幀從FrameBuffer中取出當前可能需要跳幀加速的倍數(SpeedUpTimes)。
(5)如果SpeedUpTimes為0,則表示正在緩衝中,沒有需要處理的幀。如果SpeedUpTimes是1,則表示緩衝結束,但是不需要加速,只需要處理最新的1幀。如果SpeedUpTimes大於1,則從FrameQueue裡取出這SpeedUpTimes個幀, 將裡面的SyncCmd取出來。
(6)將SyncCmd傳入OperationExecutor。
(7)OperationExecutor與具體遊戲的業務邏輯相關聯,負責將SyncCmd傳入給業務邏輯和預表現模組進行具體的處理。
其流程圖如下:
圖8 幀同步邏輯流程1
圖9 幀同步邏輯流程2
4、最新優化
4.1、斷線重連優化
在傳統網路模組開發思想中,當傳送超時達到閥值,或者底層判定斷開連線時,需要重新建立連線。之前這部分工作是交給一個偏上層的模組來執行,該模組需要等Apollo通訊模組連線成功之後,才進行PVP通訊模組的連線。這樣使邏輯變得複雜。
由於UDP本身的不可靠性,可以認為網路斷線也是其不可靠性的一部分。
而FSP協議棧就是為了解決UDP的不可靠性而設計的,所以也附帶解決了斷線重連問題。
去除了原來的斷線重連邏輯之後,用FSP模組本身的特性來處理斷線重連,實測能夠提高網路恢復的響應速度。由於PVP伺服器設定的超時閥值是15秒,有些時候,其實網路已經恢復,但是由於Apollo通訊模組對網路的恢復響應過於遲鈍,造成不必要的判輸。
4.2、接入GSDK
從目前接入GSDK後的資料來看,能夠減少一定的網路延時,但是並不明顯。
4.3、AckOnly優化
AckOnly優化是指減少伺服器收到的純確認包資料。這樣做的目的是:
減少包量,有助於在WIFI下節省路由器效能。GSDK有個統計表明,有大概20%多的網路延時是因為路由器效能造成。
節省流量,一定程度上也可以節省網路裝置效能,同時在4G下為使用者省錢。
該優化分2部分實現:
(1)空幀免確認
(2)WIFI延遲確認
在優化前的AckOnly比例為:57%
空幀免確認優化後降到:38%
WIFI延遲確認優化後降到:25%
5、一些嘗試
將FSP模組抽象得與業務無關,使之可快速完成一個使用幀同步方案通訊的Demo成為可能。
實驗了本地區域網PVP對局,只要在同一網段下,可以成功對局。(如果有需求,可以實現該功能)
實驗了本地藍芽PVP對局,發現藍芽是帶連線態的,並且其通訊是用類似TCP的資料流進行的。同時它與WIFI訊號有干擾,如果開啟WIFI,其延時非常高。在非WIFI下,其單條資料的延時很低,但是如果以66MS的頻率傳送資料,則延時又非常高。
建立了一套用於FSP線上診斷和斷線診斷的工具。