《Exploring in UE4》網路同步原理深入(下):原理分析

遊資網發表於2019-05-15
接上篇:《Exploring in UE4》網路同步原理深入[原理分析]

七.可靠資料傳輸

UE4預設收發包都在主執行緒處理,收包可以通過控制CVarNetIpNetDriverUseReceiveThread來開啟執行緒單獨處理。

《Exploring in UE4》網路同步原理深入(下):原理分析
發包堆疊

《Exploring in UE4》網路同步原理深入(下):原理分析
收包堆疊

1.資料包格式

這裡再次拿出來一般網路資料包的格式,可以看到虛幻裡面的網路包是精確到bit的,這些資訊都可以通過FBitWriter與FBitReader去讀與寫。

《Exploring in UE4》網路同步原理深入(下):原理分析
網路包分為Ack與Bunch兩種

《Exploring in UE4》網路同步原理深入(下):原理分析
對於ActorChannel,Bunch分為屬性Bunch與RPC Bunch

平時看函式堆疊的時候我們可能看到Bunch、RawBunch、Packet、RawPacket等。所謂的Bunch就是上面圖所展示的(ActorChannel傳送資料的Bunch分為屬性Bunch與RPC Bunch),Bunch如果太大就會被拆分成很多個小的Bunch,一旦拆分成小的bunch那麼這個bunch就不是一個完整的bunch(就可以叫做一個Rawbunch,具體邏輯在UChannel::SendBunchInner裡面),這些bunch可以都被塞到一個Sendbuffer裡面,如果這樣直接發出去,就是一個Packet。每一個Sendbuffer發出前還可能會被PacketHandler處理,處理之後就是RawPacket。按照這樣的理解,你就能看懂下面的堆疊了。

《Exploring in UE4》網路同步原理深入(下):原理分析
客戶端與伺服器通過ControlChannel建立連線的某次通訊堆疊

另外,由於平時我們的發包都是按照最小單位Byte來傳送的,UE4裡面又精確到bit。所以會在Sendbuffer最後面新增1bit的結束標誌位,另一端在收到包的時候就可以先找到最後一個為1的bit,把後面的0刪除,前面剩下的就是原始的網路包。

2.PacketHandler

PacketHandler是用來對原始資料包(Packet)進行處理的一個“工具”,裡面可以自由的新增元件來對原始資料包進行多層處理,目前引擎內建的元件有握手元件StatelessConnectHandlerComponent、各種加密元件FEncryptionComponent、可靠資料傳輸元件ReliabilityHandlerComponent等。由於元件的不確定性,所以網路的訊息包頭也是不確定的。比如加密元件可能會對一個Packet進行加密,然後在前面新增一個2bit的頭部以及1bit的結束標誌位,也因此各個元件應該固定的順序處理packet。預設情況下回一直存在一個StatelessConnectHandlerComponent元件。

由於PacketHandle元件可能對原有的packet進行加密從而導致位發生變化,所以PacketHandle元件本身也會對處理過的資料後新增一個bit的結束標誌位。

《Exploring in UE4》網路同步原理深入(下):原理分析

3.Bunch的傳送時機

每次只要執行sendrawbunch(可能在netdrivertick裡worldtick裡面的代理tick,也可能在worldtick裡tickgroup裡面)就會設定TimeSensitive為true,就會觸發flushnet,所以說只要每幀有資料就會傳送。只要裡面有sendbuffer或者到時間了就會觸發lowlevelsend,呼叫socket的傳送

《Exploring in UE4》網路同步原理深入(下):原理分析

4.可靠資料傳輸的實現

可靠資料傳輸的基本原理就是接收方對每一個包都要做Ack回應,如果接收方沒收到Ack,那麼就要進行重傳。

UE4底層預設是主動重傳,只要沒有按順序收到bunch就會重傳。每個包有一個OutPacketId(記錄在Connection裡面),一個packet可能包括N個Bunch,每個bunch也會記錄當前所在的OutPacketId。

簡單來說,傳送端會記錄一個已經傳送成功的序號(已經收到的Ack.OutAckPacketId)假如傳送端發了10個包(1-10),接收端收到了1那麼會回覆一個ack,裡面是OutAckPacketId 1。然後發生丟包,接收端收到了序號5,那麼就會回覆一個ack5,這時候傳送端會更新當前的OutAckPacketId並重傳序號2-4所有的packet(儲存在connection的快取裡面)。所以,可以保證所有的包到上層都是嚴格有序的。

  1. if( AckPacketId>OutAckPacketId )
  2. {
  3.         for (int32 NakPacketId = OutAckPacketId + 1; NakPacketId<AckPacketId; NakPacketId++, OutPacketsLost++, OutTotalPacketsLost++, Driver->OutTotalPacketsLost++)
  4.         {
  5.                 UE_LOG(LogNetTraffic, Verbose, TEXT("   Received virtual nak %i (%.1f)"), NakPacketId, (Reader.GetPosBits()-StartPos)/8.f );
  6.                 ReceivedNak( NakPacketId );
  7.         }
  8.         OutAckPacketId = AckPacketId;
  9. }
複製程式碼

