笃行
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
  • 引擎骨架

    • UE 引擎(客户端—服务器交互)
    • 引擎架构与一帧的时序
    • UObject / GC / 反射
    • Gameplay Framework 与端存在性矩阵
  • 复制栈底层

    • NetDriver / Channel / Bunch
    • 属性复制
    • RPC
  • 带宽与规模

    • Relevancy 与带宽预算
    • UE5 Iris 复制系统
  • 局内同步

    • 移动预测与回滚
    • 延迟补偿
    • 网络物理同步
  • 系统与运维

    • GAS 网络模型
    • 专用服务器 · Session · Travel
    • 服务器权威与反作弊

NetDriver / Channel / Bunch

这是 UE 复制栈的「地板」。属性复制、RPC、Relevancy 全都建在这层之上。搞懂 NetDriver → NetConnection → Channel → Bunch → Packet 这条链,就能解释所有上层现象:为什么复制会延迟、为什么可靠 RPC 洪泛会断线、为什么低优先级 Actor 会「饿死」发不出去。


一句话结论

UE 在 UDP 之上自建了一套可靠传输:Bunch 是逻辑消息、Packet 是物理包,靠序列号+ACK 做可靠,靠带宽配额做流控。

场景问题

打个比方:这层像一家快递公司自建的物流网。UNetDriver 是总调度室,管全国的线路;每个客户端是一条专线(UNetConnection);专线上按业务分不同传送带(Channel)——控制指令走一条、每个 Actor 各占一条、语音单独一条,互不打扰;你要发的每件东西是一个包裹(Bunch);而真正上路的是货车(Packet)——一辆车会把同方向的多个小包裹拼车装走(bunch merging),包裹太大装不进一辆车就拆箱分几车运、到站再拼回来(partial bunch)。

车都编了号,收货方每收到一车就回执(ACK),没回执的重发——这就是可靠性。但每条专线每天限运多少吨是有配额的(带宽限额),货多了就得排优先级,优先级低的包裹可能今天一直排不上车(低优先级 Actor 饿死)。而声明要「必达」的包裹(Reliable)发不出去时不能丢,只能堆在仓库里——仓库堆满(可靠队列溢出)快递公司直接终止合作(断开连接)。

面试官追问:「UE 底层用 TCP 还是 UDP?可靠性怎么保证的?」——UE 默认用 UDP,然后在应用层自己实现了一套可靠 ARQ(选择性重传 + 序列号 + ACK)。这和 KCP 解决的是同一个命题:「UDP 上怎么做可靠有序」。区别是 UE 把可靠性做进了引擎、按 Bunch 粒度控制哪些要可靠哪些不要。要讲清这套,得从对象链拆起。

实现方案

1. 对象链:从驱动到字节

层类一句话
驱动UNetDriver管所有连接、驱动每帧收发(服务器一个,GetNetDriver())
连接UNetConnection一个客户端一个,持有带宽配额、序列号状态
通道UChannel逻辑分流:UControlChannel / UActorChannel / UVoiceChannel
消息FOutBunch/FInBunch一条逻辑消息(一次属性更新或一个 RPC)
物理包Packet一个 UDP 数据报,可打包多个 Bunch

每个复制 Actor 在每个相关连接上有一个 Actor Channel——这也是为什么 Actor 数 × 连接数会成为开销瓶颈,见 Relevancy 与带宽预算。

2. Bunch 与 Packet 的关系

  • Bunch 是逻辑单位:一次属性同步、一个 RPC 调用序列化成一个 Bunch。
  • Packet 是物理单位:一个 UDP 报文。为省包头开销,UE 把同一帧内多个小 Bunch 合并(bunch merging) 进一个 Packet。
  • 超 MTU 分片:单个 Bunch 太大(超过一个 Packet 容量)时会被拆成多个 partial bunch,用标志位标记:bPartialInitial(第一片)、bPartial(中间片)、bPartialFinal(末片)。接收端按序拼回完整 Bunch。
  • MaxPacket:单包上限,默认约 1024 字节量级(UNetConnection::MaxPacket),贴近网络 MTU 避免 IP 层分片。

一句话:Bunch 讲「我要发什么」,Packet 讲「怎么塞进网线」。

3. 可靠性:序列号 + ACK

UE 有两层序列号:

  • Packet 级:每个 Packet 一个递增序列号。接收端回 ACK/NAK,发送端据此判断丢包。
  • Bunch 级:可靠 Bunch 带 bReliable + ChSequence(通道内序号)。

可靠与不可靠:

