Перейти к основному содержимому
Строительные блоки

Эта страница ещё не переведена — показана английская версия.

RPC

Типизированный вызов, тело которого находится в другом месте. Объявите процедуру там, где ей место, — в модуле или внутри сущности, и в каждом биндинге появится сгенерированный асинхронный метод. Глагол один — invoke. Односторонний вызов — это режим объявления, а не отдельный глагол.

Когда это нужно

  • Вызывающему нужен ответ: запрос и ответ с типизированным результатом.
  • Вызывающий сообщает и идёт дальше: объявленный односторонний RPC, обратно ничего не приходит.
  • Работа длится дольше вызова: объявленный отложенный RPC возвращает описание работы (work descriptor), а не падает по таймауту.
  • Вопрос один, отвечают многие: вызов группы (group) — это N вызовов, и каждый ответ приходит с привязкой к участнику, который его прислал.
  • Действие принадлежит объекту: объявите его внутри сущности (entity) — там оно и живёт, и больше нигде (Entity показывает, как это объявить).
  • Не используйте RPC, если никого не просят что-то сделать. Факт, на который другие просто реагируют, — это событие.

Кто что делает

АкторЧто делает на этой странице
schema-authorобъявляет 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, объявленный внутри сущности, — часть этой сущности, и вне её его нет. Объявить RPC на игровом сервере — это и есть зарегистрировать его в маршрутизаторе, другого способа добавить RPC нет
with a reply · immediatewith a reply · deferredone-wayinvokethe outcome, or a typed refusalinvokea work descriptor, at oncethe outcome, laterinvokeno reply within the callread it, or subscribeonly a local send failure
Что получает вызывающий: результат сразу в ответе, описание работы сразу и результат позже или никакого ответа

Если нужен результат, не делайте вызов односторонним

Односторонний вызов сообщает вызывающему только о локальной ошибке отправки. Если нужно знать, что произошло, объявите ответ: неизвестный результат обходится дороже, чем известный отказ.

Что передаётся при вызове.

Что передаётсяЧто это
argumentsтолько то, что должен выбрать вызывающий
неявный контекстполучатель, вызывающий и окружающий контекст. Они подставляются до первого параметра, который вы пишете сами, поэтому методу сущности никогда не нужно передавать идентификатор этой сущности
referencesобъект SDK, переданный аргументом, передаётся как типизированный Ref — идентификатор или курсор, а не копия содержимого
outcomeзначение объявленного выходного типа или типизированный Problem

Ссылка — это адрес, а не разрешение

Получатель разрешает Ref от своего имени, со своими разрешениями (permission) и предикатами. Передав ссылку, вы не даёте доступа к тому, на что она указывает.

Что хранит описание работы отложенного вызова.

Что хранитЧто это
stateaccepted → running → completed или failed; последние два — конечные
lifetimeвремя жизни объявляется заранее. Когда оно истекло, результат недоступен, и запрос результата получает отказ, а не пустой ответ
cancelидемпотентна. Это просьба, а не приказ: вы увидите то конечное состояние, completed или failed, до которого успела дойти работа

Что верно для любого RPC.

ВсегдаЧто это
один обработчиклогический обработчик ровно один. RPC просит выполнить действие, и обработчик у него обязан быть. Событие (event) констатирует факт, и получателей у него может не быть вовсе. Поэтому односторонний 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 ran
export 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 сказало бы вызывающему, что пробовать дальше не нужно.
  • Отказ из хука несёт собственный код и причину хука, поэтому «отклонено правилом игры» никогда не будет выглядеть как «сломался транспорт».
  • Таймаут — это не результат. Для отложенного вызова прочитайте описание работы. Для немедленного повтор безопасен благодаря объявленному ключу идемпотентности.

Ограничения

Конкретные числа появятся вместе с главой об ограничениях платформы, а поведение на каждой границе уже определено.

  • Размер входных данных: вызов отклоняется до выполнения.
  • Размер выходных данных: ответ отклоняется, а не обрезается, потому что обрезанный ответ не отличить от полного.
  • Частота вызовов от одного актора: отказ по частоте с указанием, когда можно повторить.
  • Число одновременных отложенных вызовов на актора: новый вызов отклоняется, уже запущенные доходят до конца.
  • Время жизни описания работы: после него результат недоступен, и запрос получает отказ.
  • Глубина цепочки вызовов: при превышении — объявленный отказ, а не исчерпание ресурсов или молчаливый обрыв.