UE4網絡同步概述


  在研究UE4網絡的源碼時發現一大段英文注釋。

  這一大段注釋大致說了網絡同步使用的基本類、客戶端和服務器連接握手過程、客戶端和服務器通信之間數據是如何組織的,如何傳輸的。還有大致描述了Packet和Bunches的概念以及UE4在應用層實現的可靠傳輸。

  這段注釋對於整一個網絡同步有一個很好的梳理,從梳理中可以方便找到感興趣的模塊進行源碼閱讀。遂翻譯之,這段英文概敘在UE4源碼的NetDriver.h下。

NetDrivers NetConnections Channels:

  UNetDrivers 負責管理UNetConnections集合以及UNetConnections之間共享的數據。

  對於一個游戲,UNetDrivers的數量通常來說相對較少。這些UNetDrivers可能包含:

  • Game NetDriver,負責標准游戲網絡流量
  • Demo NetDriver,負責記錄和回放之前的游戲記錄。這就是錄像回放的原理。
  • Becon NetDriver,負責超出“正常”游戲流量的網絡流量
  • 自定義NetDrivers也可以由游戲或者應用程序實現和使用。

  NetConnections表示連接到游戲的單個客戶端(或者更一般的說,連接到NetDriver)。端點數據不是由NetConnections直接處理的,NetConnections會轉交數據給Channels。

  每個NetConnection都有自己的一組Channels集合。

  這些Channels大致有一下類型:

  • Control Channel:用於發送關於連接狀態的信息(連接是否應該關閉等)
  • Voice Channel:用於在客戶端和服務器之間傳送音頻數據
  • Actor Channel:用於從Server端同步Actor數據到Client端,每個被標記為Replicated的Actor都有一個獨一無二的Actor Channel。

  除了上述這些Channel類型以外,自定義Channels也能用於特定的目的,但不是很常用。

  通常情況下,只會有一個NetDriver(在Client端和Server端創建)用於“標准”的游戲流量傳輸和游戲連接。

  Server端的NetDriver將維護一個NetConnections列表,每個NetConnection代表游戲中的一個玩家。NetDriver它負責同步Actor數據。

  Client端的NetDriver只有一個NetConnection表示到Server端的連接。

  不論是Server端還是Client端,NetDriver都負責接受來自網絡的數據包並將其轉交給給相應的NetConnection(必要時會建立新的NetConnections)。

Initiating Connections / Handshaking Flow 初始化連接/握手流程

  UIpNetDriver和UIpConnection(或者派生類)是引擎幾乎在每個平台下的默認使用類。下面的內容描述了它們如何建立和管理連接。但是,這些步驟在NetDriver的實現之間可能有所不同。

  服務器和客戶端都有自己的NetDrivers,游戲的所有同步(Replication)流量傳輸都會由IpNetDriver發送或者接收。這些通信流還包括用於建立連接的邏輯以及在何時重新建立連接的邏輯如果出現問題了的話。

  握手(Handshaking)分為幾個不同的部分:NetDriver, PendingNetGame, World, PacketHandlers等等。這么做的原因是因為有不同的需求,例如:確定到來的連接是否以“UE-Protocol”發送數據的、確定一個地址是否有惡意、確定一個給定的客戶端是否有正確的游戲版本等等。

Startup and Handshaking 啟動和握手

  當服務器加載一個map時(通過UEngine::LoadMap),我們會調用UWorld::Listen。該代碼負責創建主游戲NetDriver,解析設置並調用UNetDriver::InitListen。

  最終,這些代碼負責弄清楚我們是如何監聽客戶端連接的。例如在IpNetDriver中,我們通過調用configured Socket Subsystem(參見ISocketSubsystem::GetLocalBindAddresses和ISocketSubsystem::BindNextPort)來確定要綁定到的IP/端口。

  一旦服務器處於監聽狀態,它就可以開始接受客戶端連接了。

  當一個客戶端連接服務器時,他們首先會用服務器的Ip在UEngine::Browse中建立一個新的UpendingNetGame。UpendingNetGame::Initialize和UpendingNetGame::InitNetDriver分別負責初始化設置和設置NetDriver。

  客戶端將立即為服務器設置一個UNetConnection作為初始化的一部分,並將開始發送數據到該連接上的服務器。

  然后開始握手的過程。

  在客戶端和服務器端,UNetDriver::TickDispatch通常負責接收網絡數據,通常情況下,當我們收到一個數據包的時候,我們會檢查它的地址,看看它是否來自一個我們已經知道的連接。

  我們通過保持一個從FInternetAddr到UNetConnection的映射來判斷是否已經建立一個連接。如果一個包來自一個已經建立的連接,我們通過UNetConnection::ReceivedRawPacket將這個包傳遞給這個連接。如果包不是來自已經建立的連接,我們將其視為“非連接”,並開始握手過程。

  詳情請參閱StatelessConnectionHandlerComponent.cpp。

