【网络多人游戏架构与编程2】
1.0、虚拟现实游戏是对延迟最敏感的, 因为我们人类只要头旋转了,眼睛就期望看到不同的事物。在这些情况下,保证用户感觉在虚拟现实世界中就要求延迟少于 20 毫秒。
格斗游戏、 第一人称射击游戏和其他动作频繁的游戏是对延迟第二敏感的。 这些游戏的延迟范围可以从16 毫秒到150毫秒。
RTS游戏是对延迟容忍度最高的, 这个容忍度通常很有用, 正如第 6 章所介绍的。 这些游戏的延迟可以高达 500 毫秒, 而不影响用户体验。
1.1、非网络延迟。
1)输入采样延迟(input sampling latency)。用户按下一个按钮到游戏检测到这个按钮的时间可能很长。下图表明,游戏循环架构可能导致 Input Sampling Latency 高达接近 2帧的时间。
2)渲染流水线延迟(render pipeline latency)。驱动程序将绘制命令插入到缓冲区,GPU在未来的某个时刻执行。如果有许多渲染任务要做,可能会导致滞后 1帧 渲染出来。
3)多线程渲染流水线延迟(multithreaded render pipeline latency)。
4)垂直同步(VSync)
5)显示延迟(display lag)。显示器可能会对画面进行调整。
6)像素响应时间(pixel response time)。像素改变需要时间,大概几毫秒。
2、数据包传输过程中,有四种主要的延迟:
1)处理延迟(processing delay)。网络路由器的工作包括:读取数据包、检查目的IP、找出下一台机器等。
2)传输延迟(transmission dely)。链路层将数据写入物理层(转为物理层信号)的时间。
3)排除延迟(queuing delay)。
4)传播延迟(propagation delay)。例如从东海岸传播到西海岸的时间。
包含1400字节负载的数据包与包含200字节负载的数据包通常经历相同时间的处理延迟。如果你发送 7 个包含 200 字节负载的数据包, 最后那个数据包将不得不在队列中等待前面6 个数据包的处理, 这样将经历比一个大数据包更多的累积网络延迟。
3、网络抖动会导致数据包乱序到达。
4、数据包丢失的情况。数据包丢失必然会产生,无法避免。
1)不可靠的物理介质。电磁干扰可能导致依赖损坏或丢失,如微波炉的工作。
2)不可靠的链路。有时链路层信道完全满了,必须丢失正在发送的帧。
3)不可能的网络层。当路由器队列满了,后续到达的包将被丢弃。
5、路由器并不一定丢弃最后到达的报文。例如,有些路由器在丢弃TCP报文之前先丢弃UDP报文,因为它们知道丢弃TCP的报文会自动重传。
6、TCP的几大问题,最大问题是强制可靠性。
1)低优先级数据的丢失干扰高优先级数据的接收。例如,依次发送声音报文、技能报文,如果声音报文未收到,则永远不触发技能报文,但对玩家来说,声音播放与否无关紧要,但技能必须立即播放。
2)不相关数据流的想到干扰。例如技能报文、聊天报文使用同一个 TCP 连接,则一种报文的丢失会影响另一个报文。
3)过时游戏状态重传。
TCP中的 Nagle 算法起了非常不好的作用, 因为它在将数据包发送出去之前可以延迟长达0.5秒。事实上,使用 TCP 作为传输层协议的游戏通常禁用 Nagle 算法以避免这个问题, 虽然同时放弃了它提供的减少数据包数量的优势。
最后,TCP 为管理连接和跟踪所有可能被重传的数据分配了很多资源。这些分配通常是由操作系统管理的, 游戏需要时很难通过自定义内存管理器的方式跟踪和路 由。
7、通过UDP,可以自定义一个系统,在发生丢包时,只发送最新消息,而不是重传丢失的数据。有些第三方的UDP网络库可以使用,如 RakNet、Photon。
8、自建可靠的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); }
9、沉默终端(dumb terminal)的三个问题:
1)延迟问题。
2)跳跃(无插值)问题。
3)瞄准问题。瞄准的始终是过去几百毫秒的位置。
10、
11、
12、
13、