網絡多人遊戲架構與編程1

網絡多人遊戲架構與編程1編程

一、即便在今天,大多數的多人在線遊戲在每一個遊戲會話中仍然限制玩家的數量 ,通常支持4~32個玩家。然而,在大規模多人在線遊戲(massive multiplayer online gmme,MMO)中,成百上千的玩家將同時出如今同一個遊戲會話中。windows

二、《星際圍攻:部落》的開發者們最終將數據分爲如下4種類型:數組

  1)非保障數據。當帶寬有限時,遊戲選擇首先丟棄這些數據。服務器

  2)保障數據。網絡

  3)最近狀態的數據。只有最新玩家數據纔是重要數據的場合。如遊戲知道了玩家當前的生命值,那麼他5秒以前的生命值就不重要了。多線程

  4)最快保障數據。如一個玩家的移動信息,在一個很是短的時間內極其重要,所以要忙傳輸。架構

三、對等網絡模型須要O(n^2)的帶寬,而C/S模型只須要O(n)帶寬。框架

四、《星際圍攻:部落》的網絡模型:異步

  

  1)平臺數據包模塊。標準套接字API的封裝,能夠構建和發送不一樣的數據包格式。socket

  2)鏈接管理器。將網絡中兩臺計算機之間的鏈接抽象化,鏈接管理器是不可靠的,它保證投遞狀態通知的正確傳輸。

  3)流管理器。決定容許數據傳輸的最大速率。把請求按優先次序排列好,在帶寬限制下,移動管理器、事件管理器、ghost管理器擁有最高優先級。

  4)事件管理器。維持遊戲模擬層產生的事件隊列,這些事件能夠看做是遠程過程調用(remote procedure call, RPC)。

  5)Ghost管理器。複製被認爲與指定客戶端相關的動態對象。這些信息按優先級分爲「必須知道的」、「最好知道的」

  6)移動管理器。當有移動數據可用時,流管理器老是給出站數據包添加全部的移動管理器數據。

五、在分組交換出現以前,長距離系統間傳輸信息使用電路交換。在傳輸的過程當中,該電路要始終保持連通。一個時刻只能用於一個目的。

  

  分組交換取消了電路交換一個時刻只能用於一個傳輸的限制,提供更高的可用性。它將傳輸的信息拆分爲小塊(數據包),基於一個存儲轉發的技術將他們發送到共享的線路中。

  

六、系統互聯5層模型。

  

七、每種被選擇做爲物理層實現的物理介質,都有對應的協議或協議族來提供鏈路層所須要的服務。

  

八、網卡(Network Interface Controller,NIC)。

  以太網鏈路層的幀格式,對於每個數據包,其前導序列(preamble)、和幀開始標誌(start frame delimiter,SFD)都是同樣的,包含7個十六進制值  0x55 以及一個 0xD5。

  

  以太網標準規定幀數數據最大長度爲1500字節,稱爲最大傳輸單元(maximum transimission unit, MTU)。

  幀檢驗序列(frame check sequences,FCS),用於保證收到的幀數據的正確性。顯然,以太網幀只能保證若是收到的數據其數據確定是對的,但並不保證必定能收到數據

  

九、ARP映射表
  

十、IP路由表
  

十一、IP數據包的長度比鏈路層的最大傳輸單元長怎麼辦?答案是分片(fragmentation)。

十二、1024~49151稱爲用戶端口(user port)或註冊端口(registered port)。任何協議和應用開發者能夠向IANA申請這個範圍的端口號。

  0~1023稱爲系統端口(system port)或預留端口(reversed port)。大部分操做系統,只容許 root級別的進程才能綁定系統端口。

  49152~65535稱爲動態端口(dynamic port)。

1三、TCP協議須要維持的狀態變量:

  

1四、帶寬限制 = 接收窗口 / RTT。

1五、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,能夠直接寫這兩層頭部的值。

1六、socket庫中大部分與平臺無關的的函數使用寫小字母,如socket。Windows下的winsock2函數以大寫字母開頭,有時使用 WSA前綴,來標記它們爲非標準函數。

1七、getaddrinfo() 執行DNS查詢,會阻塞線程。Windows提供了GetAddrInfoEx函數,它容許無需手工建立的異步操做。

  

1八、sockaddr 是通用地址,注意其成員以 sa_  開頭。

  

  sockaddr_in 是IPv4地址,in指的是 internet。注意基成員以 sin_ 開頭。

   

   

1九、inet_pton()、InetPton()將字符串初始轉爲 in_addr。

  

  

20、socket 在用於發送和接收數據以前必需要bind。若是一個進程試圖使用一個未 bind 的 socket 發送數據,網絡庫將自動爲這個 socket 綁定一個可用的端口。

2一、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);

2二、UDP是無狀態的、無鏈接的、不可靠的,因此每臺主機只須要一個單獨的socket來發送和接收數據。

  但TCP是可靠的,須要發送數據前,在兩臺主機之間創建鏈接,此外必須維護和存儲狀態以從新發送丟失的數據包。所以針對每個TCP鏈接,都須要一個額外的、單獨的socket。

  若是 accept 函數執行成功,將建立一個能夠與遠程主機通訊的新socket。這個新socket被綁定到與監聽socket相同的端口號上。當操做系統收到一個目的端口是該綁定端口的數據包時,它使用源地址、源端口來肯定哪一個socket應該接收這個數據。

  監聽 socket 沒有鏈接任何主機,僅僅扮演調度者的角度。使用監聽 socket 給遠程主機發送數據,將會失敗。

  若是沒有新鏈接,accept函數阻塞,直到有新鏈接或超時。

2三、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()函數將阻塞。

2四、TCP、UDP socket 須要注意的地方。

  能夠在 tcp socket 上使用 sendto、recvfrom函數,可是地址參數將被忽略。在一些平臺上,udp socket 上能夠調用 connect 函數,以和遠程地址綁定。

2五、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, ...);

2六、Select > 非阻塞IO > 多線程

  

  select函數以下:

  

2七、一個簡單的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

2八、setsockopt

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

  包括如下經常使用選項:

  1)SO_REUSEADDR

    

  2)SO_KEEPALIVE

      

  3)TCP_NODELAY
    

2九、壓縮

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

3一、基於基本的反射系統的簡單序列化函數。

// 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;
        }
    }
}

3二、傳輸對象三步曲。從一臺主機向另外一臺主機傳輸對象的行爲稱爲複製(replication)。

  1)對象ID。LinkingContext

  2)類ID。ObjectCreationRegistry

  3)對數數據的序列化。

3三、遊戲狀態的增量更新,包含三種操做:增、改、刪。

enum ReplicationAction
{
    RA_Create,
    RA_Update,
    RA_Destroy,
    RA_MAX
}

3四、服務器客戶端代碼分離。

  

3五、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

3七、對等網絡中,主對等體的主要目的是提供遊戲中已知的對等體的IP地址。除了這一個特例,主對等體與其餘對等體行爲一致。因此若是主對等體斷開了,遊戲仍然能夠繼續。

3八、

3九、

40、

相關文章
相關標籤/搜索