UWorld/UPendingNetGame/AGameModeBase 啟動和握手

  在UNetDriver和UNetConnection完成客戶端和服務器端之間的握手過程后,UPendingNetGame::SendInitialJoin將在客戶端被調用,以啟動游戲關卡(game level)的握手。

  游戲關卡的握手是通過一組更加結構化和復雜的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,驗證Challenge數據,然后調用AGameModeBase::PreLogin。
  •   如果PreLogin沒有報告任何錯誤,服務器調用UWorld::WelcomePlayer,它調用AGameModeBase::GameWelcomePlayer,發送NMT_Welcome和地圖信息。
  •   客戶端UPendingNetGame::NotifyControlMessage接收NMT_Welcome,讀取地圖信息(以便稍后開始加載)和發送一個NMT_NetSpeed消息包含客戶端的網速。
  •   服務器的UWorld::NotifyControlMessage接收NMT_NetSpeed,並適當調整連接的網絡速度。

  至此,握手被認為是完整的, 玩家連接到游戲。

  根據加載地圖所需的時間,在控制轉換到UWorld之前客戶端在UPendingNetGame仍然可以接收到一些非握手控制消息。

  如果需要,還有其他處理加密的步驟。 

Reestablishing Lost Connections 重新建立丟失的連接

  在整個游戲過程中,連接可能會因為一些原因而丟失。例如:網絡斷開,用戶可以從LTE切換到WIFI,離開游戲等等。

  如果服務器嘗試向這些斷開的連接發送消息,或者由於超時或者錯誤服務器知道了這些斷開的連接,那么斷開連接這個連接在服務器上將被關閉UNetConnection並通知游戲。

  如果游戲支持,我們將完全重新啟動上面的握手流程。

  如果只是短暫地中斷了客戶端連接,但服務器卻不知道,那么引擎/游戲通常會自動恢復(盡管有一些包丟失/延遲峰值)。

  然而,如果客戶端IP地址或端口由於任何原因發生變化,而服務器不知道這一點,我們將通過更底層的的握手來開始恢復過程。這里是游戲代碼不會收到警告的。

  這個過程包含在StatlessConnectionHandlerComponent.cpp中。

Data Transmission 數據傳輸

  游戲NetConnections和NetDrivers通常與底層通信方法/技術無關。

  這是留給子類來決定的(類如UIpConnection / UIpNetDriver或UWebSocketConnection / UWebSocketNetDriver)。

  但是,UNetDriver和UNetConnection使用Packets和Bunches。

  Packets是主機和客戶端上的NetConnections之間發送的數據塊。

  Packets是由關於數據包的元數據(如報頭信息和ACK)和Bunches組成;

  Bunches是主機和客戶端上的Channels之間發送的數據塊。

  當一個連接收到一個Packet時,該Packets會被分解成單獨的Bunches,這些Bunches隨后被傳遞到單獨的Channels上,以便進一步處理。

  一個Packet可能不包含任何Bunches,或者包含單個Bunches或者多個Bunches

  由於Bunches的大小限制可能大於單個Packet的大小限制,因此UE4支持分Bunches的概念。

  當一個Bunches太大時,在傳輸之前我們會將它分成許多小的Bunches。這些小的Bunches將會被標記為分組的開始、分組的部分以及分組的結束。利用這些信息我們可以在對端收到這些小的Bunches時候重新組裝成完整的Bunch。

  例如:客戶端RPC到服務器

  客戶端調用Server_PRC

  該請求被轉發(通過NetDriver和NetConnection)到Actor的Channel,這個Channel擁有調用RPC的Actor。

  Actor Channel將RPC標識符和參數序列化到Bunch中,這個Bunch還將包含Actor Channel的ID。

  稍后,NetConnection將會把這個Bunch(和其他Bunch)數據組裝成一個Packet發送給服務器

  在服務器端,Packet包將被NetDriver接收。NetDriver會檢查發送這個Packet的地址,然后將這個Packet轉交給對應的NetConnection處理。

  NetConnection會將Packet分解成Bunces(一個接一個處理);

  NetConnection將使用Bunch上的Channel ID將Bunch轉交到相應的Actor Channel上。

  Actor Channel將分解Bunch,查看它是否包含RPC數據,並使用RPC ID和序列化參數在Actor上調用適當的函數。

