游戲服務器IP/TCP協議(王者榮耀為例)


 

第一部分

先分別介紹IP/TCP協議族:

IP協議:

對於TCP/IP網絡來說,網絡層是其核心所在。該層的IP協議負責生成發往目的地的數據報以實現邏輯尋址,完成數據從網絡上一個節點向另一個節點的傳輸。

IP的主要目的是通過一個互聯的網絡傳輸數據報,涉及兩個最基本的功能。

●尋址(Addressing):IP協議根據數據報首部中包括的目的地址將數據報傳送到目的節點,這就要涉及傳送路徑的選擇,即路由功能。IP協議使用IP地址來實現路由。

●分片(Fragmentation): IP協議還提供對數據大小的分片和重組,以適應不同網絡對數據包大小的限制。如果網絡只能傳送小數據包,IP協議將對數據報進行分段並重新組成小塊再進行傳送。

IP是一個無連接的、不可靠的、點對點的協議,只能盡力(BestEffort)傳送數據,不能保證數據的到達。具體地講,主要有以下特性。

●IP協議提供無連接數據報服務,各個數據報獨立傳輸,可能沿着不同的路徑到達目的地,也可能不會按序到達目的地。

●IP協議不含錯誤檢測或錯誤恢復的編碼,屬於不可靠的協議。所謂不可靠,是從數據傳輸的可靠性不能保證的角度而言的,查詢的延誤及其他網絡通信故障都有可能導致所傳數據的丟失。對這種情況,IP協議本身不處理。它的不可靠並不能說明整個TCP/IP協議不可靠。如果要求數據傳輸具有可靠性,則要在IP的上面使用TCP協議加以保證。位於上一層的TCP協議則提供了錯誤檢測和恢復機制。

●作為一種點對點協議,雖然IP數據報攜帶源IP地址和目的IP地址,但進行數據傳輸時的對等實體一定是相鄰設備(同一網絡)中的對等實體。

IP協議的效率非常高,實現起來也較簡單。這是因為IP協議采用了盡力傳輸的思想,隨着底層網絡質量的日益提高,IP協議的盡力傳輸的優勢體現得更加明顯。

下圖是IP數據包的格式:

 

TCP/UDP協議:

傳輸層是TCP/IP協議中的非常重要的層次,提供了面向連接的傳輸控制協議(Transmission Control Protocol,TCP)和無連接的用戶數據報協議(User Datagram Protocol,UDP),負責提供端到端的數據傳輸服務,將任意數據通過網絡從發送方傳輸到接收方。TCP提供的是可靠的、可控制的傳輸服務,適用於各種網絡環境;UDP提供的服務輕便但不可靠,適用於可靠性較高的網絡環境。大部分Internet應用都使用TCP,因為它能夠確保數據不會丟失和被破壞。本章將對這兩種協議進行詳細分析。