除了Bunch裡面的OutPacketId外,每個channel裡面的還有一套ChSequenceID,記錄了當前通道內可靠資料包的序號,每次傳送加1。每個Connection裡面會有N個Channel,每個Channel發出去的可靠資料包的數量會以Connection->OutReliable陣列的形式儲存,而真正發出去與接收到的資料包會快取在OutRec連結串列與InRec連結串列連結串列裡面,每次傳送一個資料包就會新增到OutRec裡面並設定其Ack狀態為0,收到一個Ack的時候就會遍歷當前Channel的OutRec連結串列,將對應Ack設為1,呼叫Channel::ReceivedAcks()並清空OutRec中被確認過的前面的所有快取。OutRec並沒有限制大小,所以理論上這裡會出現記憶體溢位的情況,不過在邏輯上層還有一些自己的處理機制,比如Channel可以設定閾值,超過閾值就退化成停等協議,具體內容請參考UChannel::SendBunchInner。

每個通過有一個ChIndex,connection在接收Bunch的時候可以通過這個Index找到對應的Channel再下發訊息。

5.屬性的可靠傳輸

首先要確認一點,屬性同步本身並不是可靠的,也就是他的屬性bunch所在的packet如果丟失並不會將這個packet重新傳送。只有Actor在第一次同步的時候才會設定合格屬性bunch為Reliable

  1. // Send initial stuff.
  2. //UActorChannel::ReplicateActor
  3. if( OpenPacketId.First != INDEX_NONE && !Connection->bResendAllDataSinceOpen )
  4. {       //第一次收到spawn的ack會把後面不可靠的屬性也重新同步一遍
  5.         if( !SpawnAcked && OpenAcked )
  6.         {
  7.                 // After receiving ack to the spawn, force refresh of all subsequent unreliable packets, which could
  8.                 // have been lost due to ordering problems. Note: We could avoid this by doing it in FActorChannel::ReceivedAck,
  9.                 // and avoid dirtying properties whose acks were received *after* the spawn-ack (tricky ordering issues though).
  10.                 SpawnAck
  11. ed = 1;
  12.                 for (auto RepComp = ReplicationMap.CreateIterator(); RepComp; ++RepComp)
  13.                 {
  14.                         RepComp.Value()->ForceRefreshUnreliableProperties();
  15.                 }
  16.         }
  17. }
  18. else
  19. {       //第一次同步是可靠的
  20.         Bunch.bClose = Actor->bNetTemporary;
  21.         Bunch.bReliable = true; // Net temporary sends need to be reliable as well to force them to retry
  22. }
複製程式碼

那麼屬性是怎樣做到可靠的呢?我發現即使接收方即使接收到的Packet裡面的bunch不是reliable的,在通道不關閉、不是拆分的Bunch等情況下還是會回覆一個Ack的,所以傳送端可以接收到一個Ack從而知道當前的屬性是否被另一端接收到。

當發生丟包或者亂序的時候,RepState就會記錄當前Nak的數量,並對當前的同步傳送歷史資訊進行標記

  1. void FRepLayout::ReceivedNak( FRepState * RepState, int32 NakPacketId ) const
  2. {
  3.         if ( RepState == NULL )
  4.         {
  5.                 return;                // I'm not 100% certain why this happens, the only think I can think of is this is a bNetTemporary?
  6.         }

  7.         for ( int32 i = RepState->HistoryStart; i < RepState->HistoryEnd; i++ )
  8.         {
  9.                 const int32 HistoryIndex = i % FRepState::MAX_CHANGE_HISTORY;

  10.                 FRepChangedHistory & HistoryItem = RepState->ChangeHistory[ HistoryIndex ];

  11.                 if ( !HistoryItem.Resend && HistoryItem.OutPacketIdRange.InRange( NakPacketId ) )
  12.                 {
  13.                         check( HistoryItem.Changed.Num() > 0 );
  14.                         HistoryItem.Resend = true;
  15.                         RepState->NumNaks++;
  16.                 }
  17.         }
  18. }
複製程式碼

《Exploring in UE4》網路同步原理深入(下):原理分析

當下一幀要進行屬性同步的時候,就會把之前的歷史記錄合併到最新的歷史記錄裡面,然後一起發出去,這樣達到了不用重發丟失的bunch還能保證屬性可靠的效果了。這一塊的邏輯主要在FRepLayout::ReplicateProperties裡面,關於屬性變化的歷史記錄可以參考上面第五章第4小節。

八.ReplicationGraph

ReplicationGraph是Epci官方針對堡壘之夜網路同步優化而加入的新的外掛系統,可以大大減少Actor的同步與遍歷,比較適合對大世界場景進行網路同步優化。這一塊已經有文章寫的比較清晰了,所以我只是簡單的列舉其優化點與基本原理。

通常伺服器在同步Actor到各個連線的時候,會遍歷場景中所有標記Replicated的Actor,但是實際上與玩家距離比較遠的根本就不需要遍歷,更不用說同步,所以ReplicationGraph加入了GridSpatialization2D節點系統,把N*N的格子,並把Actor放到當前所有與他有關的格子裡,這樣一個玩家靠近他的時候就從當前子集所在的格子裡面找一下有沒有那個Actor就可以了(只遍歷所有在這個格子裡的Actor即可)

《Exploring in UE4》網路同步原理深入(下):原理分析

當然,作為一個系統不僅僅是提供這樣一種功能和優化,其裡面還內建了很多節點用於不同的同步需求(比如可以對不同的Connection進行某一個Actor的特定屬性進行共享序列化),你也可以自定義一個節點專門處理某些需要特殊處理的Actor,嚴格控制它的同步時機。這一塊可以參考官方的Shootergame專案。

注:ReplicationGraph是一個純C++外掛系統,使用的話需要修改配置檔案DefaultEngine.ini裡面的內容。

作者:Jerish
專欄地址:https://zhuanlan.zhihu.com/p/55596030

相關文章