UE4 UDP是如何進行可靠傳輸的

毛寸發表於2021-05-18
TCP 和 UDP 都是具有代表性的傳輸層協議,很多時候我們都會拿它們做比較,區別如下:

UE4 UDP是如何進行可靠傳輸的

對它們的工作方式打個比喻:

TCP 就好比打電話,通話之前先撥通電話,通了之後互相對話,訊號不好的時候還是會詢問“喂喂喂?“、”你那邊能聽到嗎?“之類的確認對方能聽到才繼續通話,結束之後 say bye bye。

UDP 就好比寄信,提前把想說的全寫信裡,之後寄出去,然後就結束了。不清楚有沒有到對方手裡,也不清楚對方有沒有回信。

UDP 想要實現可靠性傳輸,通常的做法是在應用層模擬 TCP 的可靠性傳輸。比如

  • 新增傳送和接收緩衝區
  • 新增序列號和應答
  • 新增超時重傳,丟包重傳
  • 新增流量控制

理論歸理論,具體的實現又是怎樣的呢?小小的腦袋的我開啟了 UE4 的原始碼。。。

目錄

  • 前言
  • UE4
  • 網路框架
  • NetDrivers, NetConnections, and Channels
  • Initiating Connections / Handshaking Flow
  • 重新建立丟失的連線
  • 資料傳輸
  • 可靠性和重傳
  • 網路框架圖
  • 原始碼分析
  • 基本概念
  • 訊息的傳送
  • 訊息的接收
  • 流量控制
  • 總結


UE4

網路框架

NetDrivers, NetConnections, and Channels

UNetDrivers

網路驅動,網路處理的核心,負責管理 UNetConnections,以及它們之間可以共享的資料。對於某個遊戲來說,一般會有相對較少的 UNetDrivers,這些可能包括:

1、Game NetDriver:負責標準遊戲網路流量

2、Demo NetDriver:負責錄製或回放先前錄製的遊戲資料,這就是重播(觀戰)的工作原理。

3、Beacon NetDriver:負責不屬於“正常”遊戲流量的網路流量。

當然,也可以自定義 NetDrivers,由遊戲或應用程式實現並使用。

NetConnections

表示連線到遊戲(或更一般的說,連線到 NetDriver)的單個客戶端。每個網路連線都有自己的一組通道,連線將資料路由到通道。

Channel

資料通道,每一個通道只負責交換某一個特定型別特定例項的資料資訊。

1、Control Channel:用於傳送有關連線狀態的資訊(連線是否應該關閉等)。

2、Voice Channel:用於在客戶端和伺服器之間傳送語音資料。

3、Actor Channel:從伺服器複製到客戶端的每個 Actor 都將存在唯一的 Actor 通道。(Actor 是在世界中存在的物件,UE4 大部分的同步功能都是圍繞 Actor 來實現的。)

在正常情況下,只有一個 NetDriver(在客戶端和伺服器上建立)用於“標準”遊戲流量和連線。

伺服器 NetDriver 將維護一個 NetConnections 列表,每個連線代表遊戲中的一個玩家。它負責複製 Actor 資料。

客戶端 NetDrivers 將具有一個代表到伺服器的連線的單個 NetConnection。

在伺服器和客戶端上,NetDriver 負責接收來自網路的資料包並將這些資料包傳遞給適當的 NetConnection(必要時建立新的 NetConnections)。

Initiating Connections / Handshaking Flow.

UIpNetDriver 和 UIpConnection 是幾乎所有平臺引擎預設的,描述了它們如何建立和管理連線。伺服器和客戶端都將擁有自己的網路驅動程式,所有 UE 複製的遊戲流量都將被髮送或接收從 IpNetDriver。還包括用於建立連線的邏輯,以及在需要時重新建立連線的邏輯。

握手分為幾個不同的地方:NetDriver, PendingNetGame, World, PacketHandlers,也許還有其它地方。分開來是由於有不同的需要,例如:確定傳入連線是否在“UE 協議”中傳送資料,確定一個地址是否是惡意的,一個給定的客戶端是否有一個遊戲的正確版本,等等。

啟動和握手

當伺服器載入地圖(通過 UEngine:oadMap)時,我們將呼叫 UWorld::Listen。該程式碼負責建立主遊戲網路驅動程式、解析設定並呼叫 UNetDriver::InitListen。最終,這些程式碼將負責弄清楚我們究竟是如何監聽客戶端連線的。例如,在 IpNetDriver 中,我們將通過呼叫已配置的 Socket 子系統來確定要繫結到的 IP 和埠。一旦伺服器正在偵聽,就可以開始接受客戶端連線了。

每當一個客戶端想要加入一個伺服器時,它們首先會在 UEngine::Browse 中通過伺服器的 IP 建立一個新的 UPendingNetGame。UPendingNetGame::Initialize 和 UPendingNetGame::InitNetDriver 分別負責初始化設定和設定 NetDriver,作為初始化的一部分,客戶端將立即為伺服器設定一個 UNetConnection,並開始在該連線上向伺服器傳送資料,啟動握手過程。

在客戶端和伺服器上,UNetDriver::TickDispatch 通常負責接收網路資料。當我們收到一個資料包時,我們會檢查它的地址,看它是否來自我們已經知道的連線。我們只需儲存一個從 FInternetAddr 到 UNetConnection 的對映,就可以確定是否已經為給定的源地址建立了連線。如果資料包來自已經建立的連線,我們將通過 UNetConnection::ReceivedRawPacket 將資料包傳遞給連線。如果資料包不是來自已經建立的連線,我們將其視為“無連線”,並開始握手過程。

通知控制訊息

當 UNetDriver 和 UNetConnection 在客戶端和伺服器上完成握手過程後,客戶端將呼叫 UPendingNetGame::SendInitialJoin 來啟動遊戲級握手。遊戲級的握手是通過一組更加結構化和複雜的 FNetControlMessages 來完成的。可以在 DataChannel.h 中找到完整的控制訊息集。

處理這些控制訊息的大部分工作是在 UWorld::NotifyControlMessage 和 UPendingNetGame::NotifyControlMessage 中完成的。簡而言之,流程如下所示:

  • 客戶端的 UPendingNetGame::SendInitialJoin 傳送 NMT_Hello
  • 伺服器的 UWorld::NotifyControlMessage 接收 NMT_Hello,傳送 NMT_Challenge
  • 客戶端的 UPendingNetGame::NotifyControlMessage 接收 NMT_Challenge,發回資料 NMT_Login
  • 伺服器的 UWorld::NotifyControlMessage 接收 NMT_Login,驗證資料,然後呼叫 AGameModeBase:reLogin,如果 PreLogin 沒有報告任何錯誤,伺服器將呼叫 UWorld::WelcomePlayer,這將呼叫 AGameModeBase::GameWelcomePlayer 併傳送攜帶地圖資訊的 NMT_Welcome。
  • 客戶端的 UPendingNetGame::NotifyControlMessage 接收 NMT_Welcome,讀取地圖資訊(以便稍後開始載入),並以客戶端配置的網速傳送 NMT_NetSpeed。
  • 伺服器的 UWorld::NotifyControlMessage 接收 NMT_NetSpeed,並適當調整連線的網速。

在這一點上,握手被認為是完整的,玩家完全連線到遊戲。根據載入地圖所需的時間,客戶端進入 UWorld 之前仍然可以在 UPendingNetGame 上接收一些非握手控制訊息。如果需要的話,還可以使用其它步驟來處理加密。

重新建立丟失的連線

