【網絡多人遊戲架構與編程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); } } } } } }
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; }
下面是一個 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); }
3七、對等網絡中,主對等體的主要目的是提供遊戲中已知的對等體的IP地址。除了這一個特例,主對等體與其餘對等體行爲一致。因此若是主對等體斷開了,遊戲仍然能夠繼續。
3八、
3九、
40、