本页尚未翻译,当前显示英文版本。
RPC
**类型化的调用,函数体在别处执行。**把过程声明在它该在的地方——模块上或实体内部——每个绑定都会得到一个生成的、可 await 的方法。动词只有 invoke;单向调用是声明的一种模式,不是另一个动词。
何时使用
- 调用方需要答复:请求/响应,返回值有类型。
- 调用方只管上报、继续往下走:声明为单向的 RPC,不会有任何东西传回。
- 工作比调用本身持续更久:声明为延迟的 RPC 会交回一个工作描述符(work descriptor),而不是超时。
- 一个问题,多个回答者:群组 (group)调用就是 N 次调用,每个答复到达时都绑定着发出它的成员(member)。
- 动作属于某个对象:把它声明在实体(entity)内部,它只存在于那里(声明方式见 Entity)。
- 无需使用:如果并没有要求谁去做什么。别人只需做出反应的事实,是一个事件。
分工
| 主体 | 在本页中的职责 |
|---|---|
schema-author | 声明 RPC、它们的模式以及谁可以调用 |
任意主体 | 在声明允许的范围内,发起有答复的调用或单向调用 |
群组成员 | 答复扇出调用;每个成员传回一个答复 |
概览
声明一个有答复的 RPC 和一个单向 RPC;分别调用,再扇出到群组
[Rpc] // answering, immediate, not overridable — the bare defaults
public static ScoreVerdict SubmitScore(ScoreReport report) => Scores.Judge(report);
[Rpc(OneWay = true)] // declared one-way: nothing travels back
public static void ReportPing(PingSample sample) => Metrics.Add(sample);
// invoking — generated, typed, awaitable
var verdict = await playserv.Rpc.Invoke.SubmitScore(report);
playserv.Rpc.Invoke.ReportPing(sample); // one-way by declaration, not by call site
// group fan-out: N calls, one answer bound to each member
await foreach (var answer in squad.Invoke.ReadyCheck())
Hud.Mark(answer.Member, answer.Ready);export class MatchRpcs {
@Rpc() // answering, immediate, not overridable — the bare defaults
static submitScore(report: ScoreReport): ScoreVerdict { return Scores.judge(report); }
@Rpc({ oneWay: true }) // declared one-way: nothing travels back
static reportPing(sample: PingSample): void { Metrics.add(sample); }
}
// invoking — generated, typed, awaitable
const verdict = await playserv.rpc.invoke.submitScore(report);
playserv.rpc.invoke.reportPing(sample); // one-way by declaration, not by call site
// group fan-out: N calls, one answer bound to each member
for await (const answer of squad.invoke.readyCheck())
hud.mark(answer.member, answer.ready);@rpc() # answering, immediate, not overridable — the bare defaults
def submit_score(report: ScoreReport) -> ScoreVerdict:
return scores.judge(report)
@rpc(one_way=True) # declared one-way: nothing travels back
def report_ping(sample: PingSample):
metrics.add(sample)
# invoking — generated, typed, awaitable
verdict = await playserv.rpc.invoke.submit_score(report)
playserv.rpc.invoke.report_ping(sample) # one-way by declaration, not by call site
# group fan-out: N calls, one answer bound to each member
async for answer in squad.invoke.ready_check():
hud.mark(answer.member, answer.ready)即将推出 —— Go
Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
// invoking — generated, typed (the client surface; bodies live where routing sends them)
Client->Rpc->Call->SubmitScore(Report,
TPSOnResult<FScoreVerdict>::CreateWeakLambda(this, [this](const TPSResult<FScoreVerdict>& Result)
{
if (!Result.HasValue()) { return; }
Hud->ShowVerdict(Result.Value());
}));
Client->Rpc->CallOneWay->ReportPing(Sample); // one-way by declaration, not by call site
// group fan-out: one call, one answer bound to each member
Squad->Call->ReadyCheck(TPSOnResult<FReadyAnswer>::CreateWeakLambda(this,
[this](const TPSResult<FReadyAnswer>& Answer)
{
if (!Answer.HasValue()) { return; }
Hud->Mark(Answer.Value().Member, Answer.Value().Ready); // the delegate fires once per member
}));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.// Unity invokes; RPC bodies execute on the platform or a host — engines are not a handler runtime
var verdict = await playserv.Rpc.Invoke.SubmitScore(report);
playserv.Rpc.Invoke.ReportPing(sample); // one-way by declaration, not by call site
await foreach (var answer in squad.Invoke.ReadyCheck())
Hud.Mark(answer.Member, answer.Ready);函数体在哪里执行——云函数、客户端、主客户端还是游戏服务器——属于路由,按方法声明;覆盖和中间件见 Extensibility。调用失败会抛出类型化的 Problem(Core)。
模型
RPC 声明哪些内容。
| 声明项 | 含义 |
|---|---|
name | 取自动词词表 |
input | 调用方必须自己选定的参数 |
output | 恰好一个声明类型。精简版的答复要单独声明一个类型,而不是同一类型省掉几个字段;否则“没有请求”“不存在”和“被访问掩码隐藏”看起来会一模一样 |
答复模式 | with a reply:返回声明输出类型的值,或类型化的拒绝。one-way:没有答复,调用方只会得知本地发送失败 |
执行模式 | immediate:结果在调用内返回。deferred:调用返回工作描述符,结果之后读取或通过订阅送达。执行模式由声明决定,而不是运行时按负载选择,因为调用方依赖答复的形状 |
streaming | 输入和输出是否分段到达、边到边处理,而不是整体到达 |
idempotency | 单向 RPC 同样带有幂等键:没有答复不等于不会重复投递 |
overridability | 声明在方法本身上。没有声明的方法不能覆盖 |
context | 声明所在的位置。在实体内部声明的 RPC 是该实体的一部分,在实体之外不存在。在游戏服务器中声明 RPC,就等于在路由器中注册它——没有第二种添加方式 |
需要结果时不要用单向调用
单向调用只会把本地发送失败告诉调用方。如果你需要知道发生了什么,就声明答复:结果未知的代价比明确的拒绝更高。
一次调用携带什么。
| 携带项 | 含义 |
|---|---|
arguments | 只有调用方必须自己选定的内容 |
隐式上下文 | 接收方、调用方和环境上下文,在你写的第一个参数之前就已绑定——实体的方法永远不需要传入该实体的标识符 |
references | 作为参数传入的 SDK 对象以类型化的 Ref 传递:一个标识符或游标,而不是内容的副本 |
outcome | 声明输出类型的值,或类型化的 Problem |
引用是地址,不是权限
接收方以自己的身份解析 Ref,受它自己的权限(permission)和谓词约束。传递引用并不会授予对所指内容的访问权。
延迟调用的描述符包含什么。
| 包含项 | 含义 |
|---|---|
state | accepted → running → completed 或 failed,后两者是终态 |
lifetime | 由声明给出;超过后结果不再可用,再去查询会得到拒绝,而不是空答复 |
cancel | 幂等。它只是一个请求:你观察到的终态,是工作实际到达的 completed 或 failed |
所有 RPC 都满足的几点。
| 始终成立 | 含义 |
|---|---|
一个处理器 | 恰好一个逻辑处理器。RPC 请求执行一个动作,它的处理器必须存在;事件陈述一个事实,可以完全没有接收者。所以单向 RPC 不是事件,面向群组调用是 N 次调用而不是一次:Groups 提供地址,答复以流的形式返回,每个都绑定着发出它的成员 |
没有状态机 | 声明和即时调用都没有状态机:调用要么返回了结果,要么没有,没有时就按超时规则处理。只有延迟调用有可观察的状态 |
流 | 不是原子的。流式输出对整体不作任何保证,所以接收方必须处理中断,并区分“已完成”和“被中断” |
写入不接受谓词 | 任何写入都不接受谓词作为输入:“对所有满足该条件的人执行此操作”不是一种操作。批量动作通过枚举表达:先读出集合,再把列表交给一个声明了部分失败语义的批量操作 |
延迟 RPC:调用返回工作描述符,结果随后针对它送达
[Rpc(Execution = Execution.Deferred)] // minutes of work — an answer inside the call would be a timeout
public static MatchReport BuildMatchReport(MatchId match) => Reports.Build(match);
var work = await playserv.Rpc.Invoke.BuildMatchReport(matchId); // the descriptor, not the report
work.OnOutcome(report => Hud.ShowReport(report)); // or read it later, by descriptor
await work.Cancel(); // a request, not a promise nothing ranexport class ReportRpcs {
@Rpc({ execution: Execution.Deferred }) // minutes of work — an answer inside the call would be a timeout
static buildMatchReport(match: MatchId): MatchReport { return Reports.build(match); }
}
const work = await playserv.rpc.invoke.buildMatchReport(matchId); // the descriptor, not the report
work.onOutcome((report) => hud.showReport(report)); // or read it later, by descriptor
await work.cancel(); // a request, not a promise nothing ran@rpc(execution=Execution.DEFERRED) # minutes of work — an answer inside the call would be a timeout
def build_match_report(match: MatchId) -> MatchReport:
return reports.build(match)
work = await playserv.rpc.invoke.build_match_report(match_id) # the descriptor, not the report
work.on_outcome(lambda report: hud.show_report(report)) # or read it later, by descriptor
await work.cancel() # a request, not a promise nothing ran即将推出 —— Go
Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
// the client side of a deferred call: a descriptor now, the outcome against it later
Client->Rpc->Call->BuildMatchReport(MatchId,
TPSOnResult<FPSDeferredHandle>::CreateWeakLambda(this, [this](const TPSResult<FPSDeferredHandle>& Result)
{
if (!Result.HasValue()) { return; }
const FPSDeferredHandle Work = Result.Value();
TPSSubscription ReportWatch = Client->Rpc->Deferred->Subscribe(Work,
[this](const FMatchReport& Report) { Hud->ShowReport(Report); });
Client->Rpc->Deferred->Cancel(Work); // a request, not a promise nothing ran
}));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.// the client side of a deferred call: a descriptor now, the outcome against it later
var work = await playserv.Rpc.Invoke.BuildMatchReport(matchId);
work.OnOutcome(report => Hud.ShowReport(report));
await work.Cancel(); // a request, not a promise nothing ran错误
- 权限挂在具体 RPC 上,从不挂在原语上。没有笼统的“可以调用”:每个声明都写明调用方必须持有的原子权限。没有它的调用方会收到类型化的
forbidden拒绝,而不是被悄悄丢弃。 - 被隐藏的实例表现为 “not found”。对调用方的行谓词所隐藏的实例调用实体 RPC,答复与读取该实例完全相同,因此拒绝不会向调用方透露任何关于存在与否的信息。
- 没有处理器时返回 “unavailable”,而不是 “not found”。RPC 已经声明,所以它存在;缺的是一条路由。值得重试,因为游戏服务器可能恢复;“not found” 则会告诉调用方就此停止。
- hook 的拒绝带有 hook 自己的错误码 和原因,所以“被游戏规则拒绝”永远不会看起来像“传输断了”。
- 超时不是结果。延迟调用就去读描述符;即时调用则靠声明的幂等键(idempotency key)保证重试安全。
限制
具体数值会随平台限制一章给出;达到每个上限时会发生什么,现在已经确定。
- 输入大小:调用在执行前就被拒绝。
- 输出大小:直接拒绝而不截断,因为截断的答复无法和完整答复区分开。
- 每个主体(actor)的调用频率:返回限流拒绝,并写明何时可以重试。
- 每个主体的并发延迟调用数:拒绝新调用,进行中的调用照常完成。
- 描述符生命周期:超过后结果不再可用,查询会被拒绝。
- 调用链深度:超过时返回声明好的拒绝,而不是耗尽资源或悄然中断。