【網絡多人遊戲架構與編程2】算法
1.0、虛擬現實遊戲是對延遲最敏感的, 由於咱們人類只要頭旋轉了,眼睛就指望看到不一樣的事物。在這些狀況下,保證用戶感受在虛擬現實世界中就要求延遲少於 20 毫秒。編程
格鬥遊戲、 第一人稱射擊遊戲和其餘動做頻繁的遊戲是對延遲第二敏感的。 這些遊戲的延遲範圍能夠從16 毫秒到150毫秒。網絡
RTS遊戲是對延遲容忍度最高的, 這個容忍度一般頗有用, 正如第 6 章所介紹的。 這些遊戲的延遲能夠高達 500 毫秒, 而不影響用戶體驗。多線程
1.一、非網絡延遲。架構
1)輸入採樣延遲(input sampling latency)。用戶按下一個按鈕到遊戲檢測到這個按鈕的時間可能很長。下圖代表,遊戲循環架構可能致使 Input Sampling Latency 高達接近 2幀的時間。併發
2)渲染流水線延遲(render pipeline latency)。驅動程序將繪製命令插入到緩衝區,GPU在將來的某個時刻執行。若是有許多渲染任務要作,可能會致使滯後 1幀 渲染出來。ide
3)多線程渲染流水線延遲(multithreaded render pipeline latency)。大數據
4)垂直同步(VSync)ui
5)顯示延遲(display lag)。顯示器可能會對畫面進行調整。this
6)像素響應時間(pixel response time)。像素改變須要時間,大概幾毫秒。
二、數據包傳輸過程當中,有四種主要的延遲:
1)處理延遲(processing delay)。網絡路由器的工做包括:讀取數據包、檢查目的IP、找出下一臺機器等。
2)傳輸延遲(transmission dely)。鏈路層將數據寫入物理層(轉爲物理層信號)的時間。
3)排除延遲(queuing delay)。
4)傳播延遲(propagation delay)。例如從東海岸傳播到西海岸的時間。
包含1400字節負載的數據包與包含200字節負載的數據包一般經歷相同時間的處理延遲。若是你發送 7 個包含 200 字節負載的數據包, 最後那個數據包將不得不在隊列中等待前面6 個數據包的處理, 這樣將經歷比一個大數據包更多的累積網絡延遲。
三、網絡抖動會致使數據包亂序到達。
四、數據包丟失的狀況。數據包丟失必然會產生,沒法避免。
1)不可靠的物理介質。電磁干擾可能致使依賴損壞或丟失,如微波爐的工做。
2)不可靠的鏈路。有時鏈路層信道徹底滿了,必須丟失正在發送的幀。
3)不可能的網絡層。當路由器隊列滿了,後續到達的包將被丟棄。
五、路由器並不必定丟棄最後到達的報文。例如,有些路由器在丟棄TCP報文以前先丟棄UDP報文,由於它們知道丟棄TCP的報文會自動重傳。
六、TCP的幾大問題,最大問題是強制可靠性。
1)低優先級數據的丟失干擾高優先級數據的接收。例如,依次發送聲音報文、技能報文,若是聲音報文未收到,則永遠不觸發技能報文,但對玩家來講,聲音播放與否可有可無,但技能必須當即播放。
2)不相關數據流的想到干擾。例如技能報文、聊天報文使用同一個 TCP 鏈接,則一種報文的丟失會影響另外一個報文。
3)過期遊戲狀態重傳。
TCP中的 Nagle 算法起了很是很差的做用, 由於它在將數據包發送出去以前能夠延遲長達0.5秒。事實上,使用 TCP 做爲傳輸層協議的遊戲一般禁用 Nagle 算法以免這個問題, 雖然同時放棄了它提供的減小數據包數量的優點。
最後,TCP 爲管理鏈接和跟蹤全部可能被重傳的數據分配了不少資源。這些分配一般是由操做系統管理的, 遊戲須要時很難經過自定義內存管理器的方式跟蹤和路 由。
七、經過UDP,能夠自定義一個系統,在發生丟包時,只發送最新消息,而不是重傳丟失的數據。有些第三方的UDP網絡庫可使用,如 RakNet、Photon。
八、自建可靠的UDP系統。
1)發出數據包。從TCP借用一個技術,給每一個數據包分配一個序列號來實現。
1 InFlightPacket* DeliveryNotificationManger::WriteSequenceNumber( 2 OutputMemoryBitStream& inPacket) 3 { 4 PacketSequenceNumber sequenceNumber = mNextOutgoingSequenceNumber++; 5 inPacket.Write(sequenceNumber); 6 7 ++mDispatchedPacketCount; 8 9 mInFlightPackets.emplace_back(sequenceNumber); 10 return &mInFlightPackets.back(); 11 }
2)收到數據包併發送確認。與TCP不一樣,這裏不承諾按序處理每一個單獨的數據包。僅僅承諾不亂序處理。只回復最新的包。
bool DeliveryNotificationManager::ProcessSequenceNumber( InputMemoryBitStream& inPacket) { PacketSequenceNumber sequenceNumber; inPacket.Read(sequenceNumber); if (sequenceNumber == mNextExpectedSequenceNumber) { // 是指望的包,加入到待發ACK隊列 mNextExpectedSequenceNumber = sequenceNumber + 1; AddPendingAck(sequenceNumber); return true; } // 過期包,丟棄 else if (sequenceNumber < mNextExpectedSequenceNumber) { return false; } // 超新包,加入待發ACK隊列,更新mNextNumber else if (sequenceNumber > mNextExpectedSequenceNumber) { // 這裏有個問題,當 a,b包近 b,a序到達時,只會發b的ack,而不會發a的ack // 因此會有對方收到了a,但卻沒有回覆a的狀況發生。 mNextExpectedSequenceNumber = sequenceNumber + 1; AddPendingAck(sequenceNumber); return true; } }
下面是寫 ack 的方法。
void DeliveryNotificationManager::WritePendingAcks( OutputMemoryBitStream& inPacket) { bool hasAcks = (mPendingAcks.size()>0); // 1. write hasAcks inPacket.Write(hasAcks); if(hasAcks) { // 2. write AckRange mPendingAcks.front().Write(inPacket); mPendingAcks.pop_front(); } }
3)處理確認。ACK包亂序時,如依次回覆確認包 A,B,C,當客戶端端先收到C,則A,B將會被看成Fail處理,雖然服務端正確收到並處理了A,B,C。
void DeliveryNotificationManger::ProcessAcks(InputMemoryBitStream& inPacket) { bool hasAcks; inPacket.Read(hasAcks); if (hasAcks) { AckRange ackRange; ackRange.Read(inPacket); // ACK的 Start PacketSequenceNumber nextAckdSequenceNumber = ack.Range.GetStart(); // ACK的 End uint32_t onePastAckedSequenceNumber = nextAckdSequenceNumber + acRange.GetCount(); while(nextAckdSequenceNumber<OnePastAckedSequenceNumber && !mInFlightPacket.empty()) { const auto& nextInFlightPacket = mInFlightPacket.front(); PacketSequenceNumber nextInFlightPacketSequenceNumber = nextInFlightPacket.GetSequenceNumber(); // 1. 確認包已超越 mNextInFlightPacketSequenceNumber,代表沒有確認包,反饋丟包 if (nextInFlightPacketSequenceNumber < nextAckdSequenceNumber) { auto copyOfInFlightPacket = nextInFlightPacket; mInFlightPackets.pop_front(); HandlePacketDeliveryFailure(copyOfInFlightPacket); } // 2. 確認包等於 mNextInFlightPacketSequenceNumber,代表收到確認包,反饋收到包 else if (nextInFlightPacketSequenceNumber == nextAckdSequenceNumber) { HandlePacketDeliverySuccess(nextInFlightPacket); mInFlightPackets.pop_front(); ++nextAckdSequenceNumber; } // 3. 確認包小於 mNextInFlightPacketSequenceNumber,直接將確認包跌至 nextAckdSequenceNumber) else if (nextInFlightPacketSequenceNUmber > nextAckdSequenceNumber) { nextAckdSequenceNumber = nextInFlightPacketSequenceNumber; } } } }
綜上,自定義UDP層有個特色,就是隻處理最新SEQ,ACK的包。
4)超時機制。
void DeliveryNotificationManager::ProcessTimedOutPackets() { uint64_t timeoutTime = Timing::sInstance.GetTimeMS() - kAckTimeout; while (!mInFlightPackets.empty()) { const auto& nextInFlightPacket = mInFlightPackets.front(); // 此方法有個條件,全部的請求必須有統一超時時間 if(nextInFlightPacket.GetTimeDispatched()<timeoutTime) { HandlePacketDeliveryFailure(nextInFlightPacket); mInFlightPackets.pop_front(); } else { break; } } }
5)每個包有本身的 HandleFail、HandleSucc 的實現。
void DeliveryNotificationManager::HandlePacketDeliveryFailure( const InFlightPacket& inFlightPacket) { ++mDroppedPacketCount; inFlightPacket.HandleDeliveryFailure(this); } void DeliveryNotificationManager::HandlePacketDeliverySuccess( const InFlightPacket& inFlightPacket) { ++mDeliveredPacketCount; inFlightPacket.HandleDeliverySuccess(this); }
九、沉默終端(dumb terminal)的三個問題:
1)延遲問題。
2)跳躍(無插值)問題。
3)瞄準問題。瞄準的始終是過去幾百毫秒的位置。
十、
十一、
十二、
1三、