基于KCP的实时游戏通信案例研究

在实时在线游戏中,玩家输入传送到服务器以及返回结果的延迟时间至关重要。TCP虽然保证了可靠性和传输顺序,但在等待丢失数据的同时,后到达的数据的传递也可能会延迟。UDP不保证传输顺序和到达性,因此要处理需要可靠性的消息,就需要有补充机制。

KCP主要是在UDP之上提供重传和顺序控制的协议。本文将探讨KCP减少延迟的原理,并基于游戏通信案例,梳理连接与认证过程以及消息结构。认证流程和应用程序消息格式可能会根据实现而有所不同。

KCP是如何减少延迟的?

KCP通过使用确认接收和重传的ARQ方式实现可靠传输。通过调整重传策略和更新周期,可以减少丢失后的等待时间,但根据设置的不同,带宽和CPU的使用量可能会增加。

实际性能取决于网络状态和设置。

  • 选择性重传: 基于接收确认信息挑选丢失的数据进行重传。TCP在使用SACK时也可以进行选择性重传,因此不能将其视为KCP的独有功能。
  • 重传等待时间调整: 在快速模式下,当丢失重复发生时,减小重传等待时间的增长幅度,从而提前恢复。
  • 快速重传: 发送方通过观察后续数据包的ACK到达情况来推测前面数据包的丢失。如果满足设定的条件,则在超时前进行重传。
  • 短更新周期: 减少更新和传输周期,可以缩短等待ACK和重传数据发出的时间。实际传输时机也会受到应用程序调用周期的影响。

KCP同样按顺序传递数据,因此如果前面的数据丢失,后面的数据仍然会发生等待的现象。其核心不在于消除这种等待,而在于更快地恢复丢失的数据。

连接与认证过程

在与游戏服务器通信之前,需要进行账号认证以及选择要连接的服务器。以下是将通过Web认证获取的Token在游戏服务器中进行验证的简化流程示例。

sequenceDiagram participant Client as 客户端 participant Web as Web认证服务器 participant Game as 游戏服务器 Client->>Web: 查询服务器列表 (HTTPS) Web-->>Client: 返回服务器列表 Client->>Web: 账号认证 (HTTPS) Web-->>Client: 颁发登录Token Note over Client: 选择要连接的服务器 Client->>Game: 连接请求及状态检查 Game-->>Client: 返回服务器状态 Note over Client,Game: 通过认证的密钥交换等准备加密会话 Client->>Game: 通过受保护的通道传递登录Token Game-->>Client: 验证Token并批准连接 Note over Client,Game: 开始收发游戏消息

在该示例中,Web服务器负责认证账号,游戏服务器负责验证Token并启动游戏会话。区分这两个阶段的角色,有助于理解登录之后的通信流程。

KCP本身没有加密或用户认证功能。如果需要安全性,必须配置单独的安全层。Protobuf同样只是序列化数据的格式,仅通过Protobuf表示密钥或Token并不能保证安全传输。

KCP头部与应用程序消息

KCP段由24字节的头部和数据区域组成。下表中从 conv 到 len 的部分是KCP头部,而数据区域内的 PackLen、MsgType、CmdId 等是本案例中涉及的应用程序消息格式。此内部格式并非KCP的通用规范。

表中一格表示1字节。如果应用程序消息很大,可能会被拆分为多个KCP段,因此内部表可以作为展示重组后消息结构的示例来阅读。

KCP段与应用程序消息结构示例
0 1 2 3 4 5 6 7
conv cmd frg wnd
ts sn
una len
Data: 应用程序消息示例
0 1 2 3 4 5 6 7
PackLen 0 MsgType seqNo
rpcId CmdId crc32
Payload

在KCP头部中,conv 是会话标识符,cmd 是命令类型,frg 是分片信息,wnd 是接收窗口大小。ts 表示时间戳,sn 表示段序号,una 表示下一个要接收的序号,len 表示数据区域的长度。

应用程序消息的字段可以按如下方式解析。长度和校验和的计算范围必须确认各自实现的规则。

  • PackLen:表示消息长度的字段。
  • MsgType:消息类型和标志信息。
  • seqNo:应用程序消息的序号。
  • rpcId:连接远程过程调用的请求与响应的标识符。
  • CmdId:表示用于解析Payload的消息类型的标识符。
  • crc32:用于错误检测的校验和。它不能代替密码学认证。
  • Payload:序列化的消息数据。如果应用了加密,则解密后再进行解析。

使用Protobuf解析Payload

本案例中的Payload包含通过Protocol Buffers (Protobuf)序列化的数据。Protobuf是将结构化数据表示为二进制的格式,使用编号代替字段名称。虽然可以简洁地表示游戏消息,但其大小和处理性能取决于数据结构和实现。

首先,在 .proto 文件中定义消息结构,并使用 protoc 生成符合目标语言的代码。发送方使用此代码序列化数据,接收方则使用相同的消息定义进行反序列化。

例如,可以定义一个包含两个字符串的消息,如下所示:

proto
syntax = "proto3";

message HelloWorld {
  string message = 1;
  string name = 2;
}

假设在应用程序中,将 CmdId 与消息类型的对应关系规定如下。该编号不是Protobuf自动分配的值,而是应用程序制定的规则。

text
ServerStatusNotice = 10;
HelloWorld = 11;
GameBanNotice = 12;

将 message 为 Hello World!、name 为 AZA.GG 的消息序列化后,会得到以下字节序列。下面的Payload是加密前的数据。

text
CmdId: 11
Payload: 0a 0c 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 12 06 41 5a 41 2e 47 47

每个字符串前面都有包含字段编号和数据类型的Tag,以及字符串的字节长度。0a 和 12 分别是第1字段和第2字段的Tag,而 0c 和 06 分别表示字符串的长度12和6。

text
Field 1: 标签 0a | 长度 0c (12) | "Hello World!"
Field 2: 标签 12 | 长度 06 (6)  | "AZA.GG"

接收方确认 CmdId 为 11 后,将Payload解析为 HelloWorld。像这样,KCP负责传输和重传,应用程序头部负责区分消息,而Protobuf则负责数据表达。