要約

  • TCPが守るのは順序付きバイト列であり、CALLやREPLYの境界ではない。ONC RPCは1メッセージを、1個以上の断片からなる1レコードに収めた。
  • 各断片の先頭は4バイトのビッグエンディアン値で、下位31ビットがその断片長、最上位ビットが「この断片の後でレコード終了」を表した。
  • レコード完了はフレーミングの証拠にすぎない。XDRの妥当性、XID、認証、手続きの受理、永続的な実行結果には別の証拠が要る。

「最後」のビットは成功を意味しなかった

パケット解析画面で最上位ビットが立つと、仕事の完了印に見えやすい。しかし、そのビットが閉じるのは手続きではない。CALLまたはREPLYを運ぶrecord-markingレコードだけである。本文の資格情報が拒否されても、プログラム番号が存在しなくても、レコードは正しく終わり得る。

逆も成り立つ。接続がレコード途中で失われても、サーバー側の処理が実行されなかったとは限らない。フレーミングは受信側に届いた境界を記述する。遠隔の副作用の履歴を運んでくるわけではない。

この狭さを理解するには、土台のTCPを見る必要がある。RFC 793は順序付きオクテットを提供し、PUSHがレコード境界を供給しないことを明記した。現行のRFC 9293も、TCPを信頼できる順序付きバイトストリームとしている。セグメント端はRPCの句読点ではない。

4バイトに長さと終端を同居させた

1988年4月のRFC 1050は、この橋渡しをrecord markingと呼んだ。1件のRPCメッセージが1件のRMレコードに入り、レコードは1個以上の断片でできる。6月のRFC 1057も同じ形式を引き継いだ。

断片ヘッダーは4バイトの符号なし値で、上位バイトから並ぶ。下位31ビットは直後に続く断片データのオクテット数で、ゼロから2^31 - 1までを表せる。最上位ビットは真偽値であり、1なら現在の断片がレコードの最後、0なら同じレコードに次の断片が続く。

重要なのは判断の時点である。最上位ビットを読んだ瞬間にレコードが終わるのではない。宣言された本文をすべて受信した後で終わる。長さ分を受信してもビットが0なら、次の4バイトは次のレコードではなく同じレコードの次断片を示す。

TCPの読み出し方から独立した状態機械

受信側はまずマーカー4バイトをそろえる。1回のreadで1バイトしか来なくても異常ではない。4バイトがそろえば、終端ビットと長さを保存し、その長さだけストリームを消費する。本文が複数回の読み出しに分かれても意味は変わらない。

非終端なら再び4バイトを集める。終端なら、それまでの断片を1件のRPCメッセージとして上位層へ渡し、余った次の4バイトから別レコードを始める。1回の読み出しに複数レコードが入っても、1枚のTCPセグメントがマーカー途中で切れても、同じ規則で処理できる。

長さゼロの断片も記述範囲に含まれる。データがなくても、終端か継続かという情報は残る。一方、宣言長の途中でEOFになれば、短いレコードが完成したのではなく未完の断片である。そこで分かるのは受信の状態であり、遠隔処理の結果ではない。

三種類の「断片」を混ぜない

RM fragmentはIPフラグメントでもTCPセグメントでもない。XDRの1項目でも、アプリケーションの1回の書き込みでもない。1つのRM断片が多数のTCPセグメントにまたがり、1つの受信バッファに複数のRM断片や複数レコードが並ぶことがある。

TCP再送やオフロードが可視パケットを組み替えても、再構成されたバイト列が同じならRM境界は同じである。したがって運用記録でpacket、segment、fragment、record、messageを同じ列に入れると、損失場所も責任主体も分からなくなる。

PSHとの違いもここにある。PSHは送信済みバイトを先へ動かす時機に影響できる。RMは、現在の断片を何バイト読めばよいか、いつ1メッセージがそろうかを決める。速く届ける希望と、文法上の終点は別の契約である。

XDRの外側に置く必要があった

RPCメッセージの値はXDRで表現される。RFC 4506は整数、可変長データ、パディング、構造体などの外部表現を共通化する。それによって異なる計算機がCALLやREPLYの内容を同じ順で解釈できる。

それでもRPC仕様は、record markerがXDR標準形式ではないと明記する。バイト順は似ていても、仕事が違うからである。マーカーはRPC値全体を解釈する前に、その値を含む範囲を見つける。内容を読まなければ終点が分からない設計なら、破損時に境界を探す層が内容の意味に依存してしまう。

正しい境界と正しい内容は別々に失敗できる。過大または未完の断片を内容解釈前に拒否できる一方、完全に区切られたレコードをXDRやRPCのエラーとして拒否することもできる。

31ビットを予約済みメモリーと読まない

31ビットは1断片の長さ欄であって、受信側への割当命令ではない。さらに1レコードは複数断片を持てるため、基礎形式はレコード全体の単一上限を示さない。宣言値をそのまま一括確保する実装は、送信者に受信メモリーの支配権を渡してしまう。

累積レコード長、断片数、同時バッファ量、継続時間にローカル予算を設ける必要がある。これはRFCの数値を読み替える主張ではなく、形式を安全に実装するための運用判断である。拒否時には、宣言値と実受信量と予算のどこで止めたかを残す。

また、仕様は境界がプロトコルエラーの検出と回復に役立つと述べるが、任意のペイロード内を走査して「それらしい4バイト」から再開する方法は定めない。偶然の値を新レコードとみなすより、接続を閉じる方が正確な場合がある。

形は保たれ、別の転送では置き換えられた

RFC 1831は1995年に同じ形式を標準化し、RFC 5531は2009年にワイヤ変更なしで引き継いだ。簡単な形式が残った理由は、TCPに欠ける境界だけを補ったことにある。

一方、RFC 8166のRPC-over-RDMAは、transport streamとpayload streamからなる別のフレーミングを持ち、TCP record markingを含む他のRPC framingを置き換える。下層でTCPを使っていても重ねない。動的な切り替えはRPCメッセージとメッセージの間で、転送方式と協調して行う。

つまりrecord markingはRPC手続きそのものの永久属性ではない。TCPというバイトストリームにRPCメッセージを載せるための、転送別アダプターだった。

資料が証明しないもの

閉じた資料集合はRFC 793、RFC 1050、RFC 1057、RFC 1831、RFC 4506、RFC 5531、RFC 8166、RFC 9293である。これらは形式と改訂と層境界を証明するが、現在の採用率、特定ライブラリの動作、実接続の内容、相手の身元、権限、手続き結果を証明しない。

最上位ビットが長く生き残ったのは、控えめだったからだ。異なるパケット化の上でもメッセージ終点を共有できたが、その中の仕事まで終わったとは一度も宣言しなかった。