Capn' ProtoのRPCの仕組みを説明する
まえがき
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);
}
この時、 SampleB
の getSampleA
を呼び出すことでSampleA
を取得します
Capablity
この時、
SampleB
のgetSampleA
を呼び出すことでSampleA
を取得します
と説明しましたが、そもそも(基本的に)ClientとServerは別プロセスです
したがって、Serverの SampleA
のオブジェクトをDeep-CopyしてClientに渡しても無意味です
つまり、この時渡されているのは SampleA
のオブジェクトそのものではありません
渡されているのは SampleA
のオブジェクトへの参照であり、これが Capablity
です
先の図をもう少しこの理解に近づけると、以下のようになります
Pipeline
公式ドキュメントには以下の図があります
これはfoo()の戻り値xを利用してbar(x)を実行する場合、メッセージ送信の回数が少なくすむことを表現しています
- 通常のRPCの場合
- clientがfoo()を呼び出す(1回目の通信)
- serverがfoo()を実行し、戻り値xをclientに返す(2回目の通信)
- clientが戻り値xを受け取り、bar(x)を呼び出す(3回目の通信)
- serverがbar(x)を実行する
- Capn' Protoの場合
- clientがfoo()を呼び出し、続けてbar(x)を呼び出す。
xにはfoo()の実行結果を使うように指示する(1回目の通信) - serverがfoo()を実行し、戻り値xを得る
- serverがbar(x)を実行する
- clientがfoo()を呼び出し、続けて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