在整個遊戲過程中,可能會有很多原因導致連線丟失。網路可能退出,玩家可能離開遊戲,等等。如果伺服器啟動了其中一個斷開連線,或以其它方式意識到它(由於超時或錯誤),然後斷開連線將通過關閉 UNetConnection 並通知遊戲來處理。在這一點上,由遊戲來決定它們是否支援 Join In Progress 或者 Rejoins。如果遊戲確實支援它,我們將完全重新啟動握手流,如上所述。

如果某個東西只是短暫的中斷了客戶機的連線,但伺服器從未意識到,然後引擎或者遊戲通常會自動恢復(儘管有一些包丟失或者延遲峰值)。但是,如果客戶機的IP地址或埠由於任何原因發生更改,但伺服器沒有意識到這一點,然後我們將通過重做低階別握手來開始恢復過程。在這種情況下,遊戲程式碼不會被提醒。

資料傳輸

遊戲網路連線 NetConnections 和網路驅動 NetDrivers 通常與所使用的底層通訊技術方法或技術無關。這由子類決定,比如 UIpConnection、UIpNetDriver。相反,UNetDriver 和 UNetConnection 處理 Packets 和 Bunches。

Packets 是在主機和客戶機上的網路連線對之間傳送的資料塊,由關於 Packet 包的後設資料(如報頭資訊和確認 Ack)和 Bunches 組成。

Bunches 是在主機和客戶機上的通道對之間傳送的資料塊。當一個連線接收到一個資料包時,該資料包將被分解成單獨的 Bunch,這些 Bunch 然後被傳遞到單獨的通道以進一步處理。

一個 Packet 可以不包含 Bunch、單個 Bunch 或者多個 Bunch。由於 Bunch 的大小限制可能大於單個分組的大小限制,因此引擎支援部分 Bunch 的概念。當一個 Bunch 太大時,在傳輸之前,我們會把它切成許多小 Bunch,這些 Bunch 將被標記為 PartialInitial, Partial 或 PartialFinal。利用這些資訊,我們可以在接收端重新組裝 Bunch。

舉個例子:客戶端往伺服器傳送 RPC

  • 客戶端呼叫 RPC
  • 該請求被轉發(通過 NetDriver 和 NetConnection)到擁有呼叫 RPC 的 Actor 的 Actor 通道
  • Actor 通道將 RPC 識別符號和引數序列化為一個 Bunch。該 Bunch 還將包含其 Actor 通道的 ID
  • 然後,Actor 通道將請求 NetConnection 傳送 Bunch
  • 稍後,NetConnection 將把這些(和其他)資料組合成一個資料包 Packet,併傳送到伺服器
  • 在伺服器上,網路驅動程式 NetDriver 將接收資料包
  • 網路驅動程式 NetDriver 將檢查傳送資料包的地址,並將資料包移交給適當的網路連線 NetConnection
  • 網路連線 NetConnection 將資料包分解成 Bunch(一個接一個)
  • NetConnection 將使用 Bunch 上的通道 ID 將 Bunch 路由到對應的 Actor 通道
  • ActorChannel 解碼 Bunch,檢視它包含的 RPC 資料,並使用 RPC ID 和序列化引數
  • 對 Actor 呼叫對應的函式

可靠性和重傳

UE4 網路通常假定基礎網路協議不能保證可靠性,相反,它實現了自己的可靠性和 Packet、Bunch 的重傳。

當一個網路連線建立後,它將為它的 Packet 和 Bunch 建立一個序列號。這些可以是固定的,也可以是隨機的(隨機化後,序列將由伺服器傳送)。

資料包編號為每個網路連線的資料包編號,每傳送一個資料包,每個資料包都會包含其資料包編號,而且我們永遠不會重新傳輸具有相同資料包編號的資料包。

Bunch 序列號是每個通道的,每傳送一個可靠 Bunch 就遞增它的 Bunch 序列號。不過,與資料包不同的是,可以重新傳輸可靠的 Bunch 資料。這意味著我們將重新傳送具有相同 Bunch 序列號的 Bunch。

有一點要注意的是,在整個程式碼中,上面描述的 Packet 序列號和 Bunch 序列號通常都是序列號,只不過為了更清楚的理解,我們在這裡做了區分。

檢測接收的丟棄資料包

通過分配資料包編號,我們可以很容易的檢測到傳入的資料包何時丟失。這隻需要取最後一個成功接收的資料包編號和正在處理的當前資料包的資料包編號。

  • 在良好的條件下,所有資料包都將按傳送順序接收,這意味著差異將是 +1。
  • 如果差異大於 1,則表示丟失了一些資料包。我們只是假設丟失的資料包已被丟棄,但認為當前資料包已被成功接收,用它的號碼往前走。
  • 如果差值為負數或 0,則表示我們接收到的資料包有誤,或者是外部錯誤服務正在嘗試向我們重新傳送資料(請記住,引擎不會重用序列號)。

在這兩種情況下,引擎通常會忽略丟失或無效的資料包,並且不會為它們傳送 ack。我們確實有辦法修復在同一幀上接收到的無序資料包。啟用時,如果我們檢測到丟失的資料包(差異 > 1),我們將不會立即處理當前資料包。相反,會將其新增到佇列中。下一次成功接收資料包時(差異 = 1),我們將看看我們的佇列的頭排的是否正確,如果是,我們會處理,否則我們會繼續接收資料包。

一旦我們讀取了當前可用的所有資料包,我們將重新整理這個佇列來處理任何剩餘的資料包。在這一點上,丟失的任何東西都將被認為是被丟棄的。成功接收到的每個資料包都將其資料包編號作為確認(Ack)傳送回傳送方。

檢測傳送的丟棄資料包

如上所述,每當成功接收到包時,接收者將發回 Ack。這些Ack將按順序包含成功的接收的資料包的資料包序列號。與接收方跟蹤資料包序列號的方式類似,傳送方將跟蹤最高的已確認資料包序列號。當 Ack 被處理時,任何低於我們最後收到的 Ack 的 Ack 都將被忽略,並且資料包序列號中的任何間隙都將被視為未確認。傳送方負責處理這些 Ack 和 Nak 並重新傳送任何丟失的資料。新資料將被新增到新的傳出資料包中(同樣,我們不會重新傳送已經傳送的資料包,或者重用資料包序列號)。

重新傳送丟失的資料

如上所述,資料包本身並不包含有用的遊戲資料,相反,它們是由 Bunch 組成的有意義的資料。Bunch 可以被標記為可靠的或不可靠的。

如果不可靠的 Bunch 被丟棄,引擎將不會嘗試重新傳送它們。因此,如果被標記為不可靠,遊戲或引擎應該能夠在沒有它們的情況下繼續,或者必須建立外部重試機制,或者必須冗餘傳送資料。因此,以下所有內容僅適用於可靠 Bunch。

但是,引擎將嘗試重新傳送可靠的 Bunch。無論何時傳送可靠的 Bunch,它都將新增到未確認的可靠 Bunch 列表中。如果我們收到一個包的 Nak,引擎將重新傳輸該 Bunch 的精確副本。注意,因為 Bunch 可能是部分的,所以即使刪除一個部分 Bunch 也會導致整個 Bunch 的重新傳輸。當一個完整的 Bunch 的所有部分 Bunch 都已確認,我們將從列表中刪除它。

與資料包類似,我們將比較接收到的可靠 Bunch 的 Bunch 序列號與最後成功接收到的 Bunch 序列號。如果我們發現差異是負的,我們就忽略這個 Bunch。如果差異大於 1,我們將假設我們錯過了這個 Bunch,與資料包處理不同,我們不會丟棄這些資料。相反,我們將對該 Bunch 進行排隊,並暫停對任何 Bunch(可靠或不可靠)的處理。在檢測到已接收到丟失的 Bunch 之前,不會恢復處理,此時我們將處理它們,然後開始處理排隊的 Bunch。在等待丟失的 Bunch 時收到的任何新的 Bunch,或者在佇列中仍有任何 Bunch時,都將新增到佇列中,而不是立即進行處理。