Reliability and Retransmission 可靠性和重傳

       UE4網絡通常假定底層網絡協議不可靠,因此它實現了自己的Packets和Bunches的可靠性和重傳。

       當一個NetConnection建立,他將為他的Packets和Bunches建立一個序列號,這些序列號可以是固定的也可以是隨機的(當序列號是隨機時,序列號將由服務器發送)。

       (The packet number is per NetConnection)每個NetConnection的Packet號每次發送一個包就增加,每個Packet都包含它的packet號。我們永遠不會重傳相同Packet號的Packet。

       (The bunch number is per Channel)每個Channel的Bunch號隨着每個“可靠”Bunch的發送而增加,每個Bunch都包含它的Bunch號。但是與Packets不同,精確(可靠的)Bunches可以被重傳。這意味着我們會用相同的Bunch號重傳Bunches。

       注意,在整個代碼中,上面描述的Packet號和Bunch號通常就是一個序號值。為了更清楚地理解,我們在這里做了區分。

Detecting Incoming Dropped Packets檢測收包時發生了丟包

       通過確定Packet號,我們可以很容易地判斷接收到的Packets是否有發生過丟包。

       這只需要取最后一個成功接收到的Packet號與當前正在處理的Packet號之間的差值即可。在未發生丟包的情況下,所有Packet都會按其發出的次序接收。這意味着差異會 + 1。如果差值大於1,說明我們遺漏了一些Packets。我們會假設這些之前的Packets已被丟棄,但假設當前的Packet已被成功接收,並且使用這個Packet號向前遞增。

如果差值為負(或0),則表示我們接收到的數據包出現了失序,或者外部服務試圖重新向我們發送數據(請記住,引擎不會重用Packet號)。

       在這兩種情況下,引擎通常會忽略丟失的或無效的Packet,並且不會為它們發送ACKs。

       我們確實有方法來“修復”在同一幀上接收的無序數據Packets包。

       當啟用時,如果我們檢測到丟包發生(Packet號差異> 1),我們不會立即處理當前Packet,而是把當前Packet包加入一個隊列中。下一次我們成功地接收到數據包(difference == 1)時,我們將查看隊列的頭部是否得到了正確的排序。如果是,我們將處理它,否則我們將繼續接收數據包。

  一旦我們讀取了所有當前可用的包,我們將刷新這個隊列處理任何剩余的包。在這一點上丟失的任何東西都將被假定為已經被丟棄。

  每個成功收到的Packet包都會將其Packet號作為確認(ACK)發送回給發送方。

Detecting Outgoing Dropped Packets檢測發包時發生了丟包

  如上所述,無論何時成功接收到數據包,接收方都將發送回一個ACK。這些ACKs將按順序包含被成功接收到的Packet的Packet號。與接收方跟蹤包號的方式類似,發送方將跟蹤最高的ACK包號。當ACK被處理時,任何低於我們最后收到的ACK的ACK都被忽略,包號中的任何間隙都被認為是不被承認的(NAKed)。

發送方有責任處理這些ack和nak並重新發送任何丟失的數據。新數據將被添加到新的發送數據包(同樣,我們不會重新發送我們已經發送的數據包,或重復使用包序列號)。

Resending Missing Data重傳丟失的數據

       如上所述,Packets包本身並不包含有用的游戲數據。而組成它們的Bunches包才含有有意義的數據。

       Bunches可以被標記為可靠或不可靠。

  如果不可靠的Bunches發生了丟包,引擎將不會嘗試重新發送它們。因此,如果標記為不可靠,游戲/引擎應該能夠在沒有它們的情況下繼續運行,或者必須放置外部重試機制,或者必須發送冗余的數據。

  但是,引擎會嘗試重新發送可靠的Bunches。無論何時發送一個可靠的Bunch,它都將被添加到un-ACKed的可靠的Bunches列表中。如果我們收到一個Packet包包含這個Bunch的NAK,引擎會重新發送這個Bunch的精確副本。注意,由於Bunches可能是被拆分過的,即使丟棄一個部分的Bunch也會導致整個Bunch的重新傳輸。當所有的包含Bunch的Packets包都被ACK時,我們將把它從列表中刪除。

       與Packet類似,我們將比較接收到的可靠Bunch的Bunch號與最后成功接收Bunch的Bunch號。如果我們發現差值是負的,我們就忽略這一串。如果差值大於1,我們就會認為我們漏了Bunch。與Packet處理不同,我們不會丟棄放棄這些數據。我們會將這些bunch放入隊列並暫停處理“任何”可靠或不可靠的Bunches。

       直到我們檢測到已接收到丟失的Bunch我們才恢復處理接下來的Bunches,在此我們會處理這些Bunch並開始處理已在隊列的Bunches。

       在等待接收丟失的Bunches時收到的新的Bunches,或在隊列中仍然有Bunches時收到的新的Bunches,這些新收到的Bunches將被添加到隊列中,而不是立即處理。

 

后面關於可靠性和重傳部分翻譯得不加,可能要結合源碼才能翻譯得更加准確。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM