区分调用 和 执行
对于由 Client , Server , NetMulticast 说明符声明的三种类型的 RPC 函数,要了解它们的使用规范首先我们要区分调用 和 执行概念的区别。
假设我们声明了一个名为 FunctionName() 的 RPC 函数,用户并不需要实现这个函数,而是要额外的实现一个名为 FunctionName_Implementation() 函数处理真正要执行逻辑。而这个 FunctionName() 函数会由 UHT 来为我们实现来处理 RPC 调用的逻辑。
我们将用户调用 FunctionName() 函数行为称为用户发起了这个 RPC 函数的调用,而将真正执行到 FunctionName_Implementation() 函数时称为这个 RPC 函数被执行了。
RPC 调用和执行的简单流程
发送端调用流程
- 假设由 UFUNCTION( Client ) 声明了一个名为 FunctionName() 的 RPC 函数。这个函数会由 UHT 实现,用户无需对它实现,但用户需要额外的实现一个 FunctionName_Implementation() 函数处理函数的真正执行逻辑。
UHT 首先会生成一个名为 execFunctionName() 函数,由它来调用用户定义的 FunctionName_Implementation() 函数,并会以 "FunctionName" 为函数名,来将这个 execFunctionName() 函数注册为当前 UClass 类的一个 UFunction 对象。
然后 UHT 会生成 FunctionName() 函数的实现,其内就是获取自己所属 UClass 中名为 "FunctionName" 的 UFunction 对象,并调用 UObject::ProcessEvent() 处理这个 UFunction 的执行逻辑。因此 RPC 函数如何被远程调用,就是由 UObject::ProcessEvent() 来实现的 - 在 UObject::ProcessEvent() 中首先会调用 AActor::GetFunctionCallspace() 来获取一个 FunctionCallspace 枚举来标识此 UFunction 的执行空间。其取值如下
FunctionCallspace::Absorbed 终止执行,即不会本地执行也不会远程执行
FunctionCallspace::Remote 远程执行,需要通过UNetDriver来远程调用
FunctionCallspace::Local 本地执行,仅本地执行
在获取了函数的执行空间后,若需要本地执行则本地执行,若还需要远程执行则参见 UObject::ProcessEvent() 会回调自己的 UObject::CallRemoteFunction() 来处理远程执行的逻辑。
参见 AActor::CallRemoteFunction() 中,会调用UNetDriver::ProcessRemoteFunction() 来处理 RPC 远程调用。最终会调用 FRepLayout::SendPropertiesForRPC() 来将 RPC 调用实参封装成一个 RPC Bunch ,并通过 UActorChannel 通道发送给对端
接收端执行流程
- 在接收到的 Bunch 中通常会包含许多个属性块,来保存 AActor 和其子对象的同步属性。且每个对象都对应于一个 FObjectReplicator 对象来处理其属性块数据的解析。因此在通道的 UActorChannel::ProcessBunch() 中解析 Bunch 中每个属性块时,会获取每个属性块对应的 FObjectReplicator 对象,并调用其 FObjectReplicator::ReceivedBunch() 来解析此对象的属性。
- 在 FObjectReplicator::ReceivedBunch() 中若发现一个和 UFunction 相关的数据块,通常表示这是对端发送来的 RPC 调用的数据。此时会调用 FObjectReplicator::ReceivedRPC() 来处理这个 UFunction 的执行逻辑。大致如下。
首先会先验证此 UFunction 是否为能在本机合法执行的 RPC 函数
>>1 若本机为客户端,则被执行的需要为 UFUNCTION( Client ) 或 UFUNCTION( NetMulticast ) 修饰的函数 ,否则不能执行
>>2 若本机为服务器,则被执行的需要为 UFUNCTION( Server ) 修饰的函数 ,且同时要求当前链接拥有这个 AActor 才能执行,否则不能执行
在验证可以调用后,会获取这个 UFunction 对应的 FRepLayout
并调用 FRepLayout::ReceivePropertiesForRPC() 解析 Bunch 中的 RPC 调用实参。
然后会传入调用实参,并以当前正在解析的这个属性块所属的对象作为 this 来调用 UObject::ProcessEvent() 来执行这个 UFunction
通常此函数会在接收端立即执行,参见 RPC 调用的一些细节 中 [ 可靠 RPC 在客户端的延迟执行 ] 的分析,在客户端调用一个可靠的 RPC 函数,且其包含未求解成功的对象引用类型的实参时,还可能发生延迟执行。
三种 RPC 函数
相关概念的简介
- 由 AActor->LocalRole == ROLE_Authority 检测是否为本地权威角色
- 由 AActor::GetNetConnection() != NULL 检测 AActor 是否有所属链接
- 由 AActor->RemoteRole != ROLE_None 检测 AActor 是否为参与属性同步
UFUNCTION( Server )
用于声明由客户端发起调用,在服务器执行的 RPC 函数。
无论是否可靠,构造的 RPC Bunch 都会由 UChannel::SendBunch() 立即发送。
执行空间
- 在服务器调用时仅本地执行
- 在客户端调用时,对于本地权威角色的 AActor ,若其参与属性同步且有所属链接则远程执行,否则仅本地执行
- 在客户端调用时,对于非本地权威角色的 AActor ,若其参与属性同步且有所属链接则远程执行,否则终止执行
远程执行的过滤情况
- 若是不可靠的 RPC 调用,且链接发送缓存已饱和,则客户端会抛弃此调用
- 若还未在链接中为 AActor 创建通道则抛弃调用,需要等服务器由 AActor 通道发送 OpenBunch 促使客户端创建通道后,客户端才能发起 RPC 调用
- 若服务器接收到这个 RPC Bunch 后找不到对应的 UActorChannel 通道处理,服务器不会响应这个 RPC 调用
UFUNCTION( Client )
用于声明由服务器发起调用,在客户端执行的 RPC 函数。
无论是否可靠,构造的 RPC Bunch 都会由 UChannel::SendBunch() 立即发送。
执行空间
- 在客户端调用时则仅本地执行
- 在服务器调用时,对于本地权威角色的 AActor ,若其参与属性同步且有所属链接则远程执行,否则仅本地执行
- 在服务器调用时,对于非本地权威角色的 AActor ,若其参与属性同步且有所属链接则远程执行,否则终止执行
远程执行的过滤情况
- 若 AActor 在 PendingKill 或 Unreachable 状态,或已对其调用 UWorld::DestroyActor() 来请求析构,则服务器会抛弃调用
- 若是不可靠的 RPC 调用,且链接发送缓存已饱和,则服务器会抛弃此调用
- 若还未在链接中为 AActor 创建通道,则要验证链接的客户端已加载此 AActor 所在的 ULevel 后,才为 AActor 创建通道,否则抛弃调用
- 若通道还未发送 OpenBunch ,则会自动发送一个 AActor 属性同步的 Bunch 作为 OpenBunch 促使客户端创建 AActor 和通道 ,然后才继续进行 RPC 调用
UFUNCTION( NetMulticast )
用于声明由服务器发起调用,并广播到所有客户端执行的 RPC 函数。
若为可靠的则 RPC Bunch 会立即发送,若为不可靠的则 RPC Bunch 会随着下次此 RPC 函数所在对象向此链接进行属性同步时,才会一起发送。
此外注意,由于在服务器的 UNetDriver 可能会使用 UReplicationGraph 来处理同步,因此这里的过滤情况分为两种。
执行空间
- 在客户端调用时仅本地执行
- 在服务器调用时,对于参与同步的 AActor 即会本地执行也会远程执行,对于不参与同步的 AActor 仅本地执行
UNetDriver 处理时远程执行的过滤情况
- 遍历 UNetDriver->ClientConnections 数组中的每个链接,进行如下检测来分发
- 调用 AActor::IsNetRelevantFor() 来检测此 AActor 和每个链接是否相关,若不相关则并不向该链接分发。注意,若调用的是可靠的广播 RPC 函数,当 AActor 和链接不相关,但还没有从此链接中移除此 AActor 通道前,也任向此链接分发
- 若还未在链接中为 AActor 创建通道,则需要验证链接的客户端已加载此 AActor 所在的 ULevel 后,才为 AActor 创建通道,否则抛弃调用
- 若通道还未发送 OpenBunch ,则会自动发送一个 AActor 属性同步的 Bunch 作为 OpenBunch 促使客户端创建 AActor 和通道 ,然后才继续进行 RPC 调用
UReplicationGraph 处理时远程执行的过滤情况
- 若 AActor 在 PendingKill 或 Unreachable 状态,或已对其调用 UWorld::DestroyActor() 来请求析构,则服务器会抛弃调用,不对任何链接进行分发
- 否则,遍历 UReplicationGraph->Connections 数组中的每个链接,进行如下检测来分发
- 若链接的 UNetConnection->ViewTarget 没有引用链接的观察对象,这表示链接还未创建好 APlayerController ,此时不向此链接分发
- 若当前链接的客户端还未已加载好此 AActor 所在的 ULevel,则不向此链接分发
- 若 AActor 在当前链接中还未打开 UActorChannel 通道,取决于是否为此 AActor 是否配置了同步范围。若未配置,则直接在此链接中为此 AActor 创建 UActorChannel 通道,并向此链接分发。 若配置了,则要验证链接的观察位置是否在此 AActor 同步范围内,才在链接中为 AActor 创建通道并继续分发,否则不向此链接分发。
- 若通道还未发送 OpenBunch ,则会自动发送一个 AActor 属性同步的 Bunch 作为 OpenBunch 促使客户端创建 AActor 和通道 ,然后才继续进行 RPC 调用