網路框架圖

UE4 UDP是如何進行可靠傳輸的

原始碼分析

基本概念

原始碼版本 4.25

TSequenceHistory

這個是用來管理接收到的序列號歷史記錄的,當我們接收包的時候,一般會產生一個 Ack 或者 Nak,Ack 是 1,Nak 是 0,按順序寫入 Storage 中,Storage 是一個 uint32 陣列,最多儲存 256 位,當超過 MaxSequenceHistoryLength 的時候,會執行 FlushNet 立即傳送。 結構清晰了,那麼判斷某個序列號是 Ack 或者 Nak 的時候,只需要根據索引查詢具體的位判斷是否為 1 即可。寫入的時候根據序列號的數量寫入對應數量的 WordT 即可。

  1. <p>enum { MaxSequenceHistoryLength = 256 };</p><p>HistorySize = MaxSequenceHistoryLength</p><p>
  2. </p><p>static constexpr SIZE_T BitsPerWord = sizeof(WordT) * 8;// = 32</p><p>static constexpr SIZE_T WordCount = HistorySize / BitsPerWord;// = 8</p><p>WordT Storage[WordCount];</p><p>
  3. </p><p>template <SIZE_T HistorySize></p><p>void TSequenceHistory<HistorySize>::AddDeliveryStatus(bool Delivered)</p><p>{</p><p><span style="white-space:pre">        </span>WordT Carry = Delivered ? 1u : 0u;</p><p><span style="white-space:pre">        </span>const WordT ValueMask = 1u << (BitsPerWord - 1);</p><p><span style="white-space:pre">        </span></p><p><span style="white-space:pre">        </span>for (SIZE_T CurrentWordIt = 0; CurrentWordIt < WordCount; ++CurrentWordIt)</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>const WordT OldValue = Carry;</p><p><span style="white-space:pre">                </span></p><p><span style="white-space:pre">                </span>// carry over highest bit in each word to the next word</p><p><span style="white-space:pre">                </span>Carry = (Storage[CurrentWordIt] & ValueMask) >> (BitsPerWord - 1);</p><p><span style="white-space:pre">                </span>Storage[CurrentWordIt] = (Storage[CurrentWordIt] << 1u) | OldValue;</p><p><span style="white-space:pre">        </span>}</p><p>}</p>
複製程式碼

FNetPacketNotify

網路包通知用於實現可靠性的序列資料,包括序列號的傳送,確認,以及包頭資料和接收 Ack 的相關處理。

FNotificationHeader

這是網路資料的包頭結構,每個資料包會攜帶當前的序列號資訊。OutSeq 是傳送序列號,當 FlushNet 發包的時候才會自增;InAckSeq 是接收序列號,當我們收包的時候,不管是 Ack 還是 Nak,都會自增;WrittenHistoryWordCount 是記錄的歷史序列號的數量對 BitsPerWord 求餘的結果,最小是1,最大是8。

  1. <p>struct FNotificationHeader</p><p>{</p><p><span style="white-space:pre">        </span>SequenceHistoryT History;</p><p>    SIZE_T HistoryWordCount;    // = WrittenHistoryWordCount</p><p><span style="white-space:pre">        </span>SequenceNumberT Seq;        // = OutSeq</p><p><span style="white-space:pre">        </span>SequenceNumberT AckedSeq;   // = InAckSeq</p><p>};</p>
複製程式碼

包頭序列化的時候會壓縮在一個 uint32 中,14 位的 Seq,14 位的 AckedSeq,4位的 HistoryWordCount。 4位是因為歷史記錄陣列最大數量是8,14位是因為相容歷史?

  1. <p>static_assert(FNetPacketNotify::SequenceNumberBits <= 14, "SequenceNumbers must be smaller than 14 bits to fit history word count");</p><p>
  2. </p><p>static uint32 Pack(SequenceNumberT Seq, SequenceNumberT AckedSeq, SIZE_T HistoryWordCount)</p><p>{</p><p><span style="white-space:pre">        </span>uint32 Packed = 0u;</p><p>
  3. </p><p><span style="white-space:pre">        </span>Packed |= Seq.Get() << SeqShift;</p><p><span style="white-space:pre">        </span>Packed |= AckedSeq.Get() << AckSeqShift;</p><p><span style="white-space:pre">        </span>Packed |= HistoryWordCount & HistoryWordCountMask;</p><p>
  4. </p><p><span style="white-space:pre">        </span>return Packed;</p><p>}</p>
複製程式碼

那麼問題來了?14位的序列號的迴繞是怎麼解決的呢?

序列號的型別是一個 SequenceNumberT,通過 TSequenceNumber 封裝。建構函式只取 SequenceNumberBits 位的數字;當自增的時候呼叫 Increment 去構造一個新的 TSequenceNumber,自動從頭開始;比較大小的前提是,迴繞後的增量小於2^(n-1);做差值的時候取對應 SequenceNumberBits 位的數字(前提是(A - B) < SeqNumberHalf,也就是 A >= B)。

  1. <p>typedef TSequenceNumber<SequenceNumberBits, uint16> SequenceNumberT;</p><p>
  2. </p><p>TSequenceNumber(SequenceT ValueIn) : Value(ValueIn & SeqNumberMask) {}</p><p>void Increment(SequenceT InValue) { *this = TSequenceNumber(Value + InValue); }</p><p>
  3. </p><p>/** return true if this is > Other, this is only considered to be the case if (A - B) < SeqNumberHalf since we have to be able to detect wraparounds */</p><p>bool operator>(const TSequenceNumber& Other) const { return (Value != Other.Value) && (((Value - Other.Value) & SeqNumberMask) < SeqNumberHalf); }</p><p>
  4. </p><p>template <SIZE_T NumBits, typename SequenceType></p><p>typename TSequenceNumber<NumBits, SequenceType>::DifferenceT TSequenceNumber<NumBits, SequenceType>::Diff(TSequenceNumber A, TSequenceNumber B) </p><p>{ </p><p><span style="white-space:pre">        </span>constexpr SIZE_T ShiftValue = sizeof(DifferenceT)*8 - NumBits;</p><p>
  5. </p><p><span style="white-space:pre">        </span>const SequenceT ValueA = A.Value;</p><p><span style="white-space:pre">        </span>const SequenceT ValueB = B.Value;</p><p>
  6. </p><p><span style="white-space:pre">        </span>return (DifferenceT)((ValueA - ValueB) << ShiftValue) >> ShiftValue;</p><p>};</p>
複製程式碼

訊息的傳送

我們的訊息傳送都是通過 UChannel 來處理的,通過呼叫 UChannel::SendBunch 統一處理。 傳送的 Bunch 是以 FOutBunch 的形式存在的。當 bReliable 為 True 的時候,表示 Bunch 是可靠的。

1、判斷上限

SendBunch 的時候會去判斷當前 Bunch 的大小是否超出限制。IsBunchTooLarge 會判斷是否超出 64K。

  1. <p>if (IsBunchTooLarge(Connection, Bunch))</p><p>{</p><p><span style="white-space:pre">        </span>Bunch->SetError();</p><p><span style="white-space:pre">        </span>return FPacketIdRange(INDEX_NONE);</p><p>}</p>
複製程式碼

2、考慮合併

有些情況下是可以進行資料合併的,同一個 Channel 通道,可靠性一樣,合併後沒有超過單個 Bunch 的限制,可以合併為一個 Bunch。當然,如果是 Actor 初始化的時候需要同步 NetGUID 相關資訊,這些是肯定不能合併的。

3、考慮拆分

如果當前 Bunch 的大小超過限制時,會進行拆分,分成許多小的 Bunch。拆分 Bunch 的 bPartial 欄位為1,表示分組,bPartialInitial = 1 為拆分的第一個 Bunch,表示開始,bPartialFinal = 1 為最後一個,表示結束,bOpen 和 bClose 也分別與第一個和最後一個 Bunch 有關。這些資訊可以在接收的時候重新組成完整的 Bunch。

  1. <p>// MAX_SINGLE_BUNCH_SIZE_BITS = 7625</p><p>// MAX_PARTIAL_BUNCH_SIZE_BITS = 7624</p><p>
  2. </p><p>if( Bunch->GetNumBits() > MAX_SINGLE_BUNCH_SIZE_BITS )</p><p>{</p><p><span style="white-space:pre">        </span>uint8 *data = Bunch->GetData();</p><p><span style="white-space:pre">        </span>int64 bitsLeft = Bunch->GetNumBits();</p><p><span style="white-space:pre">        </span>Merge = false;</p><p>
  3. </p><p><span style="white-space:pre">        </span>while(bitsLeft > 0)</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>FOutBunch * PartialBunch = new FOutBunch(this, false);</p><p><span style="white-space:pre">                </span>int64 bitsThisBunch = FMath::Min<int64>(bitsLeft, MAX_PARTIAL_BUNCH_SIZE_BITS);</p><p><span style="white-space:pre">                </span>PartialBunch->SerializeBits(data, bitsThisBunch);</p><p>
  4. </p><p><span style="white-space:pre">                </span>OutgoingBunches.Add(PartialBunch);</p><p><span style="white-space:pre">        </span></p><p><span style="white-space:pre">                </span>bitsLeft -= bitsThisBunch;</p><p><span style="white-space:pre">                </span>data += (bitsThisBunch >> 3);</p><p><span style="white-space:pre">        </span>}</p><p>}</p><p>else</p><p>{</p><p><span style="white-space:pre">        </span>OutgoingBunches.Add(Bunch);</p><p>}</p>
複製程式碼

4、判斷溢位

如果設定了拆分的可靠 Bunch 上限 GCVarNetPartialBunchReliableThreshold,當拆分後的列表 OutgoingBunches 的數量超過閾值的時候,並且可靠列表沒有超出緩衝大小的時候,會標記為可靠的,同時會暫停複製,直到收到了所有可靠訊息的 Ack;

當可靠列表溢位的時候,連線會關閉。NumOutRec 為當前可靠的 Bunch 的數量,所以可靠 Bunch 的數量最多256個。

  1. <p>// RELIABLE_BUFFER = 256</p><p>
  2. </p><p>const bool bOverflowsReliable = (NumOutRec + OutgoingBunches.Num() >= RELIABLE_BUFFER + Bunch->bClose);</p><p>
  3. </p><p>if ((GCVarNetPartialBunchReliableThreshold > 0) && (OutgoingBunches.Num() >= GCVarNetPartialBunchReliableThreshold) && !Connection->IsInternalAck())</p><p>{</p><p><span style="white-space:pre">        </span>if (!bOverflowsReliable)</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>Bunch->bReliable = true;</p><p><span style="white-space:pre">                </span>bPausedUntilReliableACK = true;</p><p><span style="white-space:pre">        </span>}</p><p>}</p><p>
  4. </p><p>if (Bunch->bReliable && bOverflowsReliable)</p><p>{</p><p><span style="white-space:pre">        </span>FString ErrorMsg = NSLOCTEXT("NetworkErrors", "ClientReliableBufferOverflow", "Outgoing reliable buffer overflow").ToString();</p><p><span style="white-space:pre">        </span>FNetControlMessage<NMT_Failure>::Send(Connection, ErrorMsg);</p><p><span style="white-space:pre">        </span>Connection->FlushNet(true);</p><p><span style="white-space:pre">        </span>Connection->Close();</p><p>
  5. </p><p><span style="white-space:pre">        </span>return PacketIdRange;</p><p>}</p>
