Channel的创建和销毁

客户端和DS通过Bunch.ChIndex相互映射关联. 这样同一个Actor在每个端都会通过同一个Channel进行处理.

搭建测试用例

首先搭建一个能主动创建和销毁Channel的示例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 在DS端主动创建Character
if (ALSVCharacter==nullptr && CreateCharacterFrameIndex ==0)
{
CreateCharacterFrameIndex =1;
FVector Loc = FVector::ZeroVector;
FRotator Rot = FRotator::ZeroRotator;
FActorSpawnParameters Par;
Par.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
ALSVCharacter = GetWorld()->SpawnActor<AALSVCharacter>(AALSVCharacter::StaticClass(), Loc, Rot, Par);
}

// 在DS端主动销毁Character
if (CreateCharacterFrameIndex>50)
{
CreateCharacterFrameIndex = -1;
ALSVCharacter->Destroy();
ALSVCharacter = nullptr;
}

打开

DS打开流程

在DS端创建的Actor, 在属性同步时, 发现没有对应的Channel, 会进行创建并分配ChannelIndex, 与Actor绑定. 然后就能通过Channel进行通信了.

1
2
3
4
5
6
7
8
9
10
// DS Channel创建流程. 在SpawnActor后, 当Actor进行属性同步时, 发现缺少对应的Channel, 会进行创建.
--UNetDriver.TickFlush
|--UNetDriver.ServerReplicateActors
| |--UNetDriver.ServerReplicateActors_ProcessPrioritizedActors
| | |--UNetConnection.CreateChannelByName
| | | |--GetFreeChannelIndex // 获取Free ChannelIndex
| | | |--UActorChannel.Init
| | | | |--UChannel.Init
| | |--Channel->SetChannelActor(Actor, ESetChannelActorFlags::None);
| | |--UActorChannel::ReplicateActor

可以看到, DS端Channel主动绑定Actor的:

1
Channel->SetChannelActor(Actor, ESetChannelActorFlags::None);

客户端打开流程

在解析DS发来的Bunch时, 根据ChannelIndex在UNetConnection.Channels中直接索引, 如果为空会进行创建:

创建Channel:

然后处理bunch中的数据.

1
2
3
4
5
6
// 客户端创建Channel流程
--UIpNetDriver.TickDispatch
|--UNetConnection.ReceivedRawPacket
| |--UChannel.ReceivedNextBunch()
| | |--UNetConnection.ReceivedPacket
| | | |--UNetConnection.CreateChannelByName

注意: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

关联关系:

image-20240313193348196

FSendingRepState.PreOpenAckHistoryFSendingRepState.bOpenAckedCalled关联. 当客户端和DS之间Channel没有关联的时候, 会将History放到FSendingRepState.PreOpenAckHistory中.

image-20240311221524592

FSendingRepState.bOpenAckedCalledUActorChannel.SpawnAcked关联. 只有ActorChannel中的UActorChannel.SpawnAcked被响应了(客户端Actor创建成功), 才会将FSendingRepState.bOpenAckedCalled设置为true.

image-20240311221451214

UActorChannel.SpawnAckedUChannel.OpenAcked关联. 只有UChannel.OpenAcked(Channel打开成功, 即客户端ActorChannel也打开了), 才会将UActorChannel.SpawnAcked设置为true.

image-20240311221652311

当所有OpenBunch都Ack后, 才会将OpenAcked设置为1. 即客户端收到并处理了全部OpenBunch, 并且DS也收到了客户端处理完成的信息. 此时, Channel关联成功, 开始Channel之间的通信.

image-20240311221849911

如果Open的Bunch丢弃了, 但是后续Replicated的包收到了, 由于此时还没有创建Actor, 会直接丢弃该Bunch, 但是Packet是收到的, 所以DS收到了Ack, 针对这种情况, 使用FSendingRepState.PreOpenAckHistory记录ActorChannel创建Actor之前的所有Ack但是没有处理的Replication.

image-20240311222703913

关闭

DS关闭Channel

关闭Channel

关闭的同时会向客户端发送 CloseBunch. 注意观察, CloseBunch的bClose为true, 并且bReliable为1, 为Reliable的Close Bunch.

1
2
3
4
5
6
// 堆栈: DS Channel Close流程, 注意:这里只是将Channel close掉, 还没有销毁
--AActor.Destroy()
|--UNetDriver.NotifyActorDestroyed()
| |--UActorChannel.Close()
| | |--UChannel.Close()
| | | |--SendCloseBunch

并将Channel设置为Closing状态:

回收Channel

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

DS收到CloseBunch的ACK. 然后将Channel Close掉.

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

思考

  1. DS在Channel关闭, 但是还没有回收过程中, 客户端发来消息会怎么处理呢?

根据上文已知DS将Channel关闭后会将UChannel.Closing 设置为true. 在接收到Bunch时候, 会根据当前是否为Closing状态, 处理Bunch, 如果在Closing状态, 又不是CloseBunch则直接丢弃.

  1. 为什么不直接关闭, 反而要等客户端Ack呢?

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

总结

DS关闭Channel分成两步, 首先将Channel Close掉, 并向客户端发送CloseBunch, 等待接收到CloseBunch的Ack才将Channel彻底清理并放入Pool中等待重复利用.

客户端关闭Channel

收到Close消息主动关闭Channel并回收

1
2
3
4
5
6
7
8
// 客户端Channel Close并销毁流程
--UNetConnection.ReceivedPacket()
|--UChannel.ReceivedRawBunch()
| |--UChannel.ReceivedNextBunch()
| | |--UChannel.ReceivedSequencedBunch()
| | | |--UChannel.ConditionalCleanUp()
| | | | |--UNetDriver.ActorChannelPool.Push(Channel);
| | | | |--UActorChannel.AddedToChannelPool()// 将channel添加到Pool池中.

客户端收到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.

思考