Channel的创建和销毁
客户端和DS通过Bunch.ChIndex相互映射关联. 这样同一个Actor在每个端都会通过同一个Channel进行处理.
搭建测试用例
首先搭建一个能主动创建和销毁Channel的示例.
1 | // 在DS端主动创建Character |
打开
DS打开流程
在DS端创建的Actor, 在属性同步时, 发现没有对应的Channel, 会进行创建并分配ChannelIndex, 与Actor绑定. 然后就能通过Channel进行通信了.
1 | // DS Channel创建流程. 在SpawnActor后, 当Actor进行属性同步时, 发现缺少对应的Channel, 会进行创建. |
可以看到, DS端Channel主动绑定Actor的:
1 | Channel->SetChannelActor(Actor, ESetChannelActorFlags::None); |
客户端打开流程
在解析DS发来的Bunch时, 根据ChannelIndex在UNetConnection.Channels中直接索引, 如果为空会进行创建:

创建Channel:

然后处理bunch中的数据.
1 | // 客户端创建Channel流程 |
注意:Channel创建后,第一个Bunch不一定是SpawnActor, 第一个bunch是需要该Index的Channel进行处理, 就创建了. 比如第一个Bunch极有可能是ExportGUIDBunch, 带有SpawnActor的Bunch在其后.
思考
客户端Channel打开后会立即绑定Actor吗?
答: 不会, channel的打开纯粹是依赖Bunch的ChannelIndex, 而Channel和Actor的绑定则依赖Bunch中的SpawnActor, 二者极有可能不在同一个bunch中. 例如DS先发来ExportGUIDBunch, 然后才发来属性同步的bunch(里面带有SpawnActor).
如果客户端Channel还没有SpawnActor(与Actor绑定), 就收到了属性同步的消息(在丢包的时候可能发生), 会怎么处理?
答: 直接丢弃, 并报错:

UActorChannel.SpawnAcked和UChannel.OpenAcked
关联关系:
FSendingRepState.PreOpenAckHistory和FSendingRepState.bOpenAckedCalled关联.
当客户端和DS之间Channel没有关联的时候,
会将History放到FSendingRepState.PreOpenAckHistory中.
FSendingRepState.bOpenAckedCalled和UActorChannel.SpawnAcked关联.
只有ActorChannel中的UActorChannel.SpawnAcked被响应了(客户端Actor创建成功),
才会将FSendingRepState.bOpenAckedCalled设置为true.
UActorChannel.SpawnAcked和UChannel.OpenAcked关联.
只有UChannel.OpenAcked(Channel打开成功,
即客户端ActorChannel也打开了),
才会将UActorChannel.SpawnAcked设置为true.
当所有OpenBunch都Ack后, 才会将OpenAcked设置为1.
即客户端收到并处理了全部OpenBunch, 并且DS也收到了客户端处理完成的信息.
此时, Channel关联成功, 开始Channel之间的通信.
如果Open的Bunch丢弃了, 但是后续Replicated的包收到了,
由于此时还没有创建Actor, 会直接丢弃该Bunch, 但是Packet是收到的,
所以DS收到了Ack, 针对这种情况,
使用FSendingRepState.PreOpenAckHistory记录ActorChannel创建Actor之前的所有Ack但是没有处理的Replication.
关闭
DS关闭Channel
关闭Channel
关闭的同时会向客户端发送 CloseBunch. 注意观察, CloseBunch的bClose为true, 并且bReliable为1, 为Reliable的Close Bunch.
1 | // 堆栈: DS Channel Close流程, 注意:这里只是将Channel close掉, 还没有销毁 |

并将Channel设置为Closing状态:

回收Channel
DS向客户端发送close包后, 由于Close包时Reliable, 需要等Ack, 再接收到Ack后, 才能将Channel Close掉.
DS收到CloseBunch的ACK. 然后将Channel Close掉.


Close时候是将Channel放入Pool中, 等待下次复用:

思考
- DS在Channel关闭, 但是还没有回收过程中, 客户端发来消息会怎么处理呢?
根据上文已知DS将Channel关闭后会将UChannel.Closing 设置为true. 在接收到Bunch时候, 会根据当前是否为Closing状态, 处理Bunch, 如果在Closing状态, 又不是CloseBunch则直接丢弃.

- 为什么不直接关闭, 反而要等客户端Ack呢?
如果直接关闭, 客户端可能还未接收到CloseBunch, 还会继续发送消息, 这时候, DS会收到很客户端发来的消息. 这时候, Channel其实已经没了(Close时候会回收Channel), 会直接丢弃. 但是会报错. 但是, 明显直接丢弃就好, 不需要报错, 所以要Ack确认后才关闭.

总结
DS关闭Channel分成两步, 首先将Channel Close掉, 并向客户端发送CloseBunch, 等待接收到CloseBunch的Ack才将Channel彻底清理并放入Pool中等待重复利用.
客户端关闭Channel
收到Close消息主动关闭Channel并回收
1 | // 客户端Channel Close并销毁流程 |

客户端收到CloseBunch之后, 直接关闭Channel, 并将Channel放入池中.
销毁所有SubObj和Actor:


将Channel设置为Closing状态:

放入池中:

在放入Pool时, 需要重置属性:

总结: 客户端会同时将Channel close和销毁掉, 一帧先后执行.
思考
客户端能否主动DestroyActor呢?
不能. 在UWorld.DestroyActor中, 明确指出:只有DS才能Destroy网络Actor.

仔细观察,仿佛还有一丝生机, 如果我传入了bNetForce为true呢?
确实删除了, 但是, DS并不知情, DS和其他端的Actor还完好的存在, 只是删除了本地Actor. 而且该Actor对应的Channel还在, 只是其Actor没了. 这样就会造成:

1 | LogNetTraffic: Error: UActorChannel::ProcessBunch: New actor channel received non-open packet. bOpen: 0, bClose: 0, bReliable: 0, bPartial: 0, bPartialInitial: 0, bPartialFinal: 0, ChName: Actor, ChIndex: 5, Closing: 0, OpenedLocally: 0, OpenAcked: 1, NetGUID: 20 |
所以, 本地(非DS)销毁相当于销毁了个寂寞.
客户端收到后续该Channel的包怎么处理
由于DS先关闭的Channel, 关闭后就不会再发送数据包了, 客户端后关闭Channel, 理论上不会收到该Channel的任何包了. 所以如果再次收到该Channel的包, 直接丢弃就好.

可以看出, 当Channel没创建时, 对Bunch的判断非常严格. 必须是Open并且Close/partial中的一种才可以, 否则直接丢弃.
思考
如果客户端发送RPC的时候DS还没有创建Channel, 该怎么办?
答: 除了默认Channel, 客户端不能自主创建Channel(配置的除外), 只能被动由DS通知创建. 其次一个Actor要想发送RPC, 必须和DS对应Actor建立关联. 所以以上问题根本不存在. 而且RPC必须由主端发送, 模拟端不能发送RPC. 客户端没有Channel, 不可能发送RPC.
创建Channel的Bunch时Reliable吗?
Channel创建(第一次发包)的bunch都为Reliable

CloseBunch是否为Reliable?
是ReliableBunch.