在OSI模型中,傳輸層是介於網絡層和會話層之間的一個中間層次,彌補高層服務和網絡層服務之間的差距,並向高層用戶((應用程序)屏蔽通信子網的細節,使高層用戶看到的只是在兩個傳輸實體間的一條端到端的、用戶可控的、可靠的數據通路。在TCP/IP模型中,由於3個高層簡化為1個應用層,傳輸層是介於網絡層與應用層之間的一個層次。

網絡層協議提供網絡地址、路由、交付功能,而傳輸層協議提供了端到端數據傳輸的必要機制。傳輸層協議通常要負責以下幾項基本功能。

●創建進程到進程的通信,進程即正在運行的應用程序。進程之間通過傳輸層進行通信,發送進程向傳輸層發送數據,接收進程從傳輸層接收數據。

●提供控制機制,如流量控制、差錯控制。數據鏈路層定義相鄰節點的流量控制,而傳輸層定義端到端用戶之間的流量控制。

●提供連接機制。在數據傳輸開始時,通信雙方需要建立連接。在傳輸過程中,雙方還需要繼續通過協議來通信以驗證數據是否被正確接收。數據傳輸完成后,任一方都可關閉連接。

 

第二部分

下面就以王者榮耀為例說明TCP/IP協議族在網絡游戲中的應用。

2.1啟動、登錄游戲

    首先我們啟動王者榮耀游戲APP。進人APP資頁,第一步會進行版本更新檢測。若檢查到資源包有更新則進行下載,若未檢查到更新的資源包,則本地加載游戲資源包及解壓資源包。這期間會跟圖片服務器(image.smoba.qq.com),用戶信息(game.eve.mdt.qq.com game.str.mdt.qq.com)、數據服務器(down.qq.com,dliedl.qq.com)進行交互,游戲界面加載過程主要是TCP傳輸。

     通過對啟動階段進行抓包分析,游戲啟動過程中,客戶端通過DNS域名解析獲得王者榮耀游戲服務器的IP地址,建立TCP鏈接,進行數據交互來啟動游戲。

2.2游戲對戰階段

       在實時對戰的過程中,客戶端與服務器間主要有兩個交互連接,一個為TCP連接,一個為UDP連接。開戰(玩家選擇自己的英雄角色及技能)及游戲分出勝負(水晶被毀)時,會觸發大量UDP包,包數量大於150;正式游戲過程中,終端與主服務器保持UDP和TCP連接。

    (1)TCP長連接

    游戲客戶端與服務器之間建立一個TCP長連接,由終端發起,通過這個TCP長連接進行心跳和其它信息交互,用以確認服務器狀態正常,心跳間隔3s,消息大小固定,流程如圖1所示:

 

      (2)UDP報文

    客戶端和服務器之間交互的報文,除了TCP連接報文以外,還有大量的UDP報文,分為兩類:

    上行UDP報文:客戶端通過上行UDP報文將玩家所做的操作上報給服務器。

    下行UDP報文:服務器匯總參加對戰的所有玩家的操作,通過下行UDP報文廣播給參加對戰的所有玩家的客戶端。

    

    據騰訊消息,王者榮耀游戲采用的同步機制為幀同步(非狀態同步),主要流程如下:

    1、玩家上報操作。

    2、服務器收集各玩家上報的各自操作,進行匯總,以固定的時間間隔(例如60 ms)向參加對方的各玩家廣播所有玩家的操作。

    令各客戶端接收到廣播,知道了所有玩家的操作,按照相同的游戲邏輯進行運算,得到相同的結果,呈現在游戲界面上。

通過上述幀同步機制,基本可以保證參加對戰的各玩家游戲步調是一致的,即游戲玩家間的顯示基本是相同的(因為網絡時延的不同會略有差異)。圖2王者榮耀游戲幀同步流程:

 

    根據以上分析,總結如下:

    一、王者榮耀游戲的啟動和登錄采用TCP連接,並且游戲交互過程中保持TcP連接通過心跳包(3s間隔)來檢測用戶是否在線。

    二、王者榮耀游戲的客戶端操作與界面顯示是通過UDP數據流與服務器進行交互的,下行速率最小需80 kbps,上行速率需64 kbps。因此王者榮耀游戲對速率要求較小。

第三部分

推薦一個實現TCP/IP的簡單代碼。摘自(73條消息) C++:實現socket通信(TCP/IP)實例_Cche的博客-CSDN博客_c++ socket

(1)server端代碼

#include "pch.h"
#include<iostream>
#include<winsock.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
void initialization();
int main() {
    //定義長度變量
    int send_len = 0;
    int recv_len = 0;
    int len = 0;
    //定義發送緩沖區和接受緩沖區
    char send_buf[100];
    char recv_buf[100];
    //定義服務端套接字,接受請求套接字
    SOCKET s_server;
    SOCKET s_accept;
    //服務端地址客戶端地址
    SOCKADDR_IN server_addr;
    SOCKADDR_IN accept_addr;
    initialization();
    //填充服務端信息
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(5010);
    //創建套接字
    s_server = socket(AF_INET, SOCK_STREAM, 0);
    if (bind(s_server, (SOCKADDR *)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR) {
        cout << "套接字綁定失敗!" << endl;
        WSACleanup();
    }
    else {
        cout << "套接字綁定成功!" << endl;
    }
    //設置套接字為監聽狀態
    if (listen(s_server, SOMAXCONN) < 0) {
        cout << "設置監聽狀態失敗!" << endl;
        WSACleanup();
    }
    else {
        cout << "設置監聽狀態成功!" << endl;
    }
    cout << "服務端正在監聽連接,請稍候...." << endl;
    //接受連接請求
    len = sizeof(SOCKADDR);
    s_accept = accept(s_server, (SOCKADDR *)&accept_addr, &len);
    if (s_accept == SOCKET_ERROR) {
        cout << "連接失敗!" << endl;
        WSACleanup();
        return 0;
    }
    cout << "連接建立,准備接受數據" << endl;
    //接收數據
    while (1) {
        recv_len = recv(s_accept, recv_buf, 100, 0);
        if (recv_len < 0) {
            cout << "接受失敗!" << endl;
            break;
        }
        else {
            cout << "客戶端信息:" << recv_buf << endl;
        }
        cout << "請輸入回復信息:";
        cin >> send_buf;
        send_len = send(s_accept, send_buf, 100, 0);
        if (send_len < 0) {
            cout << "發送失敗!" << endl;
            break;
        }
    }
    //關閉套接字
    closesocket(s_server);
    closesocket(s_accept);
    //釋放DLL資源
    WSACleanup();
    return 0;
}
void initialization() {
    //初始化套接字庫
    WORD w_req = MAKEWORD(2, 2);//版本號
    WSADATA wsadata;
    int err;
    err = WSAStartup(w_req, &wsadata);
    if (err != 0) {
        cout << "初始化套接字庫失敗!" << endl;
    }
    else {
        cout << "初始化套接字庫成功!" << endl;
    }
    //檢測版本號
    if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wHighVersion) != 2) {
        cout << "套接字庫版本號不符!" << endl;
        WSACleanup();
    }
    else {
        cout << "套接字庫版本正確!" << endl;
    }
    //填充服務端地址信息
 
}

