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


    网络多人游戏架构与编程1

    1、即使在今天,大多数的多人在线游戏在每个游戏会话中仍然限制玩家的数量 ,一般支持4~32个玩家。然而,在大规模多人在线游戏(massive multiplayer online gmme,MMO)中,成百上千的玩家将同时出现在同一个游戏会话中。

    2、《星际围攻:部落》的开发者们最终将数据分为以下4种类型:

      1)非保障数据。当带宽有限时,游戏选择首先丢弃这些数据。

      2)保障数据。

      3)最近状态的数据。只有最新玩家数据才是重要数据的场合。如游戏知道了玩家当前的生命值,那么他5秒之前的生命值就不重要了。

      4)最快保障数据。如一个玩家的移动信息,在一个非常短的时间内极其重要,因此要忙传输。

    3、对等网络模型需要O(n^2)的带宽,而C/S模型只需要O(n)带宽。

    4、《星际围攻:部落》的网络模型:

      

      1)平台数据包模块。标准套接字API的封装,可以构建和发送不同的数据包格式。

      2)连接管理器。将网络中两台计算机之间的连接抽象化,连接管理器是不可靠的,它保证投递状态通知的正确传输。

      3)流管理器。决定允许数据传输的最大速率。把请求按优先次序排列好,在带宽限制下,移动管理器、事件管理器、ghost管理器拥有最高优先级。

      4)事件管理器。维持游戏模拟层产生的事件队列,这些事件可以看作是远程过程调用(remote procedure call, RPC)。

      5)Ghost管理器。复制被认为与指定客户端相关的动态对象。这些信息按优先级分为“必须知道的”、“最好知道的”

      6)移动管理器。当有移动数据可用时,流管理器总是给出站数据包添加所有的移动管理器数据。

    5、在分组交换出现之前,长距离系统间传输信息使用电路交换。在传输的过程中,该电路要始终保持连通。一个时刻只能用于一个目的。

      

      分组交换取消了电路交换一个时刻只能用于一个传输的限制,提供更高的可用性。它将传输的信息拆分为小块(数据包),基于一个存储转发的技术将他们发送到共享的线路中。

      

    6、系统互联5层模型。

      

    7、每种被选择作为物理层实现的物理介质,都有对应的协议或协议族来提供链路层所需要的服务。

      

    8、网卡(Network Interface Controller,NIC)。

      以太网链路层的帧格式,对于每一个数据包,其前导序列(preamble)、和帧开始标志(start frame delimiter,SFD)都是一样的,包含7个十六进制值  0x55 以及一个 0xD5。

      

      以太网标准规定帧数数据最大长度为1500字节,称为最大传输单元(maximum transimission unit, MTU)。

      帧检验序列(frame check sequences,FCS),用于保证收到的帧数据的正确性。显然,以太网帧只能保证如果收到的数据其数据肯定是对的,但并不保证一定能收到数据

      

    9、ARP映射表
      

    10、IP路由表
      

    11、IP数据包的长度比链路层的最大传输单元长怎么办?答案是分片(fragmentation)。

    12、1024~49151称为用户端口(user port)或注册端口(registered port)。任何协议和应用开发者可以向IANA申请这个范围的端口号。

      0~1023称为系统端口(system port)或预留端口(reversed port)。大部分操作系统,只允许 root级别的进程才能绑定系统端口。

      49152~65535称为动态端口(dynamic port)。

    13、TCP协议需要维持的状态变量:

      

    14、带宽限制 = 接收窗口 / RTT。

    15、socket,af参数为 AF_INET(IPv4)。

      

        

      

      以下调用创建一个UDP socket:

      SOCKET udpSocket = socket(AF_INET,SOCK_DGRAM,0);

      以下调用创建一个TCP socket:

      SOCKET tcpSocket = socket(AF_INET,SOCK_STREAM,0);

      操作系统为每个数据创建IP头、传输层头。但是,通过创建type为SOCK_RAW和protocol为0的socket,可以直接写这两层头部的值。

    16、socket库中大部分与平台无关的的函数使用写小字母,如socket。Windows下的winsock2函数以大写字母开头,有时使用 WSA前缀,来标记它们为非标准函数。

    17、getaddrinfo() 执行DNS查询,会阻塞线程。Windows提供了GetAddrInfoEx函数,它允许无需手工创建的异步操作。

      

    18、sockaddr 是通用地址,注意其成员以 sa_  开头。

      

      sockaddr_in 是IPv4地址,in指的是 internet。注意基成员以 sin_ 开头。

       

       

    19、inet_pton()、InetPton()将字符串初始转为 in_addr。

      

      

    20、socket 在用于发送和接收数据之前必须要bind。如果一个进程试图使用一个未 bind 的 socket 发送数据,网络库将自动为这个 socket 绑定一个可用的端口。

    21、UDP Socket。sendto()的返回值,仅表示已经成功进入发送队列。

    int sendto(SOCKET sock, const char *buf, int len, int flags, const sockaddr *to, int tolen);

      recvfrom() 是接收数据,如果没能可读数据,线程将被阻塞,直到有数据到达。一理 recvfrom() 调用成功,socket 库将不再保存数据副本。如果 len 小于未读数据大小,则超过 len 的未读数据将被丢弃。

      flags 的一个选项是 MSG_PEEK,意思不这次读取不删除缓冲区,以便下一次可以再读取。

      一个常见的错误是,调用者希望通过设置这个参数来要求只接收来自特定地址的数据包,这是不可能的。所有数据报按序交付给recvfrom函数,

    int recvfrom(SOCKET sock, char *buf, int len, int flags, sockaddr *from, int *fromlen);

    22、UDP是无状态的、无连接的、不可靠的,所以每台主机只需要一个单独的socket来发送和接收数据。

      但TCP是可靠的,需要发送数据前,在两台主机之间建立连接,此外必须维护和存储状态以重新发送丢失的数据包。因此针对每一个TCP连接,都需要一个额外的、单独的socket。

      如果 accept 函数执行成功,将创建一个可以与远程主机通信的新socket。这个新socket被绑定到与监听socket相同的端口号上。当操作系统收到一个目的端口是该绑定端口的数据包时,它使用源地址、源端口来确定哪个socket应该接收这个数据。

      监听 socket 没有连接任何主机,仅仅扮演调度者的角度。使用监听 socket 给远程主机发送数据,将会失败。

      如果没有新连接,accept函数阻塞,直到有新连接或超时。

    23、TCP 中 Client 使用 connect() 函数连接服务器。connect() 函数阻塞调用线程,直到连接被接受或超时。

    int connect(SOCKET sock, const sockaddr *addr, int addrlen);

      send() 函数,调用成功返回发送大小。如果缓冲区大小小于 len,则返回的值会比 len 小。如果缓冲区空间已满,则send()函数将阻塞,直到超时或有了空闲缓冲空间。注意,send()函数的返回成功,只表示数据已经插入队列等待发送,并不表示已经发送出去了。

    int send(SOCKET sock, const char *buf, int len, int flags)

      recv()函数,当len非0,而返回值为0时,说明对方主机发送了FIN包。当len为0,而返回值为0时,说明socket上有可读的数据。如果socket上没有数据可读,recv()函数将阻塞。

    24、TCP、UDP socket 需要注意的地方。

      可以在 tcp socket 上使用 sendto、recvfrom函数,但是地址参数将被忽略。在一些平台上,udp socket 上可以调用 connect 函数,以和远程地址绑定。

    25、windows下使用 ioctrlsocket()设置 socket 选项。cmd的取值如 FIONBIO,argp任意非零值开启非阻塞,0将阻止开启。

    int ioctrlsocket(SOCKET sock, long cmd, u_long* argp);

      posix 兼容的操作系统下,使用 fcntl 函数。必须先用 cmd 为 F_GETFL 获取状态,将取到的状态与 O_NONBLOCK按位或运算,再使用 F_SETFL cmd 进行更新。

    int fcntl(int sock, int cmd, ...);

    26、Select > 非阻塞IO > 多线程

      

      select函数如下:

      

    27、一个简单的TCP服务器循环。

    void DoTCPLoop()
    {
        // 1. 创建
        TCPSocketPtr listenSocket = SocketUtil::CreateTCPSocket(INET);
        
        // 2. Bind
        SocketAddress receivingAddress(INADDR_ANY, 48000);
        if (listenSocket->Bind(receivingAddres)!=NO_ERROR)
        {
            return;
        }
        
        // 3. Read Pending Socket
        vector<TCPSocketPtr> readBlockSockets;
        readBlockSockets.push_back(listenSocket);
        
        vector<TCPSocketPtr> readableSockets;
        
        while(gIsGameRunning)
        {
            // 4. Select
            if (SocketUtil::Select(&readBlockSockets, &readableSockets, 
                                    nullptr, nulllptr, nullptr, nullptr))
            {
                // 5. 遍历 ReadableSockets
                for (const TCPSocketPtr& socket: readableSockets)
                {
                    if (socket == listenSocket)
                    {
                        SocketAddress newClientAddress;
                        auto newSocket = listenSocket->Accept(newClientAddress);
                        // 6. 加入 Read Pending Socket
                        readBlockSockets.push_back(newSocket);
                        ProcessNewClient(newSocket, newClientAddress);
                    }
                    else
                    {
                        // it's a regular socket-process the data...
                        char segment[GOOD_SEGMENT_SIZE];
                        // 7. Process Client Request
                        int dataReceived = socket->Receive(segment, GOOD_SEGMENT_INT);
                        if(dataReceived>0){
                            ProcessDataFromClient(socket, segment, dataReceived);
                        }
                    }
                }
            }
        }
        
    }
    View Code

    28、setsockopt

    int setsockotp(SOCKET sock, int level, int optname, const char *optval, int optlen);

      包括以下常用选项:

      1)SO_REUSEADDR

        

      2)SO_KEEPALIVE

          

      3)TCP_NODELAY
        

    29、压缩

      1)稀疏数组压缩。

      2)熵编码。用某个较短的值代替较长的值。

      3)定点。用离散值代表连续值。

    30、基本的反射系统。

      1)先定义基本类型。

    enum EPrimitiveType
    {
        EPT_Int,
        EPT_String,
        EPT_Float
    };

      2)成员变量的封装。

    class MemberVariable
    {
    public:
        MemberVariable(const char* inName, EPrimitiveType inPrimitiveType, uint32_t inOffset):
                        mName(inName),mPrimitiveType(inPrimitiveType),mOffset(inOffset){}
                        
        EPrimitiveType GetPrimitiveType() const {return mPrimitiveTYpe;}
        uint32_t GetOffset() const {return mOffset;}
        
    private:
        std::string     mName;
        EPrimitiveType     mPrimitiveType;
        uint32_t        mOffset;
    }

      3)成员变量容器。

    class DataType
    {
    public:
        DataType(std::initializer_list<const MemberVariable&> inMVs):
        mMemberVariables(inMVs){}
        
        const std::vector<MemberVariable>& GetMemberVariables() const
        {
            return mMemberVariables;
        }
        
    private:
        std::vector<MemberVariable> mMemberVariables;
    }

    31、基于基本的反射系统的简单序列化函数。

    // inData: 对象指针
    // inDataType: 对象成员列表
    void Serialize(MemoryStream* inMemoryStream,const DataType* inDataType, uint8_t* inData)
    {
        for(auto& mv:inDataType->GetMemberVariables())
        {
            void* mvData = inData + mv.GetOffset();
            switch(mv.GetPrimitiveType())
            {
                EPT_Int:
                    inMemoryStream->Serialize(*(int*)mvData);
                    break;
                EPT_String:
                    inMemoryStream->Serialize(*(std::string*)mvData);
                    break;
                EPT_Float:
                    inMemoryStream->Serialize(*(float*)mvData);
                    break;
            }
        }
    }

    32、传输对象三步曲。从一台主机向另一台主机传输对象的行为称为复制(replication)。

      1)对象ID。LinkingContext

      2)类ID。ObjectCreationRegistry

      3)对数数据的序列化。

    33、游戏状态的增量更新,包含三种操作:增、改、删。

    enum ReplicationAction
    {
        RA_Create,
        RA_Update,
        RA_Destroy,
        RA_MAX
    }

    34、服务器客户端代码分离。

      

    35、RPC框架中,每一个 RPC Function 都对应一个 RCPUnwrapFunc,如:

      

      RPCManager 会使用到上面的 RCPUnwrapFunc。

    class RPCManager
    {
    public:
        void RegisterUnwrapFunction(uint32_t inName, RPCUnwrapFunc inFunc)
        {
            assert(mNameToRPCTable, find(inName)==mNameToRPCTable.end());
            mNameToRPCTable[inName]=inFunc;
        }
        
        void ProcessRPC(InputMemoryBitStream& inStream)
        {
            uint32_t name;
            inSteram.Read(name);
            mNameToRPCTable[name](inStream);
        }
        
        unordered_map<uint32_t, RPCUnwrapFunc> mNameToRPCTable;
    }
    View Code

      下面是一个 RCPUnwrapFUnc 的示例。

    void UnwrapPlaySound(InputMemoryBitStream& inStream)
    {
        string soundName;
        Vector3 location;
        float volume;
        
        // 此处解参数
        inStream.Read(soundName);
        inStream.Read(location);
        inStream.Read(volume);
        
        // 此处调用真正的函数
        PlaySound(soundName, location, volume);
    }
    
    void RegisterRPCs(RPCManager* inRPCManager)
    {
        inRPCManager->RegisterUnwrapFunction('PSND', UnwrapPlaySound);
    }
    View Code

    37、对等网络中,主对等体的主要目的是提供游戏中已知的对等体的IP地址。除了这一个特例,主对等体与其他对等体行为一致。所以如果主对等体断开了,游戏仍然可以继续。

    38、

    39、

    40、

  • 相关阅读:
    好玩夫妻
    笔记整理MS SQL2005 中查询表的字段信息,
    庆幸也与你逛过那一段旅程
    PureMVC
    简单工厂模式
    工厂方法模式
    UML类图
    PureMVC
    oracle双机热备
    一个不错的免费网络硬盘
  • 原文地址:https://www.cnblogs.com/tekkaman/p/11261528.html
Copyright © 2020-2023  润新知