您的位置:首页 > 理论基础 > 计算机网络

游戏服务器IP/TCP协议(王者荣耀为例)

2021-12-13 23:17 1016 查看

 

第一部分

先分别介绍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;
}
//填充服务端地址信息

}

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: