RPC浅析
发送RPC
1 | // 发送RPC堆栈 |
从上面堆栈可以看出, 任何一个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 | // 接收RPC堆栈 |
解析ContentBlock
解析ContentBlockHeader, 提取出Obj, 然后将Payload传递下去.
在提取Payload时, 还要设置PayloadData的大小. 在后面解析PayloadData时, 用于判断data数据是否结束.
先处理属性同步
如果该Bunch中有属性同步, 则先解析属性同步.
解析每个参数
首先为每个参数赋默认值.
然后根据Send比特位数据, 解析参数.
调用RPC函数
如果一切顺利, 则最终会调用这里:
RPC QueueBunch
- QueueBunch一般针对UnReliable FUNC_NetMulticast RPC.
- QueueBunch时UObject级别, 因为Actor/ActorComponent/ReplicationUObject都可能有自己的UnReliable FUNC_NetMulticast RPC.
- QueueBunch会放入RemoteFunctions中,
在ReplicateProperty时候将
RemoteFunctions
放入Bunch
中, 进而发送. - 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中.
当没有OnRep时候, 怎么处理呢?
不会进行同步, 直到OnRep启动.
测试用例:
- 开启
net.PauseReplicateActor
, 这是我自己添加的, 参考UChannel.bPausedUntilReliableACK
的实现. - 执行ReliableMulticast和UnreliableMulticast, ReliableMulticast是ok的, UnreliableMulticast失败了, 直到开启OnRep才会调用.
原因:
从上图可以看出,
FObjectReplicator.RemoteFunctions
都是从Replication中Send出去的,
如果暂停Replication, 则不会进行发送.
并且在FObjectReplicator.StopReplicating
时候还会直接置空FObjectReplicator.RemoteFunctions
.
思考
属性同步会执行ReadFieldHeaderAndPayload吗?
会的, 但是如果发现Bunch已经到结尾了, 就直接返回了.
DS和客户端的函数不匹配
客户端向DS发送RPC
1 | // 主端代码: |
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找到对应的处理函数, 然后调用.
其数据结构如下:
其中:
- FClassNetCache.FieldsBase:该FClassNetCache所描述的类的起始索引
- FClassNetCache.Super: 其对应Class的父类的FClassNetCache
- FClassNetCache.Fields:所有能进行网络传输的数据, 包括变量和函数.
- FClassNetCache.FieldMap: 函数/UObj地址-FFieldNetCache指针, 可以通过变量或者函数地址查询其FClassNetCache
DS和客户端能够构建一套相同结构的类, 就可以通过只传输Index就能索引对应的属性, 然后进行操作.
注意
在UClass中, 将网络同步的变量放在了UClass.ClassReps
中,
将网络同步的函数放在了UClass.NetFields
中,
并且他们的初始化全部都在Class->SetUpRuntimeReplicationData();
中.
在CloseChannel时, 如果有ReliableBunch还未发Ack, 会关闭吗?
总结
RPC Bunch由ContentBlockHeader+[FileHeader+ParamList] list构成
对于还没创建Actor的情况就发送RPC, 则先进行属性同步(为的是创建Actor), 在发送RPC.
DS与客户端函数不匹配---如果DS向客户端发送RPC, 客户端只报一次错误, 并将FieldCache->bIncompatible设置为true, 后续遇到该RPC直接丢弃.
DS与客户端函数不匹配---如果客户端向DS发送RPC, DS直接踢掉该Connection.