类型丢了怎么办用途
可靠 Bunch(bReliable)进重发队列,收到 ACK 前反复重发,保证到达且有序初始复制、可靠 RPC、通道打开关闭
不可靠 Bunch丢了就丢,不重发高频属性(位置)、不可靠 RPC

Reliable buffer overflow 断线

可靠 Bunch 有重发队列上限(RELIABLE_BUFFER,默认 256 量级)。如果可靠消息产生速度长期快于对端 ACK 速度(比如疯狂发可靠 RPC),队列塞满 → 连接直接断开。这是「可靠 RPC 洪泛会踢人」的底层原因,详见 RPC。

4. 握手与安全

新连接不是直接就能发游戏数据的,要先过 Control Channel 的握手:

客户端                         服务器
  │── (UDP包) ──────────────────▶│  PacketHandler 层:StatelessConnectHandler
  │                              │  challenge-response(防伪造源 IP / SYN 洪泛)
  │◀───────── challenge ─────────│
  │── response ─────────────────▶│  验证通过才分配 UNetConnection
  │── NMT_Hello (版本号) ────────▶│  校验引擎/网络版本
  │── NMT_Login (URL/OnlineID) ──▶│  → GameMode::PreLogin / Login
  │◀───────── NMT_Welcome ───────│  下发首个地图
  │── NMT_Join ─────────────────▶│  → PostLogin,开始复制 Actor
  • PacketHandler / StatelessConnectHandler:连接建立前的一层,用无状态 challenge-response 确认对端 IP 真实可回包,防伪造源 IP 的放大攻击(服务器不为未验证的包分配状态)。
  • Control Channel 消息:NMT_Hello(版本协商)→ NMT_Login(带登录 URL 和在线 ID)→ NMT_Welcome → NMT_Join。每一步失败都有对应断线原因。完整连接时序见 专用服务器 · Session · Travel。
  • DDoSDetection:UNetDriver 内置,按包速率阈值检测异常连接并限流/丢弃,防单连接刷包打爆服务器。

5. 饱和(Saturation)与饥饿

每个 UNetConnection 有每秒带宽配额(MaxClientRate,默认约 15000 B/s 量级,可协商)。每帧 TickFlush 时:

  1. 引擎按 NetPriority 给相关 Actor 排序,优先级高的先发。
  2. 逐个 Actor 打包 Bunch,累加已用字节。
  3. 配额用尽 → IsNetReady() 返回 false → 本帧停止发送,剩下的 Actor 等下一帧。

饥饿现象:如果高优先级 Actor 持续占满配额,低优先级 Actor 可能长期轮不到,表现为「远处的东西半天不更新」。缓解靠调 NetPriority、NetUpdateFrequency、Dormancy 减少参与竞争的 Actor 数,详见 Relevancy 与带宽预算。

这本质上是流控——和 TCP 的拥塞/流控、KCP 的带宽换延迟 是同层问题,只是 UE 把它做成了「按 Actor 优先级分配固定配额」。

6. 一句话记住这几组容易混的概念

一组差别一句话类比
Bunch vs PacketBunch 是「发什么」的逻辑消息,Packet 是「怎么上路」的 UDP 报文;一辆车可拼多个包裹,大包裹也可拆几辆车包裹 vs 货车
Reliable vs Unreliable可靠的丢了必重发、堵住会堆仓库直到断线;不可靠的丢了就算,绝不拖累别人挂号信 vs 平信
饱和 vs 饥饿饱和是「这条连接本帧配额用完了」;饥饿是「某个 Actor 因为优先级低长期抢不到配额」车厢装满 vs 你的货永远排在队尾
UE 可靠层 vs KCP同一个命题(UDP 上做可靠),UE 按 Bunch 粒度逐条选要不要可靠;KCP 是整条流统一可靠一个包裹一个包裹挑寄送方式 vs 整批统一挂号

一条主线串起来:包裹(Bunch)按可靠性分类装车(Packet),车有载重上限(带宽配额),载重不够时按优先级排队,排不上的下一趟——必达包裹排不上就只能堆着,堆爆了断线。

为什么这么做

选 UDP + 自建可靠层:游戏要的是「部分数据要可靠有序(登录、扣血 RPC)、部分数据丢了无所谓且不能等(每帧位置)」。TCP 是「全部可靠有序」,一个位置包丢了会队头阻塞后面所有数据——这对实时游戏是灾难。UE 在 UDP 上按 Bunch 粒度选择可靠性,正是为了让不可靠的高频数据不被可靠数据拖累。这与 KCP/QUIC 的动机完全一致。

