属性同步协议那篇讲的是"一个属性怎么编包",这篇把镜头拉远:整个 UE 网络复制是由哪几层对象搭起来的,各层职责边界在哪。基于 UE5.3 源码(Runtime/Engine/Private/NetDriver.cpp、NetConnection.cpp、DataChannel.cpp)梳理。
三层对象与各自管什么
UNetDriver(每个 UWorld 一个)。 服务器上 UWorld 持有 NetDriver,它是复制的总调度器:ServerReplicateActors 每网络 tick 跑一遍 gather(收集所有连接的相关 actor)、prioritize(按 NetPriority 排序)、replicate(逐连接逐 actor 走 channel 编包)。它还持有 channel definition 表——每种 channel 类型(actor/voice/file)的 class 注册在这里。
UNetConnection(每个客户端一个)。 一个 TCP 或 UDP 会话抽象成 UNetConnection,UNetDriver 的 ClientConnections 数组里一个元素对应一个连进来的玩家。它管的东西都是会话级的:发送缓冲队列、ack 跟踪(OutAckSeq/InSeq)、带宽统计(StatUpdateTime 内的字节数)、饱和检测(IsNetReady 返回 false 时暂停复制,防止写爆发送缓冲)。
UChannel(每个连接里的每个对象一个)。 channel 是"一条对象的状态流":服务器上一个 replicated actor 对某个连接有状态要同步,就开一个 UActorChannel(通过 ActorChannelIndex 索引)。channel 自己维护可靠 bunch 队列(OutReliableBunch)、属性 replicator(FObjectReplicator 挂在 actor channel 上)。channel index 是 bunch 头里的字段,客户端靠它把 bunch 路由回对应的对象。
为什么要分三层?因为变化频率和生命周期完全不同:driver 每 tick 都在转,connection 跟着会话生死(断线就销毁),channel 跟着对象生死(actor 销毁 channel 就 close)。混在一层里,actor 的销毁逻辑会污染会话管理;拆开之后,channel 的 open/close 只是 connection 里的数组增删,driver 完全不用关心。
bunch 与 packet:两个不要混的"包"
网络层的资料里最常被混用的词是 bunch 和 packet,分清这两个单元是读懂日志的前提:
- bunch 是 channel 内的逻辑数据单元,一个 channel 一次属性同步或一次 RPC 就产出一个 bunch。bunch 头里带 channel index、可靠标志(
bReliable)、open/close 标志。 - packet 是物理发送单元。
UNetConnection::SendRawBunch把多个 channel 的 bunch 拼进一个 UDP 报文,凑不满 MTU 就等下一个 tick(ReadyForSend)。一个 packet 丢了,里面所有 channel 的 bunch 一起丢;bunch 超大了走SendLargeBunch分片重组。
这意味着带宽优化的杠杆在两个层面:bunch 层面(属性增量、量化——属性同步那篇的内容)和 packet 层面(连接饱和时谁优先上飞机)。stat net 里 OutPackets 和 OutBunches 两个计数器分开看,bunch 多 packet 少说明拼包效率高,反过来说明 channel 太碎。
channel 的开销从哪来
channel 不是免费抽象。每个 UActorChannel 挂着:自己的可靠 bunch 重传队列、FObjectReplicator(含影子状态和发送历史,属性同步那篇讲过)、以及 actor 指针和 NetGUID 映射项。账要这么算:1000 个 relevant actor × 50 个连接,就是 5 万个 channel 及其影子状态——channel 数量是服务端网络内存的第一大户。所以大世界的第一批优化动作永远是降低 relevant actor 数:NetCullDistanceSquared 砍掉远距离的、dormancy 让静态 actor 的 channel 休眠(下下篇)、ReplicationGraph 缓存相关性结果避免每 tick 重算。
连上这条链的入口命令
net.ListChannels:列出某个连接的所有 channel,看 channel 数量是不是炸了。net.List/stat net:连接层统计,看饱和频率( saturated 次数多说明 NetUpdateFrequency 或内容量超带宽了)。Obj List Class=ActorChannel:直接数 channel 对象数。
这条三层链在代码里的对应关系记一句话就够:ServerReplicateActors 在 driver 层转,拿到 actor 后下钻到 connection 选 channel,进 channel 后才是 ReplicateProperties 那套协议。后面几篇(DORM、Push Model、ReplicationGraph、Iris)各自动的都是这条链上的某一层。