複製程式碼

5、可靠 Bunch 預處理

呼叫 SendRawBunch 之前會有預處理,執行PrepBunch,當可靠的時候,

  • OutReliable 儲存著每個 Channel 的可靠 Bunch 數量,會去初始化 Bunch 的通道序列號 ChSequence,可以看出每個通道的可靠 Bunch 序列號是遞增的。
  • 調整可靠資料包的數量 NumOutRec
  • 加入到 OutRec(傳送的未確認的可靠訊息資料)中,用於重傳。只儲存可靠的 Bunch。

  1. <p>Bunch->Next<span style="white-space:pre">        </span>= NULL;</p><p>Bunch->ChSequence = ++Connection->OutReliable[ChIndex];</p><p>NumOutRec++;</p><p>OutBunch = new FOutBunch(*Bunch);</p><p>FOutBunch** OutLink = &OutRec;</p><p>while(*OutLink) // This was rewritten from a single-line for loop due to compiler complaining about empty body for loops (-Wempty-body)</p><p>{</p><p><span style="white-space:pre">        </span>OutLink=&(*OutLink)->Next;</p><p>}</p><p>*OutLink = OutBunch;</p>
複製程式碼

6、SendRawBunch

UChannel::SendRawBunch

會重置 Ack 確認標記 ReceivedAck 為0,並根據 bClose 標記設定 Channel 的狀態,把當前 Channel 的 OutBunch 傳給 UNetConnection。

7、SendRawBunch

UNetConnection::SendRawBunch

設定敏感標記 TimeSensitive 為1,把當前的 OutBunch 寫入傳送緩衝區 SendBuffer 中,緩衝區滿了會呼叫 FlushNet 立即傳送出去。當前,寫入緩衝區之前會呼叫函式 PrepareWriteBitsToSendBuffer 預處理,判斷當前的 Bunch 寫入緩衝區之後是否會溢位,如果會溢位,則呼叫 FlushNet 立即傳送出去,並且重置緩衝區 SendBuffer。

8、傳送時機

那麼什麼時候去 Flush 呢?正常情況下是在 UNetConnection::Tick 的時候,會判斷是否有敏感標記或者超時的時候。

TimeSensitive:敏感標記,是否立即傳送。比如呼叫 SendRawBunch 的時候或者收到資料包有 DirtyAcks 的時候

  1. <p>// KeepAliveTime = 0.2</p><p>// Flush.</p><p>if ( TimeSensitive || (Driver->GetElapsedTime() - LastSendTime) > Driver->KeepAliveTime)</p><p>{</p><p><span style="white-space:pre">        </span>bool bHandlerHandshakeComplete = !Handler.IsValid() || Handler->IsFullyInitialized();</p><p>
  2. </p><p><span style="white-space:pre">        </span>// Delay any packet sends on the server, until we've verified that a packet has been received from the client.</p><p><span style="white-space:pre">        </span>if (bHandlerHandshakeComplete && HasReceivedClientPacket())</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>FlushNet();</p><p><span style="white-space:pre">        </span>}</p><p>}</p>
複製程式碼

9、傳送

當呼叫 FlushNet 的時候,會重置 TimeSensitive ,並且判斷髮送緩衝區是否有資料,或者是否 ack 包,或者是否心跳包,才會去真正傳送。

  1. <p>TimeSensitive = 0;</p><p>
  2. </p><p>// If there is any pending data to send, send it.</p><p>if (SendBuffer.GetNumBits() || HasDirtyAcks || ( Driver->GetElapsedTime() - LastSendTime > Driver->KeepAliveTime && !IsInternalAck() && State != USOCK_Closed))</p>