Bunch/Packet 分离:逻辑消息(Bunch)和物理传输(Packet)解耦,才能做 bunch merging(省包头)和分片(发大消息),同时让上层只管「发什么」不管「怎么塞进 MTU」。

带宽配额 + 优先级:带宽是硬约束(尤其服务器上行 = 每客户端下行 × 客户端数),必须有流控。按优先级分配让「重要的先到」,是有限带宽下的最优策略。

为什么别的选择不行

  • 直接用 TCP:队头阻塞——一个丢包阻塞所有后续数据,实时性崩溃;且无法对不同数据选择不同可靠性。详见 KCP 与 QUIC。
  • 全部 Bunch 都可靠:高频位置包也重发,带宽爆炸且延迟不可控(旧位置重发到了也没用)。
  • 不做带宽配额:一个热闹场景瞬间产生海量更新,直接打爆客户端下行,还不如按优先级丢弃低价值更新。
  • 不做握手 challenge:伪造源 IP 就能让服务器为大量假连接分配状态,被放大攻击打垮。

沉淀结论

UE = UDP + 自建 ARQ。Bunch 逻辑消息 / Packet 物理包,序列号+ACK 做可靠,bReliable 按需选可靠性。可靠队列满会断线(RPC 洪泛坑)。带宽配额 + 优先级做流控,配额耗尽即饱和、低优先级会饥饿。

记忆口诀

对象链:NetDriver → NetConnection(每客户端)→ Channel(Control/Actor/Voice)→ Bunch → Packet
Bunch vs Packet:Bunch 逻辑消息 / Packet 物理 UDP 包 / 合并省包头 / 超 MTU 分片 partial
可靠:Packet 序列号 + ACK / Bunch 级 bReliable / 可靠队列满 → 断线
流控:MaxClientRate 配额 / NetPriority 排序 / 配额耗尽饱和 / 低优先级饥饿

内容来源

综合整理。主要参考:Epic 官方文档 Networking Architecture、Data Transfer 、UE 源码 NetConnection.cpp/DataChannel.cpp/NetDriver.cpp/PacketHandler,社区分析 Alex Forsythe《How Unreal's Networking Works》。数字为引擎默认值或业界数量级估算。

相关专题:UDP 可靠传输原理与 KCP/QUIC 对比见 KCP 与 QUIC;TCP 队头阻塞与流控见 TCP 网络;底层 I/O 模型(epoll)见 LVS 与 epoll;建在这层之上的属性同步见 属性复制。

自测:合上资料能说清楚吗?

UE 底层用 TCP 还是 UDP?可靠性怎么保证?

参考答案

默认用 UDP,在应用层自建一套可靠 ARQ:Packet 级序列号 + ACK/NAK 判断丢包,Bunch 级 bReliable 标记哪些消息需要可靠(进重发队列直到收到 ACK),不可靠的丢了就丢。好处是能按数据粒度选择可靠性——登录/扣血用可靠、每帧位置用不可靠,避免 TCP 那种一个丢包阻塞全部的队头阻塞。这和 KCP 是同一命题。

Bunch 和 Packet 是什么关系?

参考答案

Bunch 是逻辑消息(一次属性更新或一个 RPC),Packet 是物理 UDP 报文。为省包头,多个小 Bunch 会合并(merging)进一个 Packet;单个 Bunch 超过 Packet 容量则拆成 partial bunch(bPartialInitial/bPartial/bPartialFinal)分片发送,接收端拼回。MaxPacket 默认约 1024 字节贴近 MTU。

为什么疯狂发可靠 RPC 会导致客户端断线?

参考答案

可靠 Bunch 进重发队列,队列有上限(RELIABLE_BUFFER,256 量级)。若可靠消息产生速度长期快于对端 ACK 速度,队列塞满,连接直接断开(Reliable buffer overflow)。所以高频事件不该用可靠 RPC,更不该用 RPC 传状态。

什么是复制饱和(saturation)和饥饿?

参考答案

每个连接有每秒带宽配额(MaxClientRate)。每帧发包时按 NetPriority 排序逐个 Actor 打包,配额用尽(IsNetReady 为 false)就停止发送、剩余 Actor 等下一帧——这是饱和。若高优先级 Actor 持续占满配额,低优先级 Actor 长期轮不到就是饥饿,表现为远处物体半天不更新。靠调优先级、更新频率、Dormancy 缓解。

最近更新: 2026/9/10 11:38
Next
属性复制