UDPの受信領域が足りないとき、続きを読めない理由とMSG_TRUNC

緑の浅い器の中に紫の小片が四つ、外に二つ並び、奥の桃色の器には青い小片が二つ入った抽象的な構図。

UDPで送ったデータが、受信側では途中までしか見えない。次の受信で続きを読めると思って待つと、別のデータが返ってくる。こうした問題を調べるには、ネットワークの欠落と、受信処理が用意した領域の不足を分ける必要があります。Linuxの通常のUDP受信を例に、メッセージの境界と切り詰めの検出を整理します。

簡単にまとめると?

  • 通常のUDP受信はデータグラム単位です。受信先の領域が小さいと、入らなかった末尾は失われ、次回の受信で同じデータグラムの続きを読めるわけではありません。
  • Linuxのrecvmsgでは、結果のmsg_flagsにあるMSG_TRUNCが切り詰めを知らせます。受信時に指定する同名のフラグとは、役割を分けて読みます。
  • 一部の内容が届いたことを、完全なメッセージの受信成功と扱わないことが大切です。長さ・切り詰めの有無・通信規約の条件を合わせて判定します。

バイト列を少しずつ読む場合との違い

データグラムは、ひとまとまりのメッセージとして扱う通信の単位です。ここでは、UDP_GROなどの受信オフロードを使わず、MSG_PEEKによる覗き読みもしない通常の受信を考えます。LinuxのUDPでは、一回の受信で一つのデータグラムを取り出します。

たとえば、ABCDEFという6バイトのデータグラムと、XYという2バイトのデータグラムが、この順で受信待ちになったとします。最初の受信に用意した領域が4バイトなら、ABCDEFのうちABCDしか収まりません。この受信で末尾EFは切り捨てられるため、次の受信はEFではなく、次のデータグラムXYを扱います。

これに対し、同じ文字列ABCDEFとXYを単なる連続したバイト列として読む独自モデルでは、4バイトずつの読取りをABCD、EFXYと表せます。後者の考え方をUDPへ持ち込むと、「次に読むと続きが来る」という前提が崩れます。この比較は、通信速度や実際のTCP受信回数の予測ではありません。

6バイトのABCDEFと2バイトのXYが待っている例。4バイトの領域でUDPを受信すると、一回目はABCDでEFは切り捨て、二回目はXYになる。連続バイト列のモデルではABCDとEFXYになる。
順序どおり到着済みのデータを使った独自モデル。UDPの到着順や配達を保証する図ではない。

計算では、この二つの取り出し方を別々の関数にし、UDP型の一回目で切り詰めあり、二回目で切り詰めなしになることを確かめています。送受信の実測はしていません。また、UDP自体は配達・順序・重複の排除を保証するものではありません。図は受信待ちになった後の取り出し方だけを切り出したものです。

MSG_TRUNCは、入力と出力を区別する

Linuxのrecvmsgは、受信した内容に加えて、msghdrという構造体のmsg_flagsへ結果のフラグを返します。ここにMSG_TRUNCが立っていれば、データグラムが用意した領域より大きく、末尾が捨てられたことを示します。

一方、recvmsgなどを呼ぶ際のflags引数にも、同じ名前のMSG_TRUNCを指定できます。Linuxの対応するデータグラムソケットでは、実際にコピーできた長さではなく、元のデータグラムの長さを戻り値で知るための指定です。同じ名前でも、「結果として切り詰めを知らせる印」と「呼出し時に長さの返し方を指定する指示」を区別します。

この入力フラグを使って6という長さが返っても、4バイトの領域に6バイトが保存されたという意味ではありません。コピーされた内容と元の長さを混同して、領域の外まで読み進めないようにします。また、長さが分かっただけで、既に捨てられた末尾を回復できるわけでもありません。

領域の長さと受信した長さが同じ、という情報だけでも判断は足りません。今回の4バイト領域には、ちょうど4バイトのメッセージも、もっと大きなメッセージの先頭も収まります。結果の切り詰めフラグを確認できるAPIを選ぶと、この二つを区別できます。言語のラッパーがどの情報を公開するかも確認対象です。

ゼロバイトと待ち状態も、同じ意味にしない

UDPでは、長さがゼロのデータグラムを受信する場合があります。したがって、受信結果の0を見ただけで、ストリームの正常終了と同じ意味だと決めることはできません。ソケットの種類と、その呼出しで指定した条件を一緒に読みます。

受信待ちのデータがない非ブロッキング操作では、EAGAINやEWOULDBLOCKというエラー条件もあります。これは、正常に受け取った空のメッセージとは別です。アプリケーションの状態表には「完全なデータグラム」「切り詰め」「空のデータグラム」「今はデータなし」を分けて置くと、再試行や破棄の理由が曖昧になりにくくなります。

受信先の領域を増やすだけで解決するか

今回不足していたのは、アプリケーションが一回の受信に渡した領域です。カーネル側で受信待ちのデータを蓄える容量や、経路上で扱えるパケットの大きさとは、同じものではありません。一つの設定値を大きくして、三つの問題がまとめて解消すると考えないようにします。

受信側での比較材料は、通信規約が許す最大長、その長さを収める領域、切り詰めた際の扱いです。部分的なJSONや固定長の計測記録を通常のデータとして処理する前に、完全なメッセージだったかを確かめます。再送が必要なら、相手へ何を要求するか、同じメッセージをどう識別するかも通信規約に含めます。

MSG_WAITALLを使えば複数回分をつないで埋められる、という解決策もUDPには当てはまりません。Linuxの資料では、この指定はデータグラムソケットに効果がないとされています。受信方法の契約を変えずに、バッファだけを満たす発想へ寄せないことが大切です。

なお、UDP_GROを明示的に使う構成では、複数データグラム分をまとめた領域と、区切りを表す補助情報を受け取る場合があります。本稿の通常受信モデルを、その設定へ無条件に広げることはできません。まず有効なソケットオプションを確認し、その上でアプリケーションが受け取る単位を追います。

出典・参考資料