网络多人游戏架构与编程 笔记
文章目录
网络多人游戏架构与编程(Multiplayer Game Programming)笔记
互联网
TCP/IP 模型
分组交换:基于一种叫做存储转发的技术将它们发送到共享的线路中。取消了电路一个时刻只能用于一个传输的限制,将信息拆分为小块,成为分组或数据包。
物理层
物理介质,同轴电缆和光纤等。
链路层
提供一种方法,实现源主机封装信息、通过物理层传输信息、目的主机接受信息并从中提取信息。
- 对于每种物理介质,都有对应的协议来提供链路层需要的服务。比如2.4GHz无线电波的802.11b/g/n,5GHz的802.11n/ac,光纤的光纤分布式数据接口。
- 为了唯一标识主机,以太网引入了介质访问控制地址 MAC。Mac理论上是一个48比特数字。一台主机可能有多个MAC地址,每个地址对应一个网卡。由于MAC地址可以修改,所以不再可靠。
- 链路层的数据单元成为帧。对于每个数据包,其帧格式都是一样的。以太网规定帧内封装的报文数据最大长度为1500字节,称为最大传输单元 MTU。许多现代网卡支持的 MTU 大于1500字节,为了支持这个长度,一般要在帧头指定一种以太网类型。

链路层的不足:MAC固定地址的不灵活性,不支持划分更小的局域网,没有定义从一个链路层到另一个链路层的通信方法
网络层
在链路层的基础上提供一套逻辑地址的基础设施。
- 方便硬件更换地址,主机群可划分子网。子网中的主机可以用不同的链路层协议和物理介质相互发送信息。
- 互联网协议第四版 IPV4,定义了一个为每台主机单独标识的逻辑寻址系统,一个定义地址空间的逻辑分段作为物理子网的子网系统,一个在子网之间转发数据的路由系统。
- IPV4的IP地址是32比特数字,以4个8比特数字展示,比如10.1.1.1。

IP数据包总长,16比特,能表示的最大数是65535,因为IP数据包的包头至少要20字节的必须信息,所以数据包最大长度为65515字节。TTL 生存时间,用于限制数据包的转发次数,每次转发会减一。
- 
IPV4允许数据包的目标地址是IP地址,而用链路层发送数据时需要的是MAC地址。有一个链路层协议提供了一种IP地址和MAC地址的对应关系表,即地址解析协议 ARP。该表可以通过查询子网中主机来更新。 
- 
子网掩码(subnet mask)是一个32比特数,与IP地址表示方法一样。如果主机IP与子网掩码按位与运算得到的结果相同,那么这些主机在同一个子网中。比如子网掩码是255.255.255.0,那么18.19.100.1与18.19.100.2都是这个子网的有效IP。子网中有两个地址是保留的,不被其它主机使用。一个是网络地址,任意IP与子网掩码按位与的结果。另一个是广播地址,由子网掩码按位非的结果与网路地址按位或运算得到。 

- 
子网定义后,IPV4提供了一种在不同网络的主机间传输数据包的方法,实现的关键是每台主机IP模块中的路由表。对于每个可达的目标子网,路由表包含了发往目标地址的路由信息。 
- 
为了把数据发往其它网络,需要从互联网提供商 ISP 获得一个有效 IP和网关。假设得到一个IP 18.181.0.29 和网关 18.181.0.1,那么必须在主机上安装一个额外网卡,使用这个分配的IP地址配置好。 
- 
有两个特殊的IP地址。一个是 127.0.0.1 称位 回路地址 或者 本地地址。如果要求IP模块发往本地地址,则不会发送到任何地方,只会发送到下一层处理。第二个是 255.255.255.255,称为受限的 广播地址。数据包会被发送到相同链路层网络的所有主机,但不被路由器发送。实现方法是将数据包打包成链路层帧,并发送到广播MAC地址FF:FF:FF:FF:FF:FF。 
- 
MTU 是1500字节,但IPV4包的最大传输单元是65535字节。如果IP模块要传输的IP数据包比链路层的MTU大,那么需要被分片成一些数据包为MTU的片段数据。IP分片数据包与普通IP数据包类似,只需在头部设置一些值,即使用片标识符、片标记、片偏移这三个域。当IP模块将IP数据包分割成一组片段时,为每一个片段创建一个新的IP数据包,并设置这些域值。根据片标记、片偏移,以及总长度,这些可能乱序到达的分片数据包可以在目标主机上重建原始数据包。 
- 
分片可能导致低效,MTU 1500 字节必须包括20字节的IP头、IP数据和任何协议,为了避免分片,最好将IP数据包限制在1300字节以内。 
- 
IPV4 只允许40亿个不同的IP地址,所以 IPV6 解决了这个问题。IPV6 的地址长度是 128 比特,写成由冒号分隔的8组数,每一组是4个16进制数。另一个改进是,IPV6 不再支持路由层面的数据包分片技术。所以删除了IP头部所有与分片技术相关的域,节省了每个数据包的带宽。如果IPV6数据包到达路由器,发现对于链路层来说太大,那么直接丢弃并通知发送方太大,由发送发决定使用小一些的数据包发送。 

传输层
网络层的任务是实现远程网络上两台主机之间的通信,而传输层的任务是实现这些主机上单独进程之间的通信。一台主机上有很多进程,只知道主机A给主机B发了数据是不够的。传输层引入了 端口 (port) 的概念,一个16比特的无符号数,表示一台主机的通信端点。如果将IP地址比作一个街道地址,那么端口就好像这栋楼的门牌地址。一个进程可以看作从一个或多个房间收邮件的租户。进程会绑定一个特定的端口,表示它想获得所有发送到这个端口的内容。为了避免进程争夺端口,有互联网机构负责分端口注册,每一个传输层协议只能注册一个端口号。端口号 1024—49151称为用户端口或注册端口,0到1023为系统预留端口。
- 用户数据报协议 (User datagram protocol, UDP) 是一个轻量级的协议,封装数据并将其从一台主机的端口发往另一台主机的端口。UDP数据报包含一个8字节的报头,后面跟着数据。

UDP每个数据报都是一个独立的实体,不提供阻塞网络的流量限制服务,不保证数据顺序和准确到达。
- 传输控制协议(transmission control protocol, TCP)是在两台主机之间创建持久性的连接,提供可靠数据流传输,保证数据按顺序抵达。所以,它需要比 UDP 更大的头部数据和用于跟踪连接中每台主机的连接状态数据。接收者确认收到的数据,发送者重新发送没有收到确认消息的数据。

序列号,一个递增的数字。报文段的序列号是本报文段所发送的数据的第一个字节的序号。 确认号,包含发送方期望收到的下一个字节的序列号。 接收窗口,表示对于传入的数据,剩余缓冲空间的最大容量,对流量控制很有用。
- 
超时重传:源主机给目标主机发送一个唯一标识的数据包,等待来自目标主机的确认响应数据包,如果在一定时间内没有收到确认,则重新发送这个数据包。为了重发报文段,TCP 模块必须存储发送出去的每一个字节,直到这个字节已经被接收方确认收到,即收到报文段的确认后,才能把报文段从缓冲区清除。 
- 
三次握手:连接的初始化过程。主机A发送报文段发起连接,包含SYN标志和一个随机初始序列号。如果主机B愿意,它将响应一个包含SYN和ACK的数据包,并将确认号设置为A的序列号加1,意识是希望主机A发来的下一个报文段的序列号比前一个增加1。同时,主机B也随机选一个序列号。即各自有自己的独立序列号。当主机A收到这个报文段,需要确认主机B的初始序列号,所以发送一个包含ACK标志的报文段。如果A收不到确认,就会重新发送报文段。 

- 
流量控制:为了提高传输速度,TCP连接允许一次有多个未被确认的报文段同时传输。但是需要限制这个数量,因为目标主机的缓冲区是有固定大小的。TCP头部包含一个接收窗口域,来指明数据发送方有多少可用的接收缓冲区。假如有一台快速传输的主机A和较慢的B,流量控制以这种方式继续,主机B总是提醒主机A它可以接收的数据量,比如还剩多少字节可以发,主机A不能发送比主机B可以缓存的数据量更多的数据。TCP 允许延迟确认,即收到报文段后不用马上响应确认。 
- 
拥塞控制:流量控制帮助较慢的主机不被数据淹没,但是没法阻止较慢的网络和路由器不被淹没。网络上的交通就像高速路容易堵塞,TCP 实现拥塞控制相当于交通灯。为了降低拥塞,TCP模块主动限制网络中传输的未被确认的数据量,不是根据窗口大小,而是根据已经确认的和丢弃的数据报的数量计算限制本身。这样,如果发生堵塞,出现了大量丢弃数据报,那么TCP发送方就减小发送速度,如果丢弃报文段减少就增加速度。游戏使用TCP协议时,纳格算法虽然减少了带宽使用,但是明显增加了延迟,所以大部分TCP实现提供一个选项来禁用这个拥塞控制。 
- 
断开连接:关闭TCP连接需要分别来自两端的终止请求和确认。主机没有要发送的数据时,发送一个FIN数据报,不再接收新数据,已发送的数据仍然可能重传。另一台主机可以接收数据,当它没有要发送的数据时,也会发送一个FIN数据报。 
应用层
最顶层,我们代码存在的地方。
- 
DHCP:动态主机配置协议(dynamic host configuration protocol)允许主机在接入网络时请求自动配置信息来解决IP地址管理问题,通过发送UDP广播报文的方式。 
- 
DNS:域名系统(domain name system)协议能够将域名和子域名翻译为IP地址。 
- 
NAT:为了保证公开路由地址的唯一性,ICANN机构给大型机构分配独立的IP地址块,如特大企业、大学、ISP。IP地址不够用了,但是可以将整个子网的主机通过一个共享的公开IP地址连接到互联网。网络地址转换(network address translation,NAT)可以实现这个功能。当游戏机的数据包到达路由器,NAT模块同时将源IP和源端口记录在NAT表中,然后随机选一个没有被使用的端口号用于表示源地址和源端口的组合,把这个端口号也记录下来。然后使用路由器自己的IP地址和新选取的端口号重写数据包。重写的数据包到达服务器,然后路由器收到响应数据包,NAT模块使用这个数据包的端口号查询之前记录的NAT表,得到正确的IP和端口号,并将响应数据转发到对应的局域网主机。 
- 
NAT穿越:UDP对NAT的简单穿越方式(simple traversal of UDP through NAT,STUN)。 
套接字
Socket 提供了进程与TCP/IP模型各个层之间通信的标准方法。TCPSocket 包含 TCP 特有的方法:Send、Receive、Connect、Listen、Accept、Bind等。
阻塞和非阻塞I/O
从socket接收数据是典型的阻塞操作。如果没有可以接收的数据,线程被阻塞直到有数据到达。实际应用不可能在主线程阻塞操作,有3种方法可以解决这个问题:多线程、非阻塞I/O、select函数。
- 多线程:给每个可能阻塞的调用生成一个线程。下图展示了服务端的多线程操作。

这种方式有一个缺点,就是每个客户端都需要一个线程。客户端数量增加时,不能很好的扩展。而且很难管理,因为所有客户端数据在并行的线程中进行,这些数据需要以安全的方式输入模拟。
- 
非阻塞I/O:当socket处于非阻塞模式,调用任何阻塞函数都是安全的,因为如果它不能在没有阻塞的情况下完成,它会立刻返回。游戏可以每帧检查是否有准备好的待接收数据,如果有会处理第一个挂起的数据报,没有就进行其它游戏处理。限制每帧读取的数据报数量也很重要。 
- 
Select:上面那种轮询方式遇到大量socket时效率较低,作为替代,socket库提供了同时检查多个socket的方式,只要其中有一个socket准备好了就开始执行。 
- 
Epoll:对Select进一步改进,去掉了轮询,改为通知机制 
- 
IOCP:类似linux系统上的Epoll机制,IO完成端口,数据处理完了就进行通知 
对象序列化
序列化是一种将对象从内存中的随机访问格式转换为比特流格式的行为。这些比特流可以在硬盘上存储,或者通过网络传输,之后再恢复为原始格式。
流指的是一种数据结构,封装了一组有序的数据元素,并允许用户对其进行数据读写。输出流作为用户数据的输出槽,允许用户顺序插入元素,但不能从中读取数据;输入流作为数据源,允许用户顺序提取数据,但不提供插入数据的功能。通常情况下,一个流是其他数据结构或计算资源的接口。例如,文件输出流可以封装一个已经打开准备写的文件,提供顺序存储不同类型数据到磁盘的方法。网络流可以封装一个socket,提供send()和recv()函数的封装,专门用于与用户相关的特定数据类型。内存流(memory stream)封装了内存的缓冲区,通常动态分配在堆上。
流解决了序列化的第一个问题,它提供了一种简单方法来创建缓冲区,使用源对象中各个字段的值来填充缓冲区,给远程主机发送这个缓冲区,顺序提取数据,将它们插入到目标对象的合适字段。
字节在一个平台上的存储顺序被称作这个平台的字节序,可以是小端字节序(little-endian)或大端字节序(big-endian)。小端字节序存储多字节数字,是将低序字节存储在起始地址(低位编址)。例如,值为 0x12345678 的整数存储在地址为 0x01000000 的内存中,存储方式如图。

流数据压缩:稀疏数组压缩、浮点数转为定点数、几何压缩。
定点数是一个数,看起来像整数,但实际上会将其进行缩放和加减来表示一个浮点数。
几何压缩:四元数是一种数据结构,包括四个浮点数,用于表示三维空间的旋转。四元数是归一化的,每个分量都在-1到1之间,所有分量的平方和是1。因为平方和是固定的,所以序列化四个分量只需要序列化其中三个,同时使用一个比特表示第四个分量的符号。同时,取决于精度,浮点数可以转换为定点数。
对象复制
从一台主机向另一台主机传输对象状态的行为称为复制。
远程过程调用(remote procedure call,RPC):是一台主机可以在另一台或多台远程主机上执行程序的动作。
远程方法调用(remote method invocation,RMI):主机在一个特定对象上调用一个方法。
网络拓扑
网络拓扑决定了网络中的计算机是如何连接的。
客户端-服务器:一个游戏实例为权威服务器,其他所有游戏实例为客户端。
对等网络:每个单独的参与者都与其他所有的参与者连接。意味着客户端之间有大量数据来回传输。对等网络游戏中,权威的概念更加模糊。常见的做法是,每个对等体共享所有动作,每个对等体都模拟这些动作的执行。也被称作输入共享模型。
延迟、抖动、可靠性
非网络延迟
- 
输入采样延迟:从用户按下按钮到游戏检测到按钮被按下之间的时间可以很长。所以,如同安卓的触控采样率提升,游戏可以把输入检测的频率提高一点。 
- 
渲染流水线延迟:GPU给用户显示渲染图像可能滞后于CPU请求,可能有一帧的延迟。 
- 
多线程渲染流水线延迟:多线程游戏将更多的延迟引入到了渲染流水线中。 
- 
垂直同步(Vsync):为了避免画面撕裂,通常的做法是仅仅在显示器的垂直消隐间隙改变显示的图像。这样显示器就不会同时显示这一帧的部分图像和下一帧的部分图像。这意味着GPU的更新图像调用必须等到显示器的垂直消隐间隙,通常为1/60秒一次。如果游戏的一帧只需要16毫秒,那么刚好没问题。但是即使一帧的时间延长一毫秒,那么在显卡准备更新图像时也不能完成渲染。这种情况下,更新显示的命令将延迟到下一个间隔执行,等待额外的15毫秒才行,用户将感到一帧的延迟。一些新的显示器,被称为G-SYNC,具有可变的刷新率,根据帧率调解,避免垂直同步带来的延迟。 
- 
显示延迟:大部分HDTV和LCD显示器在真正显示图像之前,都会在一定程度上处理输入,比如一些图像效果,缩放,降噪,过滤等。这些都会增加延迟。 
- 
像素响应时间:LCD显示还有一个问题是像素亮度改变需要时间,可能要几毫秒。 
网络延迟
往返时间(round trip time, RTT)。指的是数据包从一台主机传输到另一台主机的时间,加上响应数据包返回的时间。
- 处理延迟:路由器检查源地址和确定合适路由的时间,也包括NAT和加密所用的时间
- 传输延迟:向物理介质写比特流所花费的时间
- 排队延迟:如果路由器处理的慢则数据包会进入队列中,在队列中小号的时间
- 传播延迟:在传播过程中花费的时间。理想情况,数据包要穿越美国至少需要12毫秒。
通过发送少量大的数据包来替代许多小的数据包可以降低总的排队延迟
抖动
延迟会随着时间推移而变化,导致RTT与期望值有偏差,这个偏差称为抖动(jitter)。抖动会影响RTT抑制算法,还会导致数据包乱序到达。减少抖动的办法:发送尽可能少的数据包来保持低流量;将服务器布置在玩家附近来降低拥堵;把客户端复杂操作合理分散在多个帧中,防止由帧率导致的抖动。
数据包丢失
TCP 还是 UDP
TCP 的优点:保证数据按序到达,提供拥塞控制功能,可以限流。 TCP 的缺点:低优先级的数据的丢失干扰高优先级数据的接收;两个单独的可靠数据流相互干扰,不同的也要有序导致的;过时游戏状态的重传。
UDP 没有提供可靠性和流量控制,但是可以自定义可靠系统。

模拟真实世界
可以设计一个测试模块,使用随机数来决定丢弃数据包还是将其传给应用程序;随机延迟数据的到达。
改进的延迟处理
CS架构中服务器是唯一运行最重要模拟的主机,意味着玩家动作到观察到动作总有一些延迟

上面的例子中,在按下跳跃按钮之后的100毫秒,玩家A才能看到跳跃动作的结果。结论:运行在服务器上的真实模拟通常比远程玩家感觉到的真实模拟早半个往返时间。换句话说,玩家感知到的状态滞后于服务器的状态。这些游戏中的客户端被称为沉默的终端,因为它们并不需要对模拟有任何了解,唯一的就是发送输入,接收结果状态,并显示给用户。因为整个系统的状态总是一致和正确的,所以这种网络方法称为保守算法,以延迟为代价。
除了延迟,单纯保守算法还有另一个问题。以上面的跳跃为例,受限于服务器发送状态的频率,比如每秒15次,客户端也只能得到15帧的体验。还有第三个问题,这种延迟在一些射击游戏中会导致很难瞄准。下面是一些降低延迟的方法。
客户端插值
使用客户端插值时,客户端不是自动将对象移动到服务端发来的位置。而是每当客户端收到一个对象的新状态时,它使用被称为本地感知过滤器的方法根据时间平滑地插值到这个状态。为了保证客户端状态平滑地变化,插值不应该停止。即每当客户端完成插值到一个状态时,它都已经接收到了下一个状态,并再一次启动插值过程。
客户端插值仍然是一个保守算法,不会得到一个离谱的状态。但是不能让客户端更接近服务器实际发生的状态。
客户端预测
为了推测当前的状态,客户端必须能运行与服务器相同的模拟算法。当客户端收到一个状态更新,它知道该更新是 1/2 RTT 之前的。为了追上服务器的状态,客户端只需运行额外的 1/2 RTT 的模拟。
客户端必须先粗略估计 RTT。客户端给服务器发送一个包含基于客户端本地时钟的时间戳数据包,服务器收到时复制时间戳到新的数据包,并发回给客户端。客户端收到时,根据自己的时钟,从当前的时间中减去旧的时间戳。这样就得到了客户端首次发送数据包到收到响应之间的确切时间。
航位推测法
航位推测法(dead reckoning)是基于实体继续做当前正在做的事情这一假设,进行实体行为预测的过程。如果是一个奔跑的玩家,意味着假设玩家会保持相同方向奔跑。只要玩家不断的做正在做的事,航位推测就能保证准确的模拟。但是当玩家采取了意外行动,客户端模拟就与真实不同,必须被纠正,这种模拟被称为 乐观算法。
本地预测与服务器发来的状态不一致时,有三种方式来弥补:
- 即时状态更新:只需立即更新到最新状态。玩家位置可能发生跳跃,但这样还是比错误的状态好。记住即使是及时更新,来自服务器的状态仍然是滞后1/2 RTT,所以客户端应使用航位推测来模拟额外的1/2 RTT。
- 插值:可以在一定数量的帧内平滑的插值到新的状态。一种流行的方法是使用三次样条插值创建路径,以实现位置和速度同时平滑地从预测状态过度到正确状态。
- 二阶状态调整:如果一个静止对象突然加速,即使插值也可能发生抖动。为了更精细地处理,可以调整二阶参数,例如加速度,非常平缓地对模拟进行同步修正。快节奏的游戏通常为小错误使用插值,为大错误使用瞬间移动。慢节奏的游戏,可能用二阶状态调整处理除了最大错误之外的错误。
客户端移动预测和重放
航位推测不能为本地玩家隐藏延迟,只能尽量缓解你看到的别的玩家的延迟,本地玩家就是你。一个好的方法是,当客户端A收到来自服务器的玩家A的状态,客户端A可以使用玩家A的输入重新模拟从服务器计算该传入状态起玩家A发起的所有状态改变。客户端不是使用航位推测模拟 1/2 RTT,而是使用玩家A的精确输入来模拟。每当输入状态到达本地玩家,客户端可以指出在计算该状态时服务器还没收到哪些移动,然后本地应用这些移动。为了让游戏支持移动重放,客户端要做的是在移动列表中保存这些移动,直到服务器将它们用于状态模拟。
通过技巧和优化隐藏延迟
客户端可以在本地执行适当的模拟和特效给玩家的任何输入提供及时反馈,同时等待服务器更新真实模拟。如果有一个发射技能,并不意味者客户端要产生抛射物,但可以开始播放施法动画和声音。如果一切顺利,在施法过程中客户端就会收到产生火球的数据包,感觉到没有什么延迟。如果发生问题,比如服务器发现玩家被沉默,那么这个延迟优化就失去了意义,但这是一种罕见的情况,可以容忍。
服务器端回退
有一种常见的游戏动作是客户端预测不能很好处理的:长距离的即时射击。玩家扣动扳机,希望一次完美的命中。但是,由于航位推测的不准确性,客户端上的完美射击可能在服务端上就不准了。这个问题有一个解决方案,类似反恐精英的射击体验。
它的核心是,当瞄准和开火时,让服务器状态回退到玩家感受到的那个状态。这样,玩家感觉喵的很准,那么就能命中。为了实现这个技巧,游戏需要在客户端预测的基础上做一些修改:
- 远程玩家使用客户端插值,而不是航位推测。客户端插值引入了额外的延迟,但是鉴于移动重放和服务器回退算法,它不会被玩家明显感觉到。
- 使用本地客户端移动预测和移动重放。消除本地玩家感到的延迟。
- 发送给服务器的每个移动数据包中保存客户端视角。客户端在每个发送的数据包中记录客户端当前插值的两个帧的ID,以及插值进度百分比。这给服务器提供客户端当时感知的世界的精确指示。
- 在服务器端,存储每个相关对象最近几帧的位置。当客户端数据包中包含射击时,查找在射击时刻用于插值的两帧。使用数据包中的插值进度百分比将所有相关对象回退到客户端扣动扳机的那一刻。然后从客户端的位置采用光线投射法来确定是否击中。
但是,仍然有些不足。因为服务器回退的时间是根据服务器和客户端之间的延迟决定的,对于被击中的玩家会有不好的体验。玩家A可能以为自己躲开了B,但是如果B的延迟大,他看到A还没有躲开。如果B瞄准开火,服务器判断为命中的话,通知A被击中,那么就有问题。这是一个要权衡的问题。
可扩展性
当网络游戏增大时,减少发送给任何一个客户端的数据量是很重要的。可以对一个特定的客户端,减少其范围内的对象总数。例如,距离客户端太远的对象被认为在范围外;或者将大世界划分为很多静态区域,只有同一个区域内的玩家彼此相关。还有一种做法是,同一个静态区域内控制玩家总数,超过数目是把玩家放到另外一个分线中。
安全性
数据包嗅探
数据包嗅探(packet sniffing)表示以非正常网络操作目的读取数据包数据。中间人攻击。使用公钥加密算法保护敏感信息,各种登录账号信息,以及交易信息。
当A和B首次握手成功,它们交换密钥。接着,A给B发送一条消息时,将使用B的公钥加密该信息。这条信息只有使用B的私钥才能解密。这意味者A发送的信息只有B能看到,同时B发送的也只有A能看到。这就是公钥加密算法的本质。

在RSA系统中,公钥基于非常大的半素数,即两个素数的乘积。接着,私钥基于该半素数的素数因数分解。该系统可行的原因是对于整数分解没有多项式时间的算法,即使超级计算机也不能暴力破解一个1024位或者2048位的两个大素数之积。所以可以直接用RSA的一些开源实现来加密数据,比如OpenSSL。
输入验证
输入验证是努力保证没有玩家执行的动作是无效的。它的实现归结为简单的前提,即游戏不应该盲目地执行来自网络数据包中的操作,应该先验证数据有效性。例如服务端收到玩家A的开火信息,应该先确认玩家A有武器,有子弹,并且不在冷却中。检测到违规操作时可能需要踢掉玩家,但也要考虑数据偶然性。
客户端验证服务器数据是很困难的。解决办法是不让玩家主持游戏,使用权威服务器。
软件作弊检测
游戏进程主动检测游戏的完整性。作弊软件可能嵌入到游戏进程中,有些会重写游戏进程的内存,还有一些是用于自动化的第三方程序,甚至一些会修改游戏使用的文件。
以使用锁步对等网络的RTS游戏为例,战争迷雾只让玩家看到附近的地图,但是在对等网络模型中,每个对等体模拟整个游戏状态,那么作弊软件可以去掉迷雾。这种作弊通常称作地图黑客。
另一种流行的作弊是机器人外挂(bot),扮演成玩家游戏,或者以某种方式辅助玩家。比如自动进行游戏,以升级或者赚钱。
- 维尔福反作弊系统(Value Anti-Cheat, VAC),一个软件作弊检测平台,适用于 Steam 游戏。被禁止的玩家会被服务器拒绝。
- 典狱长反作弊系统(Warden)。暴雪公司的检测程序,会扫描计算机的内存来检测已知的作弊程序,作弊玩家将在将来某个时间被禁止游戏。
保护服务器
- 分布式拒绝服务攻击(DDos)的目的是让服务器不能成功地完成请求,最终导致服务器不可用。原因是太多的输入数据让服务器的网络连接饱和了,或者耗尽了服务器的处理能力。如果使用云托管方案,可以交给云提供商。
- 坏数据。考虑恶意用户给服务器发送错误的数据,试图让服务器崩溃。更有甚者,试图通过数据包缓冲区溢出让服务器执行恶意代码。最好的办法是使用称为模糊测试的一种自动测试方法,给服务器发送大量的非结构化数据,去发现正常测试无法发现的错误。
- 时序攻击。任何将期望的字节签名或哈希与收到的签名比较的代码都容易受到时序攻击。这种攻击中,通过分析特定哈希算法或者加密系统已经拒绝无效数据所花费的时间,可以学习到其实现的信息。假设比较两个包含8个32位整数的数组,确定是否相等,下面的代码中的提前 return false使得不正确的值返回的更快,通过反复试验就能得到正确的证书。
|  |  | 
解决方法是重写 Compare 函数,不管如何都执行相同的时间,避免时序攻击。
- 入侵。攻击者试图窃取用户数据,或者毁掉数据库。所以要保持软件更新,避免漏洞被利用。所有可以访问互联网并能访问服务器的电脑都应该保持软件更新。限制电脑访问服务器的方式,进行双因素身份验证。万一被攻破,还要保证用户敏感数据的安全,密码不能保存为明文。不要使用简单的哈希算法SHA-256、MD5来加密密码,可以使用密码哈希算法,比如河豚加密算法。所有关键操作进行日志记录,方便审计。数据还要备份到某个地方,这样即使最坏的情况,也能进行一定的恢复。
文章作者 huijian142857
上次更新 2020-11-30