RPC浅析

发送RPC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 发送RPC堆栈
--APlayerController::ClientSetHUD()
|--AActor::ProcessEvent()
| |--UObject.ProcessEvent()
| | |--AActor.CallRemoteFunction()
| | | |--UNetDriver.ProcessRemoteFunction()
| | | | |--UNetDriver.InternalProcessRemoteFunctionPrivate()
| | | | | |--UNetDriver.ProcessRemoteFunctionForChannelPrivate()
| | | | | | |--// 如果该Channel第一次发送消息, 则首先进行属性同步, 创建对应的Actor.
| | | | | | |--Ch->ReplicateActor(); // UActorChannel.ReplicateActor()
| | | | | | |--FRepLayout.SendPropertiesForRPC()
| | | | | | | |-- 遍历所有参数, 对参数进行序列化.
| | | | | | | |-- 如果该参数不需要序列化, 则将send标记为0, 表示后边无数据
| | | | | | | |-- 如果该参数需要序列化, 则将send标记为1, 表示后边有数据.
| | | | | | | |-- 由于两端使用同一个版本的函数, 所以直接按照类型解析即可.
| | | | | | |-- 添加FieldHeader--WriteFieldHeaderAndPayload
| | | | | | |-- 添加ContentBlockHeader
| | | | | | |--//如果为QueueBunch, 则将其放入FObjectReplicator.RemoteFunctions中
| | | | | | |--UActorChannel.QueueRemoteFunctionBunch
| | | | | | |--// 如果不是QueueBunch, 则将其发送.
| | | | | | |--UChannel.SendBunch

从上面堆栈可以看出, 任何一个UObj RPC时候, 需要找到使用哪个Channel发送数据. Channel和Actor绑定, 所以需要找到对应的Actor, UObj找Actor的方法一般是通过Outer. 所以, 这里引出自定义UObject进行Replicate的时候, 需要重写CallRemoteFunction和GetFunctionCallspace(用于识别本地执行还是远端执行).

识别本地执行还是远端执行.

使用Actor归属的NetDriver, 执行该函数.

处理Multicast

如果是Multicast, 需要遍历所有Connection依次发送., 因为所有Connection共用同一套参数, 可以使用SharedSerialization对参数进行优化.

如果Channel第一次发送数据, 要首先进行属性同步

填充消息代码

body结构: bSend+属性

填充RPC消息头

RPC消息头是由ContentBlockHeader+FileHeader组成.

相关代码:

注意, 这里还会填入Payload的bit大小, 用于远端处理该数据包.

接收RPC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 接收RPC堆栈
--UIpNetDriver.TickDispatch()
|--UNetConnection.ReceivedRawPacket()
| |--UNetConnection.ReceivedPacket()
| | |--UChannel.ReceivedRawBunch()
| | | |--UChannel.ReceivedNextBunch()
| | | | |--UChannel.ReceivedSequencedBunch()
| | | | | |--UActorChannel.ReceivedBunch()
| | | | | | |--UActorChannel.ProcessBunch()
| | | | | | | |--FObjectReplicator.ReceivedBunch()
| | | | | | | | |--FObjectReplicator.ReceivedRPC()
| | | | | | | | | |--FRepLayout.ReceivePropertiesForRPC()
| | | | | | | | | | |--FRepLayout.SerializeProperties_r()
| | | | | | | | | | | |--AActor.ProcessEvent()
| | | | | | | | | | | | |--UObject.ProcessEvent()
| | | | | | | | | | | | | |--UFunction.Invoke()
| | | | | | | | | | | | | | |--ABasePlayerController.execChangeControllerNumber()
| | | | | | | | | | | | | | | |--ABasePlayerController.ChangeControllerNumber_Implementation

解析ContentBlock

解析ContentBlockHeader, 提取出Obj, 然后将Payload传递下去.

image-20230712103608109

在提取Payload时, 还要设置PayloadData的大小. 在后面解析PayloadData时, 用于判断data数据是否结束.

先处理属性同步

如果该Bunch中有属性同步, 则先解析属性同步.

解析每个参数

首先为每个参数赋默认值.

然后根据Send比特位数据, 解析参数.

调用RPC函数

如果一切顺利, 则最终会调用这里:

RPC QueueBunch

  1. QueueBunch一般针对UnReliable FUNC_NetMulticast RPC.
  2. QueueBunch时UObject级别, 因为Actor/ActorComponent/ReplicationUObject都可能有自己的UnReliable FUNC_NetMulticast RPC.
  3. QueueBunch会放入RemoteFunctions中, 在ReplicateProperty时候将RemoteFunctions放入Bunch中, 进而发送.
  4. QueueBunch是一种优化, 将RPC信息随ReplicationBunch一同发送, 目的是减少流量.

确定为QueueBunch

首先根据类型确定其是否为QueueBunch, 默认情况下UnReliable FUNC_NetMulticast RPC为QueueBunch. 仅仅填充FieldHeaderAndPayload然后放入Replicator的RemoteFunctions中

缓存RPC信息

每个PendingNetRPC维护一个FRPCCallInfo, 里面记录着RPC的名字和调用次数. 如果超过一定限度, 会直接丢弃.

如果超过一定限度, 会直接丢弃.

如果满足条件, 则将QueueBunch放入FObjectReplicator.RemoteFunctions中, 等待发送.

将RemoteFunctions合入ReplicationBunch中

FObjectReplicator.ReplicateProperties时候, 将RemoteFunctions信息合入Bunch中.

