• 网络多人游戏架构与编程2


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

  • 相关阅读:
    跨域问题(使用jsonp解决)
    通用Mapper使用(Maven+SSM)
    hosts文件修改(域名和IP地址对应修改)
    ExportExcel(用poi导出Excel文件:用List<Map<String,Object>>)
    activiti已结束子流程退回
    一个数据库死锁的案例
    mysql函数使用的例子
    mybatis拦截器
    Objects工具类
    Spring Boot 中读取配置属性
  • 原文地址:https://www.cnblogs.com/tekkaman/p/11287719.html
Copyright © 2020-2023  润新知