(2)client端:

#include "pch.h"
#include<iostream>
#include<winsock.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
void initialization();
int main() {
    //定義長度變量
    int send_len = 0;
    int recv_len = 0;
    //定義發送緩沖區和接受緩沖區
    char send_buf[100];
    char recv_buf[100];
    //定義服務端套接字,接受請求套接字
    SOCKET s_server;
    //服務端地址客戶端地址
    SOCKADDR_IN server_addr;
    initialization();
    //填充服務端信息
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(1234);
    //創建套接字
    s_server = socket(AF_INET, SOCK_STREAM, 0);
    if (connect(s_server, (SOCKADDR *)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR) {
        cout << "服務器連接失敗!" << endl;
        WSACleanup();
    }
    else {
        cout << "服務器連接成功!" << endl;
    }
 
    //發送,接收數據
    while (1) {
        cout << "請輸入發送信息:";
        cin >> send_buf;
        send_len = send(s_server, send_buf, 100, 0);
        if (send_len < 0) {
            cout << "發送失敗!" << endl;
            break;
        }
        recv_len = recv(s_server, recv_buf, 100, 0);
        if (recv_len < 0) {
            cout << "接受失敗!" << endl;
            break;
        }
        else {
            cout << "服務端信息:" << recv_buf << endl;
        }
 
    }
    //關閉套接字
    closesocket(s_server);
    //釋放DLL資源
    WSACleanup();
    return 0;
}
void initialization() {
    //初始化套接字庫
    WORD w_req = MAKEWORD(2, 2);//版本號
    WSADATA wsadata;
    int err;
    err = WSAStartup(w_req, &wsadata);
    if (err != 0) {
        cout << "初始化套接字庫失敗!" << endl;
    }
    else {
        cout << "初始化套接字庫成功!" << endl;
    }
    //檢測版本號
    if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wHighVersion) != 2) {
        cout << "套接字庫版本號不符!" << endl;
        WSACleanup();
    }
    else {
        cout << "套接字庫版本正確!" << endl;
    }
    //填充服務端地址信息
 
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM