Tekkaman

导航

 

网络多人游戏架构与编程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 }
View Code

  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;
            }
        }
        
    }
}
View Code

   综上,自定义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;
        }
        
    }
}
View Code

   5)每一个包有自己的 HandleFail、HandleSucc 的实现。

void DeliveryNotificationManager::HandlePacketDeliveryFailure(
    const InFlightPacket& inFlightPacket)
{
    ++mDroppedPacketCount;
    inFlightPacket.HandleDeliveryFailure(this);
}

void DeliveryNotificationManager::HandlePacketDeliverySuccess(
    const InFlightPacket& inFlightPacket)
{
    ++mDeliveredPacketCount;
    inFlightPacket.HandleDeliverySuccess(this);
}
View Code

 9、沉默终端(dumb terminal)的三个问题:

  1)延迟问题。

  2)跳跃(无插值)问题。

    

  3)瞄准问题。瞄准的始终是过去几百毫秒的位置。  

10、

11、

12、

13、

posted on 2019-08-02 11:49  Tekkaman  阅读(849)  评论(0编辑  收藏  举报