複製程式碼

實際的最底層傳送是 FSocketBSD::SendTo 。

  1. <p>bool FSocketBSD::SendTo(const uint8* Data, int32 Count, int32& BytesSent, const FInternetAddr& Destination)</p><p>{</p><p><span style="white-space:pre">        </span>// TODO: Consider converting IPv4 addresses to v6 when needed</p><p><span style="white-space:pre">        </span>if (Destination.GetProtocolType() != GetProtocol())</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>return false;</p><p><span style="white-space:pre">        </span>}</p><p>
  2. </p><p><span style="white-space:pre">        </span>const FInternetAddrBSD& BSDAddr = static_cast<const FInternetAddrBSD&>(Destination);</p><p><span style="white-space:pre">        </span>// Write the data and see how much was written</p><p><span style="white-space:pre">        </span>BytesSent = sendto(Socket, (const char*)Data, Count, 0, (const sockaddr*)&(BSDAddr.Addr), BSDAddr.GetStorageSize());</p><p>
  3. </p><p>//<span style="white-space:pre">        </span>NETWORK_PROFILER(FSocket::SendTo(Data,Count,BytesSent,Destination));</p><p>
  4. </p><p><span style="white-space:pre">        </span>bool Result = BytesSent >= 0;</p><p><span style="white-space:pre">        </span>if (Result)</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>LastActivityTime = FPlatformTime::Seconds();</p><p><span style="white-space:pre">        </span>}</p><p><span style="white-space:pre">        </span>return Result;</p><p>}</p>
複製程式碼

傳送後會呼叫 InitSendBuffer 重置傳送緩衝區。

傳送堆疊

UE4 UDP是如何進行可靠傳輸的

訊息的接收

1、TickDispatch

UIpNetDriver::TickDispatch

TickDispatch 負責接收網路資料,然後分發到對應的 NetConnection 中。所有的接收包都是通過資料包迭代器 FPacketIterator 來實現的,每次迭代呼叫 AdvanceCurrentPacket 來取資料包,最底層也是呼叫 FSocketBSD::RecvFrom 去接收的。每次接收到一個資料包,都會通過它的地址找到對應的連線 NetConnection,沒有則建立新的連線並開始初始化連線的流程,傳遞給對應的連線呼叫函式 ReceivedRawPacket 處理。DDoS 偵查也是在這一階段,比如空的資料包。

2、ReceivedRawPacket

UNetConnection::ReceivedRawPacket

每個進來或者出去的資料包都會在 PacketHandler 中做處理,比如握手,校驗,加密,壓縮等。

3、ReceivedPacket

UNetConnection::ReceivedPacket

這一步進行了丟包檢測。讀取資料包頭資訊,並根據包頭攜帶的序列號資訊和最後一個成功接收到的序列號去判斷序列號的增量,正常情況下,所有資料包都會按發出的順序接收,所有增量會相差1。如果大於1,說明發生了丟包,不會立即處理當前的資料,會把當前的資料包加入佇列 PacketOrderCache 中。如果小於1,說明接收到的資料包發生了失序,引擎傳送的每一個資料包序列號都是唯一的,不會重用,這種情況下引擎會忽略無效的資料包。

  1. <p>const int32 PacketSequenceDelta = PacketNotify.GetSequenceDelta(Header);</p><p>if (PacketSequenceDelta > 0)</p><p>{</p><p><span style="white-space:pre">        </span>const bool bPacketOrderCacheActive = !bFlushingPacketOrderCache && PacketOrderCache.IsSet();</p><p><span style="white-space:pre">        </span>const bool bCheckForMissingSequence = bPacketOrderCacheActive && PacketOrderCacheCount == 0;</p><p><span style="white-space:pre">        </span>const bool bFillingPacketOrderCache = bPacketOrderCacheActive && PacketOrderCacheCount > 0;</p><p><span style="white-space:pre">        </span>const int32 MaxMissingPackets = (bCheckForMissingSequence ? CVarNetPacketOrderMaxMissingPackets.GetValueOnAnyThread() : 0);</p><p>
  2. </p><p><span style="white-space:pre">        </span>const int32 MissingPacketCount = PacketSequenceDelta - 1;</p><p>
  3. </p><p><span style="white-space:pre">        </span>// Cache the packet if we are already caching, and begin caching if we just encountered a missing sequence, within range</p><p><span style="white-space:pre">        </span>if (bFillingPacketOrderCache || (bCheckForMissingSequence && MissingPacketCount > 0 && MissingPacketCount <= MaxMissingPackets))</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>int32 LinearCacheIdx = PacketSequenceDelta - 1;</p><p><span style="white-space:pre">                </span>int32 CacheCapacity = PacketOrderCache->Capacity();</p><p><span style="white-space:pre">                </span>bool bLastCacheEntry = LinearCacheIdx >= (CacheCapacity - 1);</p><p>
  4. </p><p><span style="white-space:pre">                </span>// The last cache entry is only set, when we've reached capacity or when we receive a sequence which is out of bounds of the cache</p><p><span style="white-space:pre">                </span>LinearCacheIdx = bLastCacheEntry ? (CacheCapacity - 1) : LinearCacheIdx;</p><p><span style="white-space:pre">                </span></p><p><span style="white-space:pre">                </span>int32 CircularCacheIdx = PacketOrderCacheStartIdx;</p><p><span style="white-space:pre">                </span>for (int32 LinearDec=LinearCacheIdx; LinearDec > 0; LinearDec--)</p><p><span style="white-space:pre">                </span>{</p><p><span style="white-space:pre">                        </span>CircularCacheIdx = PacketOrderCache->GetNextIndex(CircularCacheIdx);</p><p><span style="white-space:pre">                </span>}</p><p>
  5. </p><p><span style="white-space:pre">                </span>TUniquePtr<FBitReader>& CurCachePacket = PacketOrderCache.GetValue()[CircularCacheIdx];</p><p><span style="white-space:pre">                </span>// Reset the reader to its initial position, and cache the packet</p><p><span style="white-space:pre">                </span>if (!CurCachePacket.IsValid())</p><p><span style="white-space:pre">                </span>{</p><p><span style="white-space:pre">                        </span>CurCachePacket = MakeUnique<FBitReader>(Reader);</p><p><span style="white-space:pre">                        </span>PacketOrderCacheCount++;</p><p>
  6. </p><p><span style="white-space:pre">                        </span>ResetReaderMark.Pop(*CurCachePacket);</p><p><span style="white-space:pre">                </span>}</p><p><span style="white-space:pre">                </span>else</p><p><span style="white-space:pre">                </span>{</p><p><span style="white-space:pre">                        </span>TotalOutOfOrderPackets++;</p><p><span style="white-space:pre">                        </span>Driver->InOutOfOrderPackets++;</p><p><span style="white-space:pre">                </span>}</p><p><span style="white-space:pre">                </span>return;</p><p><span style="white-space:pre">        </span>}</p><p>
  7. </p><p><span style="white-space:pre">        </span>InPacketsLost += MissingPacketCount;</p><p><span style="white-space:pre">        </span>InTotalPacketsLost += MissingPacketCount;</p><p><span style="white-space:pre">        </span>Driver->InPacketsLost += MissingPacketCount;</p><p><span style="white-space:pre">        </span>Driver->InTotalPacketsLost += MissingPacketCount;</p><p><span style="white-space:pre">        </span>InPacketId += PacketSequenceDelta;</p><p>}</p>
複製程式碼

