Tekkaman

导航

 

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

posted on 2019-07-28 23:31  Tekkaman  阅读(1612)  评论(0编辑  收藏  举报