image-20230907112035162

当没有OnRep时候, 怎么处理呢?

不会进行同步, 直到OnRep启动.

测试用例:

  1. 开启net.PauseReplicateActor, 这是我自己添加的, 参考UChannel.bPausedUntilReliableACK的实现.
  2. 执行ReliableMulticast和UnreliableMulticast, ReliableMulticast是ok的, UnreliableMulticast失败了, 直到开启OnRep才会调用.

原因:

从上图可以看出, FObjectReplicator.RemoteFunctions都是从Replication中Send出去的, 如果暂停Replication, 则不会进行发送. 并且在FObjectReplicator.StopReplicating时候还会直接置空FObjectReplicator.RemoteFunctions.

思考

属性同步会执行ReadFieldHeaderAndPayload吗?

会的, 但是如果发现Bunch已经到结尾了, 就直接返回了.

DS和客户端的函数不匹配

客户端向DS发送RPC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 主端代码:
UFUNCTION(Server, Reliable)
void ChangeControllerNumber(int32 InNumber, int32 InNumber2);

// DS端代码:
UFUNCTION(Server, Reliable)
void ChangeControllerNumber(int32 InNumber);

// tick时候, 主端向DS发送消息:
if (GetLocalRole() == ENetRole::ROLE_AutonomousProxy)
{
++LocalControllerNumber;
ChangeControllerNumber(LocalControllerNumber, 0);
}

// 报错:
[2023.07.11-16.56.32:153][161]LogNet: Error: ReceivedRPC: ReceivePropertiesForRPC - Mismatch read. Function: ChangeControllerNumber, Object: NewPlayerController_C /Game/ThirdPersonCPP/Maps/ThirdPersonExampleMap.ThirdPersonExampleMap:PersistentLevel.NewPlayerController_C_2147482552
[2023.07.11-16.56.32:158][161]LogNet: Error: UActorChannel::ProcessBunch: Replicator.ReceivedBunch failed. Closing connection. RepObj: NewPlayerController_C /Game/ThirdPersonCPP/Maps/ThirdPersonExampleMap.ThirdPersonExampleMap:PersistentLevel.NewPlayerController_C_2147482552, Channel: 2

FObjectReplicator::ReceivedRPC里面有各种检测. 只有完全匹配的函数才能执行.

踢掉连接的原因为FObjectReplicator.ReceivedRPC()函数返回false, 并一路返回false之后, DS直接关闭连接.

结果: DS直接关闭链接, 踢掉客户端.

DS向客户端发送RPC

首次接收到RPC时, 在解析属性时候, 如果解析过程出了问题(ReaderBit到了Reader结尾, 读取参数个数错误等), 会直接将FieldCache->bIncompatible设置为true, 表示该结构体在DS与客户端不兼容, 后边所有收到该函数的RPC都会直接丢弃.

丢弃逻辑: DS再次发来RPC, 发现其FFieldNetCache.bIncompatible为true, 则直接忽略.

总结: DS向客户端发送RPC, 如果不匹配, 客户端只是丢弃该RPC, 其他正常执行.

每个Bunch最多只能包含一个RPC吗?

这个是根据bunch类型决定的, 首先来看一下Bunch的填充流程:

声明bunch:

填充bunch body:

如果是QueueBunch, 则将bunch拼接, 注意:这里只填充了FileHeader+Body. 而且QueueBunch是需要满足特殊条件的:

如果不是QueueBunch,则需要填充ContentBlock+FileHeader+Body.

发送bunch:

如果是QueueBunch则, 将其放入FObjectReplicator.RemoteFunctions中. 最终会在FObjectReplicator.ReplicateProperties时候一同发送给客户端. 所以从这一点还可以看出, 只有DS才能有QueueBunch.

如果不是则直接发送:

从解析RPC代码来看, 是允许多个RPC组合在一个Bunch里面的.

循环处理FileHeader+Body.

FFieldNetCache

在发送RPC过程中将FieldNetIndex序列化到Bunch中. 在接收RPC时,通过FieldNetIndex找到对应的处理函数, 然后调用.

其数据结构如下:

其中:

  1. FClassNetCache.FieldsBase:该FClassNetCache所描述的类的起始索引
  2. FClassNetCache.Super: 其对应Class的父类的FClassNetCache
  3. FClassNetCache.Fields:所有能进行网络传输的数据, 包括变量和函数.
  4. FClassNetCache.FieldMap: 函数/UObj地址-FFieldNetCache指针, 可以通过变量或者函数地址查询其FClassNetCache

DS和客户端能够构建一套相同结构的类, 就可以通过只传输Index就能索引对应的属性, 然后进行操作.

注意

在UClass中, 将网络同步的变量放在了UClass.ClassReps中, 将网络同步的函数放在了UClass.NetFields中, 并且他们的初始化全部都在Class->SetUpRuntimeReplicationData();中.

在CloseChannel时, 如果有ReliableBunch还未发Ack, 会关闭吗?

总结

  1. RPC Bunch由ContentBlockHeader+[FileHeader+ParamList] list构成

  2. 对于还没创建Actor的情况就发送RPC, 则先进行属性同步(为的是创建Actor), 在发送RPC.

  3. DS与客户端函数不匹配---如果DS向客户端发送RPC, 客户端只报一次错误, 并将FieldCache->bIncompatible设置为true, 后续遇到该RPC直接丢弃.

  4. DS与客户端函数不匹配---如果客户端向DS发送RPC, DS直接踢掉该Connection.