通信の先頭にある数バイトを見て、どの処理へ渡すか決めたい。ただ、その数バイトは後で動く処理にも読んでほしい。受信を一度進めてしまうと、読み取った部分を自分で保存して引き渡す必要があります。ソケットの受信に指定するMSG_PEEKは、データを受信待ちの列へ残したまま、内容を読むためのフラグです。
簡単にまとめると?
- 通常のMSG_PEEKは、受信待ちの先頭を消費せずに読みます。繰り返しても、自動的に次の部分へ進むわけではありません。
- 先に覗いても、そのデータを自分用に確保したことにはなりません。別の処理が受信すると、後の読み取り位置は変わります。
- 読める状態、今はデータがない状態、送信終了後のEOFを区別します。覗くだけの処理を繰り返すと、受信待ちのデータが残り続けます。
「内容が返る」と「列から取り出す」を分ける
ソケットは、通信の相手とデータをやり取りするための窓口です。届いたデータのうち、アプリケーションがまだ受け取っていない部分は、OS側で読み取りを待っています。通常のrecvはそこからデータを受け取り、読み終えた分だけ先へ進みます。一方、MSG_PEEKを付けたrecvでは、内容が返ってもその部分が受信待ちの列に残ります。[1]
Pythonならsock.recv(2, socket.MSG_PEEK)という形です。2は欲しいデータの上限で、結果はbytesとして返ります。フラグの有無で変わるのは受信待ちのデータの扱いです。「見るだけ」という名前でも、アプリケーションへ内容を渡さずに済む機能や、コピーの費用がなくなる指定ではありません。[2]
ここからは、データを連続したバイト列として扱うストリームソケットで考えます。先頭位置を変更する特別なソケットオプションは使わず、受信サイズは1以上にします。データグラムの区切りや長さゼロのメッセージは、今回の例に含めません。
ABを見た後も、次はABから読める
受信待ちの列にABCDEFが届いたとします。最初に2バイトを覗くとAB、続いて4バイトを覗くとABCDが返ります。最初の操作でABを消費していないため、2回目も同じ先頭から始まるのです。
そこで通常のrecvで2バイトを受け取るとABが返り、初めて受信待ちの先頭がCへ移ります。その後に4バイトを覗けばCDEFです。覗き読みを何回実行したかではなく、どの操作が実際にデータを消費したかを追うと、位置を見失いにくくなります。

この順序は、macOS 26.6.2・Python 3.12.14のsocketpairで作ったローカルのAF_UNIX / SOCK_STREAMソケットで3回実行し、いずれも同じ結果になりました。通常のrecvで残りのCDEFを受け取るまで、列には未読のデータが残りました。Linuxの仕様説明と、このMacでの観測は分けています。TCPのパケット分割や通信速度を測った実験ではありません。
覗いた内容が、後でも自分に届くとは限らない
先頭を見た直後に別の処理が同じ受信側から読むと、状況は変わります。今回の別の試行では、ABCDを送り、最初の読み手がABを覗いた後、複製したソケットを持つ読み手に通常のrecvでABを受け取らせました。最初の読み手が改めてrecvすると、返ったのはCDでした。
この試行は、操作の順序を意図的に固定したものです。同時実行の競合がどの頻度で起きるかは測っていません。ただ、覗き読みがデータの予約や占有にならないことは、この順序でも確かめられます。複数の読み手を置く設計では、「先ほど見た先頭を、この後の処理が必ず受け取る」という前提を置けません。
たとえば、先頭の識別子を見て処理を選ぶ部分と、本体を読み取る部分を分けるなら、誰が受信を進めるかを一つの流れとして管理します。読み手が一つならMSG_PEEKで先頭を残す方法が使えます。複数の段階で受信内容を共有したいなら、一つの読み手が受信してアプリケーション内のバッファへ保存し、必要な部分を各処理へ渡す構成も比較対象になります。
読めるという通知は、覗いても消えない
受信できるようになるまで待つ仕組みとして、selectなどがあります。Linuxのselectの説明でいう「読み取り可能」は、その読み取りが待たずに進められる状態です。届いたデータがある場合だけでなく、EOFを返せる場合も含まれます。[3]
今回のMacで待ち時間を0にしたselectを呼ぶと、ABを覗いた後も読み取り可能でした。通常のrecvでABを消費してもCDEFが残る間は同じです。残りをすべて受け取り、相手がまだ開いている状態になって初めて、読み取り可能ではなくなりました。
この違いは、受信通知を受けた処理が覗くだけで戻るときに効いてきます。未読データを残したまま、読めるかどうかを何度も調べれば、同じデータに対して処理を繰り返すことになります。先頭の判定後は誰が消費するのか、まだ必要な長さがないならどう待つのかまで決めておく必要があります。特定のイベント通知方式すべてが同じ動作をする、という比較ではありません。
短い結果とEOFを、必要な長さと一緒に読む
MSG_PEEKは、指定したバイト数が必ずそろうまで待つという指示でもありません。Linuxの通常の受信は、要求した上限までの範囲で、その時点で返せるデータを返します。[1] 今回も、XYを送って送信側を終了させた試行では、8バイトを覗く指定に対して返ったのは2バイトのXYでした。先頭4バイトに長さを持つ通信規約なら、2バイトの結果だけで長さを解釈してはいけません。
さらに、何も届いておらず相手も送信を終えていない場合と、送信終了後に未読データを読み切った場合は別です。非ブロッキングにした今回のPythonの受信側では、前者の覗き読みはBlockingIOErrorになりました。後者では、1バイトを要求しても空のbytesが返りました。こちらが、このストリームでのEOFです。
相手が送信を終えていても、XYが未読なら最初のpeekはXYを返します。通常のrecvでそれを受け取った後、次のpeekが空になりました。終了の有無だけでなく、まだ読んでいないデータが残っているかも結果へ表れます。長さ0を要求した呼出しや、空のUDPデータグラムまでこのEOF判定へ含めることはできません。
MSG_PEEKを選ぶ判断材料は、「先頭を残す必要があるか」「受信を進める読み手は誰か」「判定に必要な長さが足りないときどうするか」です。単に受信済みの情報を何度も参照したいのであれば、受信後のバッファを参照する方法でも目的を満たせます。OS側の列に残したい理由があるときに、覗き読みの役割がはっきりします。
出典・参考資料
- [1] Linux man-pages 6.19 — recv(2):DESCRIPTION、MSG_PEEK、RETURN VALUE。ページ日付2025年10月29日。通常のフラグ設定での説明を参照。
- [2] Python 3.12.15 documentation — socket:socketpair、recv、dup、setblocking。呼出し先のOSによる差がある点を含む。
- [3] Linux man-pages 6.19 — select(2):DESCRIPTION、readfds。文書と実験結果は2026年10月25日に確認。図は上記のローカル実験から作成しました。