接收完資料包 ReceivedPacket 或者呼叫 PostTickDispatch 的時候,會再呼叫函式 FlushPacketOrderCache 去處理之前快取下來的資料包。

當前幀接收完所有資料包後,會呼叫 PostTickDispatch 執行 Dispatch 後的邏輯,如果快取 PacketOrderCache 中有資料(可能發生了亂序,或者丟包),接受完所有資料後會直接處理。

4、解析資料包頭

每個到來的資料包都需要到 PacketNotify 中更新序列號資訊。

1、根據包頭攜帶的序列號資料計算出當前確認的序列號數量,然後根據 AckRecord 去更新 InAckSeqAck

2、如果超出數量上限 SequenceHistoryT::Size = 256,則視為收到 Nak

3、從序列號歷史記錄(History Storage)中判斷是 Ack 還是 Nak,然後呼叫對應的處理函式

  1. <p>template<class Functor></p><p>void FNetPacketNotify::ProcessReceivedAcks(const FNotificationHeader& NotificationData, Functor&& InFunc)</p><p>{</p><p><span style="white-space:pre">        </span>if (NotificationData.AckedSeq > OutAckSeq)</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>SequenceNumberT::DifferenceT AckCount = SequenceNumberT::Diff(NotificationData.AckedSeq, OutAckSeq);</p><p>
  2. </p><p><span style="white-space:pre">                </span>// Update InAckSeqAck used to track the needed number of bits to transmit our ack history</p><p><span style="white-space:pre">                </span>InAckSeqAck = UpdateInAckSeqAck(AckCount, NotificationData.AckedSeq);</p><p>
  3. </p><p><span style="white-space:pre">                </span>// ExpectedAck = OutAckSeq + 1</p><p><span style="white-space:pre">                </span>SequenceNumberT CurrentAck(OutAckSeq);</p><p><span style="white-space:pre">                </span>++CurrentAck;</p><p>
  4. </p><p><span style="white-space:pre">                </span>// Everything not found in the history buffer is treated as lost</p><p><span style="white-space:pre">                </span>while (AckCount > (SequenceNumberT::DifferenceT)(SequenceHistoryT::Size))</p><p><span style="white-space:pre">                </span>{</p><p><span style="white-space:pre">                        </span>--AckCount;</p><p><span style="white-space:pre">                        </span>InFunc(CurrentAck, false);</p><p><span style="white-space:pre">                        </span>++CurrentAck;</p><p><span style="white-space:pre">                </span>}</p><p>
  5. </p><p><span style="white-space:pre">                </span>// For sequence numbers contained in the history we lookup the delivery status from the history</p><p><span style="white-space:pre">                </span>while (AckCount > 0)</p><p><span style="white-space:pre">                </span>{</p><p><span style="white-space:pre">                        </span>--AckCount;</p><p><span style="white-space:pre">                        </span>InFunc(CurrentAck, NotificationData.History.IsDelivered(AckCount));</p><p><span style="white-space:pre">                        </span>++CurrentAck;</p><p><span style="white-space:pre">                </span>}</p><p><span style="white-space:pre">                </span>OutAckSeq = NotificationData.AckedSeq;</p><p><span style="white-space:pre">        </span>}</p><p>}</p>
複製程式碼

5、接收 Ack

當接收到 Ack 的時候,會對當前確認的包 id 相同的 bunch 修改標誌位 ReceivedAck,並且從 OutRec 列表中刪除已確認的訊息 bunch。

  1. <p>auto AckChannelFunc = [this, &OutChannelsToClose](int32 AckedPacketId, uint32 ChannelIndex)</p><p>{</p><p><span style="white-space:pre">        </span>UChannel* const Channel = Channels[ChannelIndex];</p><p>
  2. </p><p><span style="white-space:pre">        </span>if (Channel)</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>if (Channel->OpenPacketId.Last == AckedPacketId) // Necessary for unreliable "bNetTemporary" channels.</p><p><span style="white-space:pre">                </span>{</p><p><span style="white-space:pre">                        </span>Channel->OpenAcked = 1;</p><p><span style="white-space:pre">                </span>}</p><p><span style="white-space:pre">                        </span></p><p><span style="white-space:pre">                </span>for (FOutBunch* OutBunch = Channel->OutRec; OutBunch; OutBunch = OutBunch->Next)</p><p><span style="white-space:pre">                </span>{</p><p><span style="white-space:pre">                        </span>if (OutBunch->bOpen)</p><p><span style="white-space:pre">                        </span>{</p><p><span style="white-space:pre">                                </span>UE_LOG(LogNet, VeryVerbose, TEXT("Channel %i reset Ackd because open is reliable. "), Channel->ChIndex );</p><p><span style="white-space:pre">                                </span>Channel->OpenAcked  = 0; // We have a reliable open bunch, don't let the above code set the OpenAcked state,</p><p><span style="white-space:pre">                                                                                </span>// it must be set in UChannel::ReceivedAcks to verify all open bunches were received.</p><p><span style="white-space:pre">                        </span>}</p><p>
  3. </p><p><span style="white-space:pre">                        </span>if (OutBunch->PacketId == AckedPacketId)</p><p><span style="white-space:pre">                        </span>{</p><p><span style="white-space:pre">                                </span>OutBunch->ReceivedAck = 1;</p><p><span style="white-space:pre">                        </span>}</p><p><span style="white-space:pre">                </span>}</p><p><span style="white-space:pre">                </span>Channel->ReceivedAck(AckedPacketId);</p><p><span style="white-space:pre">                </span>EChannelCloseReason CloseReason;</p><p><span style="white-space:pre">                </span>if (Channel->ReceivedAcks(CloseReason))</p><p><span style="white-space:pre">                </span>{</p><p><span style="white-space:pre">                        </span>const FChannelCloseInfo Info = {ChannelIndex, CloseReason};</p><p><span style="white-space:pre">                        </span>OutChannelsToClose.Emplace(Info);</p><p><span style="white-space:pre">                </span>}<span style="white-space:pre">        </span></p><p><span style="white-space:pre">        </span>}</p><p>};</p><p><span style="white-space:pre">        </span>// Invoke AckChannelFunc on all channels written for this PacketId</p><p>FChannelRecordImpl::ConsumeChannelRecordsForPacket(ChannelRecord, AckPacketId, AckChannelFunc);</p>
複製程式碼

6、接收 Nak

當我們傳送一個可靠的 Bunch 的時候,會把它新增到 OutRec 中,這是一個已傳送的未確認的可靠訊息列表。當接收到 Nak 的時候,會為每個通道的包 id 為 NakPacketId 的未確認的可靠資料重新傳送一次。丟包發生的時候,只會按 Bunch 去重新傳送,Bunch 序列號還是原來的 Channel 序列號,而之前的 Packet 是不會重用的,只會生成新的 Packet,以及最新的 PacketId。意味著不會重新傳送之前傳送的資料包,也不會重用資料包序列號,資料包的傳送每一次都是新生成的資料包,資料包序列號都是遞增的,不會重複。

1、由於 OutRec 只儲存了可靠的資料包,如果是不可靠的訊息發生了丟包,引擎是不會重新傳送它們的。

