依頼のデータを送り終えたので、相手に終わりを伝えたい。しかし、相手が返す結果はこれから受け取りたい。このとき、通信全体を閉じる操作と、自分から送る方向だけを終える操作は区別する必要があります。
ソケットのshutdownと半閉鎖は、この違いを扱う仕組みです。ここでは同じ計算機内のAF_UNIX・SOCK_STREAMのソケット対を使い、送信の終了、受信で見えるEOF、記述子のcloseを分けて考えます。Linuxの仕様説明と、手元のmacOSで実行した小さな観測例を区別して示します。
簡単にまとめると?
- shutdownのSHUT_WRは、そのソケットからの以後の送信を止めます。受信する方向まで止める指定ではありません。
- 正の長さを要求するストリーム受信では、相手の正常な送信終了後、残るデータを読み終えるとEOFを確認できます。「今データがない」とは違います。
- closeは記述子を閉じる操作です。依頼の終端を知らせながら返答を待つ設計では、送信終了と記述子の解放を別の段階にします。
二つの方向を持つ接続として見る
socketpairは、既につながった名前のないソケットを二つ作り、それぞれを参照するファイル記述子を返します。[1] AF_UNIXは同じ計算機内の通信に使う方式で、ここではバイト列を扱うSOCK_STREAMを選びます。[2] 図では二つの端をAとBと呼び、AからBへ送る方向と、BからAへ返す方向を分けます。
たとえばAが検査してほしい文章を渡し、Bが最後まで受け取ってから結果を返す処理を考えます。Aが何もせず返答を待ち、Bが入力の終わりを待ち続けると、互いに相手の次の動きを待つ構成になり得ます。本文の長さや終端を別途取り決めるか、送信方向の終了で入力の終わりを示すかを、通信手順として決めておく必要があります。
SHUT_WRで止めるのは、自分から送る方向
shutdownの指定には、受信を止めるSHUT_RD、送信を止めるSHUT_WR、両方を止めるSHUT_RDWRがあります。[3] AがSHUT_WRを成功させた場合、この接続についてAがさらに依頼を送り足す段階は終わります。一方で、Bから届く結果をAが受け取る方向は残せます。
これが、片方向だけを閉じる「半閉鎖」と呼ばれる考え方です。Aが送る方向の終了を、Bが送る方向の終了へ読み替えてはいけません。両端の状態を一つの「接続済み/切断済み」だけで表すと、どちらからのデータがまだ来るのかを見失いやすくなります。

7バイトを受け終えてから、2バイトを返す
手元の例では、Pythonのsocketpairで二つの端を作り、AからASCIIのseaとwaveを順に送信しました。内容は合計7バイトです。続いてAがSHUT_WRを行い、Bは一度に最大2バイトずつ受信して、EOFまで読み続けます。Bは受け取った内容がseawaveであることを確認し、OKという2バイトを返した後、自分の送信方向を終えます。
この実行では、Bの受信片はse、aw、av、eとなり、その後に空のバイト列を受け取りました。Aは自分の送信を終えた後でもOKを受信できました。ここで確認したのは、この順序で動く小さなローカル例です。macOS上の結果を、Linuxカーネルで試験した証拠や、全ソケット方式で同じ動作になる保証にはしません。
また、最初にseaとwaveへ分けて送った境目は、2バイトずつの受信片の境目と一致していません。今回の主題は分割数ではなく終了の伝え方ですが、データの一部が届いたことと、依頼全体を受け取ったことも分けて追う必要があります。図の7と2は独自例のバイト数であり、通信性能の比較値ではありません。
EOFと、今は受け取れない状態を混同しない
Linuxのrecvの説明では、ストリームの相手が正常に送信を終えた場合、受信は0を返します。一方、データがまだなく非ブロッキング受信が待つ必要のある場合は、EAGAINまたはEWOULDBLOCKで失敗します。[4] EOFはその方向の正常な終了を示す観測であり、待てば次が来るかもしれない一時的な空きとは違います。
ただし、0という数だけでは判断できません。受信を要求した長さ自体が0の場合や、長さ0のデータグラムを受信した場合にも0が返り得ます。本稿の説明は、正の長さを要求するストリーム受信を前提にしています。ソケットの種類、要求した長さ、戻り値、エラーを一緒に記録して初めて、終端として扱える条件を確認できます。
同様に、通信がエラーで途切れたことを、正常に全て届いたことへ変換してはいけません。受信の終了を観測しても、送られた内容がアプリの期待する完全な依頼かは別の検査です。件数、形式、必要な項目など、アプリ側の受入条件も用意します。
closeは、最後の片付けとして別に考える
closeは指定したファイル記述子を閉じ、その番号を再利用可能にする操作です。同じ実体への別の参照がある場合もあり、一つの記述子を閉じることと、全ての参照をなくすことも同一ではありません。[5] この例では、AとBが必要な受信まで終えた後で、両方の記述子を閉じました。
依頼を送った直後にAの記述子をcloseすると、その記述子を使って返答を読むことはできません。返答が必要な設計なら、送信の終了を通知する段階と、受信を終えて資源を解放する段階を分けます。逆に、通信方式や上位の手順が半閉鎖を想定していないなら、SHUT_WRを付ければ解決すると決めつけず、その方式の終了規則を確認します。
shutdownはアプリの処理成功を確認する命令でもありません。依頼を出し終えたこと、相手が終端まで読んだこと、処理結果が届いたこと、記述子を閉じたことを別々に確認します。この四つを分けると、単に「送信完了」と書かれたログから、どこまで進んだのかを読み取りやすくなります。
出典
- Linux man-pages 6.19、socketpair(2)、DESCRIPTION。
- 同、unix(7)、DESCRIPTION。
- 同、shutdown(2)、DESCRIPTIONとRETURN VALUE。
- 同、recv(2)、DESCRIPTIONとRETURN VALUE。
- 同、close(2)、DESCRIPTION。具体的な文字列、手順、図は独自例。
