💬

Capn' ProtoのRPCの仕組みを説明する

2024/10/06に公開

まえがき

Capn' ProtoはRPCを実現するためのフレームワークです
マイナーであるため、そのコンセプトや仕組みを解説しているドキュメントが少ないです
このため、以下のような疑問をもつことが多いです

  • なんでgRPCとかじゃなくてCapn' Proto使ってんだっけ?
  • CapablityとかPipelineって何だっけ?
  • これ本当にgRPCに比べて速いんか?

この疑問を解決するため、このドキュメントは以下の内容を説明します

  • Capn' Protoのコンセプト
  • Capn' ProtoのRPCの仕組み(概要レベル)

逆に、このドキュメントは以下の内容を説明しません

  • Capn' Protoを使ったソフトウェアの作り方
  • Capn' Protoのスキーマをc++やrustにtranspileする方法
  • Capn' Protoのソースコードの設計や読み方

対象読者

以下のすべての条件を満たす人を想定して記述します

  • すでにCapn' Protoを使っており、その使い方は理解している
  • Capn' Protoの仕組みが知りたいと思っている

Capn' Protoのコンセプト

Capn' Protoにはいくつかの重要なコンセプトがあります
普段はあまり意識しませんが、知っておくと仕組みの理解に役立つので説明します

Interface

Capn' ProtoでServerの関数を呼び出す場合、Interfaceに対してMethodを指定します
InterfaceはServerの中でオブジェクトであり、そこにClientから関数を呼び出すイメージです

では、Client側はどうやって呼び出し対象のInterfaceのオブジェクトを指定するのでしょうか?
例えば、以下のようなスキーマを考えます

interface SampleA {
    send @0 (param: Int16) -> (result: Bool);
}

interface SampleB {
    getSampleA @0 () -> (result: SampleA);
}

この時、 SampleBgetSampleA を呼び出すことでSampleA を取得します

Capablity

上のセクション

この時、 SampleBgetSampleA を呼び出すことでSampleA を取得します

と説明しましたが、そもそも(基本的に)ClientとServerは別プロセスです
したがって、Serverの SampleA のオブジェクトをDeep-CopyしてClientに渡しても無意味です

つまり、この時渡されているのは SampleA のオブジェクトそのものではありません
渡されているのは SampleA のオブジェクトへの参照であり、これが Capablity です

先の図をもう少しこの理解に近づけると、以下のようになります

Pipeline

公式ドキュメントには以下の図があります

これはfoo()の戻り値xを利用してbar(x)を実行する場合、メッセージ送信の回数が少なくすむことを表現しています

  • 通常のRPCの場合
    1. clientがfoo()を呼び出す(1回目の通信)
    2. serverがfoo()を実行し、戻り値xをclientに返す(2回目の通信)
    3. clientが戻り値xを受け取り、bar(x)を呼び出す(3回目の通信)
    4. serverがbar(x)を実行する
  • Capn' Protoの場合
    1. clientがfoo()を呼び出し、続けてbar(x)を呼び出す。
      xにはfoo()の実行結果を使うように指示する(1回目の通信)
    2. serverがfoo()を実行し、戻り値xを得る
    3. serverがbar(x)を実行する

このように、ある関数呼び出しに続けて別の関数呼び出しを実行する機能をPipelineと呼びます

Capn' ProtoのRPCの仕組み

例えば、以下のスキーマを考えます


struct MyStruct {
  structMember @0 : List(Int8);
}

Interface MyInterface {
  myMethod @0 : (param: UInt8) -> (result:MyStruct);
}

このスキーマの myMethod を呼び出したときのシーケンスは以下のように理解できます

しかし、実際にCapn' Protoが行う通信のシーケンスはより複雑です
このセクションではこのシーケンスをもうちょっとだけ深掘りします

Capn' Protoの関数呼び出しを掘り下げる

再び以下のスキーマを例に考えます


struct MyStruct {
  structMember @0 : List(Int8);
}

Interface MyInterface {
  myMethod @0 : (param: UInt8) -> (result:MyStruct);
}

この myMethod を呼び出す場合、例えば以下のようなコードになります

void SampleClient::myMethod() {
  auto cap = m_SendRPC->bootstrap().castAs<Sample>();
  auto req = cap.myMethodRequest();
  req.setParam(10);
  auto ret = req.send().wait(m_SendAsyncIoContext.waitScope);
}

このコードを実行すると、以下のようなClientとServerの間で以下のような通信が発生します

続くセクションで、それぞれのメッセージの意味について紹介します
ただし、ここで取り上げるメッセージはあくまで代表例であり、その他のメッセージも存在することに注意してください

Bootstrap

サーバーのInterfaceのMethodを呼び出すための Capability を取得します
メッセージに付属する主要なパラメータは以下の通り

名前 概要
Question ID このメッセージのID。対応するRETURNのQuestion IDに設定される

Call

サーバーのInterfaceのMethodを呼び出す。 呼び出す際には必ずCapabilityが必要です
メッセージに付属する主要なパラメータは以下の通り

名前 概要
Question ID このメッセージのID。このメッセージに対応するRETURNのQuestion IDに設定される
Interface ID InterfaceのID。スキーマでInterfaceに付与するIDはここで使用されている
Method ID 呼び出したいMethodのID。MethodのIDは単純にInterfaceの中で0から順番に割り当てられている
Param 呼び出したいMethodの引数
MessageTarget このメッセージを送信する先(Capability)
CapablityTable 相手に渡すCapabilityの集合

Return

BootstrapやCallに対する応答を通知します
メッセージに付属する主要なパラメータは以下の通り

名前 概要
ANSWER ID このメッセージが返答するメッセージのQuestion ID
Param 返答内容。どのようなメッセージに返答するのかによって中身は変化する
CapablityTable 相手に渡すCapabilityの集合

Finish

CallやBoostrapから始まる一連のセッションが終了(あるいは中断)したことを通知します
メッセージに付属する主要なパラメータは以下の通り

名前 概要
ANSWER ID このメッセージが返答するメッセージのQuestion ID

Release

指定したCapabilityの参照カウンタを減らします
受信側は参照カウンタを減らし、0になったらCapabilityを破棄します
メッセージに付属する主要なパラメータは以下の通り

名前 概要
Capability ID 参照カウンタを減らす対象のCapabilityのID
Ref Count 参照カウンタを減らす量

メッセージの基礎的なデータ構造

各メッセージのPayloadはStruct, List, Pointerなどのより基礎的なデータ構造の組み合わせで表現されます
基礎的なデータ構造については公式のドキュメントによくまとまっています
公式のドキュメント をもう少し噛み砕いたドキュメントを別ページにまとめました

メッセージの具体的な定義

上述の基礎的なデータ構造を組み合わせて具体的なBootstrap, Call, Returnなどのメッセージを表現する方法はrpc.capnpに定義されています

Discussion