2、這裡儲存的是 RawBunch,如果 Bunch 是拆分的,丟棄了一部分,會導致整個 Bunch 的重新傳送。

  1. <p>void UChannel::ReceivedNak( int32 NakPacketId )</p><p>{</p><p><span style="white-space:pre">        </span>for( FOutBunch* Out=OutRec; Out; Out=Out->Next )</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>// Retransmit reliable bunches in the lost packet.</p><p><span style="white-space:pre">                </span>if( Out->PacketId==NakPacketId && !Out->ReceivedAck )</p><p><span style="white-space:pre">                </span>{</p><p><span style="white-space:pre">                        </span>check(Out->bReliable);</p><p><span style="white-space:pre">                        </span>UE_LOG(LogNetTraffic, Log, TEXT("      Channel %i nak); resending %i..."), Out->ChIndex, Out->ChSequence );</p><p><span style="white-space:pre">                        </span></p><p><span style="white-space:pre">                        </span>FNetTraceCollector* Collector = Connection->GetOutTraceCollector();</p><p><span style="white-space:pre">                        </span>if (Collector)</p><p><span style="white-space:pre">                        </span>{</p><p><span style="white-space:pre">                                </span>// Inject trace event for the resent bunch if tracing is enabled</p><p><span style="white-space:pre">                                </span>// The reason behind the complexity is that the outgoing sendbuffer migth be flushed during the call to SendRawBunch()</p><p><span style="white-space:pre">                                </span>FNetTraceCollector* TempCollector = UE_NET_TRACE_CREATE_COLLECTOR(ENetTraceVerbosity::Trace);</p><p><span style="white-space:pre">                                </span>UE_NET_TRACE(ResendBunch, TempCollector, 0U, Out->GetNumBits(), ENetTraceVerbosity::Trace);</p><p><span style="white-space:pre">                                </span>Connection->SendRawBunch(*Out, 0, TempCollector);</p><p><span style="white-space:pre">                                </span>UE_NET_TRACE_DESTROY_COLLECTOR(TempCollector);</p><p><span style="white-space:pre">                        </span>}</p><p><span style="white-space:pre">                        </span>else</p><p><span style="white-space:pre">                        </span>{</p><p><span style="white-space:pre">                                </span>Connection->SendRawBunch( *Out, 0 );</p><p><span style="white-space:pre">                        </span>}</p><p><span style="white-space:pre">                </span>}</p><p><span style="white-space:pre">        </span>}</p><p>}</p>
複製程式碼

7、分發bunches

解析資料,分發所有的Bunch。通過通道索引 ChIndex 找到對應的通道 Channel,呼叫函式 UChannel::ReceivedRawBunch 解析。

8、ReceivedRawBunch

UChannel::ReceivedRawBunch

1、如果是可靠的訊息,但是通道序列號不是有序的,則放入接收可靠訊息列表 InRec 中,並按通道序列號 ChSequence 順序儲存,同樣的,接收的可靠訊息列表數量 NumInRec 一樣不能超過可靠緩衝區大小256(RELIABLE_BUFFER)。

2、呼叫 ReceivedNextBunch 接收完之後,會再處理之前快取的可靠訊息列表 InRec,按順序處理。

9、ReceivedNextBunch

UChannel::ReceivedNextBunch

1、如果是可靠訊息,重置序列號

2、如果是 PartialBunch,當第一個初始化 bPartialInitial 的時候,會建立 InPartialBunch,後續遇到所有的 PartialBunch 都會合併到 InPartialBunch 中。對合並後的 InPartialBunch 進行大小檢查 IsBunchTooLarge,超過 64K 不處理。

10、ReceivedSequencedBunch

UChannel::ReceivedSequencedBunch

在確認有序 Bunch 後執行對應 Channel 的 ReceivedBunch 函式,處理各自 Channel 的接收邏輯,如果標記了 bClose 的 Bunch,則關閉 Channel。

11、確認

當收到一份資料的時候,我們會對資料進行確認,會回覆 Ack 或者 Nak,寫入到序列號歷史記錄中,由於歷史記錄最多 256 位,所以當 Ack 累計超過之後,會呼叫 FlushNet 立即傳送。同時改變敏感標誌位 TimeSensitive。

  1. <p>if( !IsInternalAck() )</p><p>{</p><p><span style="white-space:pre">        </span>// We always call AckSequence even if we are explicitly rejecting the packet as this updates the expected InSeq used to drive future acks.</p><p><span style="white-space:pre">        </span>if ( bSkipAck )</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>// Explicit Nak, we treat this packet as dropped but we still report it to the sending side as quickly as possible</p><p><span style="white-space:pre">                </span>PacketNotify.NakSeq( InPacketId );</p><p><span style="white-space:pre">        </span>}</p><p><span style="white-space:pre">        </span>else</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>PacketNotify.AckSeq( InPacketId );</p><p>
  2. </p><p><span style="white-space:pre">                </span>// Keep stats happy</p><p><span style="white-space:pre">                </span>++OutTotalAcks;</p><p><span style="white-space:pre">                </span>++Driver->OutTotalAcks;</p><p><span style="white-space:pre">        </span>}</p><p>
  3. </p><p><span style="white-space:pre">        </span>// We do want to let the other side know about the ack, so even if there are no other outgoing data when we tick the connection we will send an ackpacket.</p><p><span style="white-space:pre">        </span>TimeSensitive = 1;</p><p><span style="white-space:pre">        </span>++HasDirtyAcks;</p><p>
  4. </p><p><span style="white-space:pre">        </span>if (HasDirtyAcks >= FNetPacketNotify::MaxSequenceHistoryLength)</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>FlushNet();</p><p><span style="white-space:pre">                </span>if (HasDirtyAcks) // if acks still are dirty, flush again</p><p><span style="white-space:pre">                </span>{</p><p><span style="white-space:pre">                        </span>FlushNet();</p><p><span style="white-space:pre">                </span>}</p><p><span style="white-space:pre">        </span>}</p><p>}</p>
複製程式碼

bSkipAck 又是如何去確認是 Nak 的呢?有幾種情況:

1、不可靠的資料包去開啟通道,並且不是暫時的。

2、不可靠的 PartialBunch 破壞了未解析完的上一個可靠的 PartialBunch。

3、PartialBunch 的合併出現問題,比如序列號不匹配。

4、通道未完全開啟。

當然確認序列號時發生丟包的情況下,也是返回 Nak。

12、丟包

發生丟包的時候,快取 PacketOrderCache 中肯定是有資料的,接受完所有資料後會直接處理快取中的資料包。

連線記錄接收的 PacketId 是實時計算的,每收到一個資料包,InPacketId 會加上計算出的增量 PacketSequenceDelta。所以發生丟包的時候,當前計算出的 InPacketId 與上次儲存的序列號 InAckSeq 之間的差值大於 1,會把中間所有丟失的序列號標記為未確認,序列號歷史記錄中記為 False,返回 Nak 給傳送方。


舉個例子,如果當前最後接收到的資料包序列號(InPacketId)為 3,下一幀陸續接收到了資料包序列號 8 、 6,那麼接收資料包為 8 的時候會和 3 比較,差值為 5,發生了丟包,會加入到快取 PacketOrderCache 中,在快取中的位置為 5。同理,當接收到資料包為 6 的時候,差值為 3,在快取中的位置是 3,這樣資料包就已經排好了順序。當所有資料包都已經接收完畢後,會按順序處理快取 PacketOrderCache 中的資料包 6 、8,第一次處理資料包 6 的時候,由於 InPacketId 加上當前的差值 3,所以現在已經接收到的序列號就是 6,當把 InPacketId 傳入函式 AckSeq 中確認 ACK 的時候,只會確認與當前 AckedSeq 相等的序列號,中間的所有序列號都會視為丟包,這裡會按順序確認 4、5、6,4 和 5 被視為丟包,返回 NAK, 6 視為確認,返回 ACK。同理,處理資料包 8 的時候,7 被視為丟包, 8 視為確認。所以當前 ACK 歷史記錄中就被計為 00101,記錄的是 4 - 8 的資料包確認狀態,對端再根據確認狀態進行 ACK 和 NAK 的處理。如果網路中的異常情況導致下一幀接收到了資料包 4,由於當前已接收到資料包序列號已經是 8,會丟棄不處理。
  1. <p>void FNetPacketNotify::AckSeq(SequenceNumberT AckedSeq, bool IsAck)</p><p>{</p><p><span style="white-space:pre">        </span>check( AckedSeq == InSeq);</p><p>
  2. </p><p><span style="white-space:pre">        </span>while (AckedSeq > InAckSeq)</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>++InAckSeq;</p><p>
  3. </p><p><span style="white-space:pre">                </span>const bool bReportAcked = InAckSeq == AckedSeq ? IsAck : false;</p><p>
  4. </p><p><span style="white-space:pre">                </span>UE_LOG_PACKET_NOTIFY(TEXT("FNetPacketNotify::AckSeq - AckedSeq: %u, IsAck %u"), InAckSeq.Get(), bReportAcked ? 1u : 0u);</p><p>
  5. </p><p><span style="white-space:pre">                </span>InSeqHistory.AddDeliveryStatus(bReportAcked);<span style="white-space:pre">                </span></p><p><span style="white-space:pre">        </span>}</p><p>}</p>
複製程式碼

接收堆疊

UE4 UDP是如何進行可靠傳輸的

流量控制

下面聊一聊 UE4 是如何進行網路頻寬限制的,也就是通常所說的限流。限流的實現與兩個部分有關,一個是網路速度,一個是可以傳送的最大流量。

CurrentNetSpeed

當前的網路速度是一開始就初始化的,如果是區域網就讀取配置中的區域網速度 ConfiguredLanSpeed,否則讀取網際網路速度 ConfiguredInternetSpeed。客戶端連線過程中接收到訊息 NMT_Welcome,會以初始化的網速傳送 NMT_NetSpeed ,伺服器接收 NMT_NetSpeed,並適當調整當前連線的網速。

通過閱讀原始碼發現,當前網速是固定的,只在連線過程中同步客戶端配置的網速,此後不再改變。

引擎預設配置的網速為每秒的位元組數,比如預設配置中的網路速度10000,轉換成通俗一點的網速是(10000 / 1024 = )9.76 kb/s ,區域網的會快一點。通常每個專案會根據需要修改合適的網速。需要特別說明的是,如果是重播相關的 UDemoNetDriver,初始化連線的時候會傳入固定的網速1000000,相當於976 kb/s。

  1. <p>[/Script/Engine.Player]</p><p>ConfiguredInternetSpeed=10000</p><p>ConfiguredLanSpeed=20000</p>
複製程式碼

QueuedBits

這個就是當前網路可以傳送的最大流量,類似於 TCP 的滑動視窗。

增加

有了當前的網路速度,再計算時間,就可以得到當前的流量了。DeltaTime 為當前 Tick 的時間差,DesiredTickRate 為當前的期望幀率(值得一提的是,如果編輯器在後臺執行,幀率會退化為3),實際的頻寬時間差 BandwidthDeltaTime 會根據期望幀率去修改時間差(如果這一幀跑了太長的時間,會修復,不會有太大的偏差)。所以計算出的流量 DeltaBits 就是這一幀可以增加的流量。引擎同時做了優化,限定了當前可傳送的流量(介於1倍和2倍之間),允許一部分的延遲。

減少

當我們呼叫 FlushNet 去傳送資料的時候,QueuedBits 會相應的減少傳送的資料量。

判斷

函式 IsNetReady 用於判斷網路是否暢通,當最大流量 QueuedBits 與緩衝區的差值小於0時,說明還有流量可以傳送,網路暢通,可以準備寫入緩衝區,如果差值大於0時,說明沒有可傳送的流量,緩衝區已滿,網路飽和,不能繼續寫入。

  1. <p>int32 UNetConnection::IsNetReady( bool Saturate )</p><p>{</p><p><span style="white-space:pre">        </span>if (Saturate)</p><p><span style="white-space:pre">        </span>{</p><p><span style="white-space:pre">                </span>QueuedBits = -SendBuffer.GetNumBits();</p><p><span style="white-space:pre">        </span>}</p><p>
  2. </p><p><span style="white-space:pre">        </span>return QueuedBits + SendBuffer.GetNumBits() <= 0;</p><p>}</p>
複製程式碼

當我們上層需要進行 Actor 網路複製或者 RPC 呼叫時,需要判斷當前網路是否飽和,如果是,則不會繼續。特別的,如果是重要的 RPC 函式,比如標記了 FUNC_NetReliable 或者 FUNC_NetMulticast,儘管網路飽和,也會傳送。

網路連線的函式 Tick 中限流核心程式碼

  1. <p>float BandwidthDeltaTime = DeltaTime;</p><p>if (DesiredTickRate != 0.0f)</p><p>{</p><p><span style="white-space:pre">        </span>BandwidthDeltaTime = FMath::Clamp(BandwidthDeltaTime, 0.0f, 1.0f / DesiredTickRate);</p><p>}</p><p>
  2. </p><p>float DeltaBits = CurrentNetSpeed * BandwidthDeltaTime * 8.f;</p><p>QueuedBits -= FMath::TruncToInt(DeltaBits);</p><p>float AllowedLag = 2.f * DeltaBits;</p><p>if (QueuedBits < -AllowedLag)</p><p>{</p><p><span style="white-space:pre">        </span>QueuedBits = FMath::TruncToInt(-AllowedLag);</p><p>}</p>
複製程式碼

總結

可靠有序

1、每一個 Bunch 都是攜帶資料的,Bunch 大小有限制,過大會進行拆分。同一個 Channel 的多個 Bunch 有可能合併。

2、Packet 裡包括 Ack 和多個 Bunch,也可能沒有 Bunch,只傳送 Ack。

3、每個 Channel 的傳送接收緩衝區只會儲存可靠的 Bunch,不可靠的 Bunch 沒有備份,上層自己維護。上限256個。

4、每一個發出去的包都有一個 Packet 序列號,如果發生丟包,只會重新傳送當前 Packet 裡可靠的原始未拆分的 Bunch,保證單個 Channel 內的可靠 Bunch 是有序的,Channel 間的 Bunch 有序性是不確定的,部分丟失的 Bunch 會傳送完整的 Bunch,並且傳送 Bunch 會重新組裝成一個新的 Packet,以及新的 Packet 序列號,和丟失的 Packet 毫無關係,內部的 Bunchs 也不一定完全相同。所以可靠不是相對於 Packet 來說的,只有可靠的 Bunch。

5、發生 Packet 亂序或者 Bunch 亂序的時候,會先快取起來,等第一個有序到來的時候,再一起按序處理。

6、呼叫 FlushNet 立即傳送的時機?

  • 正常情況下,UNetConnection::Tick 的時候,如果設定了敏感標記 TimeSensitive,或者距離上次傳送時間超過了心跳時間 KeepAliveTime 的時候
  • 緩衝區滿了
  • 如果新加入的 Bunch 大小會使緩衝區大小越界,會立即傳送已在緩衝區的資料
  • Ack 數量累計超過 256
  • 需要立即關機某個 Channel
  • 連線 NetConnection 設定了自動傳送 bAutoFlush
  • 連線關閉之前,會重新整理緩衝區

7、沒有超時重傳,只有收到 Nak 才會重傳。

最後

這篇文章只是想了解 UDP 是如何進行可靠傳輸的,涉及到了很多的原始碼,更多的是我對原始碼的理解。如果發現有錯誤或者想交流學習,可以聯絡我。


來源:程式設計師毛寸
原文:https://mp.weixin.qq.com/s/wOaC0Zf2LIeKYJ1yt00Kow


相關文章