受信終了と応答完了は別の出来事
一言でいうと
相手が送り終えたからといって、自分が返信を送れなくなるわけではありません。受信EOFと送信キューの消化を、別々の状態として管理する必要があります。
なぜ必要なのか
前のラボの行単位サーバーは、EOFに出会うとclosedを記録してソケットを回収しました。クライアントがリクエストとレスポンスをやり取りしたあとで接続を閉じる範囲では、わかりやすい出発点です。しかし、リクエストを全部送ったあとで自分の送信方向だけを閉じ、結果を待つクライアントまでサポートするには、このポリシーを拡張する必要があります。多重化そのものが壊れたのではなく、終了を1つのブール値だけで表したモデルの限界です。
今回は、おもちゃの「大文字印刷所」を作ります。お客さんは原稿のバイトを送り、送信方向を閉じます。印刷所は、EOFを原稿の終わりとして、ASCIIの小文字を大文字に変えた返信を送ります。原稿を渡したお客さんが口を閉じたからといって、耳まで閉じたと判断すると、返信を捨ててしまいます。ラボ用のプロトコルは、接続あたり1リクエストで、HTTP、TLS、永続接続プロトコルの代わりにはなりません。
どう動くのか
shutdown(2)は、読み取りと書き込みのうち、終了する方向を区別します。クライアントのshutdown(socket.SHUT_WR)は、これ以上送らないという意味で、同じソケットで応答を読み続けることはできます。ローカルのファイル記述子を回収するcloseとは、役割が違います。closeしたソケットでまたrecvしようとする例は、半閉鎖をテストしていません。
recv(2)のストリームEOFの戻り値は0です。Pythonでは、正のサイズで要求したrecvがb""を返すことで観察します。UDPの長さ0のデータグラムやrecv(0)まで、同じ規則で一般化してはいけません。特に、入力バッファーが上限まで埋まったからとrecv(0)を呼ぶと、原稿が終わったという証拠なしに、終わったと判定してしまうことがあります。ラボは、最大許容量より1バイト多く観察できる余地を置いて、EOFなのか超過なのかを区別します。
| 状態 | 関心イベント | 次の行動 |
|---|---|---|
| 原稿を受け取り中 | READ | 最大4096バイトを1回読みます |
| EOF、返信が残っている | WRITE | 1回送り、実際に出た長さだけ消します |
| 返信がすべてキューから出た | なし | 登録を解除してからソケットを閉じます |
| サイズ超過・エラー・締め切り | なし | 理由を保存し、バッファーとソケットを回収します |
EOFは、読み続けてみるイベントではありません。返信が残っているからとREADを登録したままにしておくと、EOFを繰り返し観察して、ループが空回りすることがあります。逆に、EOFでWRITEまで消すと、送るべき返信が永遠に進めなくなります。関心マスクは、「接続が生きているか」の1つではなく、受信状態と残りの出力で計算します。
send(2)の成功は、相手のアプリケーションが返信を読んで処理したという確認ではありません。ラボのcompleteは、ローカルの出力キューを空にしたという意味に限定します。取引の完了のように、相手の処理まで確認が必要なシステムには、別の応答やアプリケーション確認メッセージが必要です。採点ツールが、completeが残ったという事実だけを見るのではなく、実際のクライアントが受け取ったバイトまで突き合わせる理由です。
メモリ上限と締め切りは別々の防御
原稿の上限を4096バイトにしても、1バイトずつゆっくり送るお客さんは、接続を長く占有できます。活動のたびに延長するidle timeoutだけでは、生き残り続けることもあります。ここでは、開始時にtime.monotonic() + 수명(プレースホルダーは寿命です)で絶対的な締め切りを決め、バイトを受け取ってもEOFを見ても延長しません。EOFのあと、相手が返信を読まずに送信が塞がるときも、同じ締め切りを適用します。運用では、リクエストの受信・処理・応答の送信の締め切りを別々に決めることもできますが、今回のモデルは、1つの総寿命で責任を終えます。
上限超過は、巨大なエラー応答として返さず、閉じます。小さな変換サーバーに対する学習用のポリシーで、本番のプロトコルでは、エラーフレームと未受信の入力の処理まで、別に設計する必要があります。1つの接続の入力と出力がそれぞれ有限でも、接続数が無制限なら、総メモリは有限ではありません。したがって、このラボだけで、サービス全体のリソース保護が終わったとは言えません。
所有権を終えるのは1か所
Peer.finishは、終了理由とバッファーの状態だけを変え、ソケットを直接閉じません。Python selectorsが案内する順序のとおりに、ループ側でunregisterしてからcloseします。先に閉じると、登録リストにすでに無効になった記述子が残り、例外やリーク分析を難しくします。最後のfinallyも、正常に完了した場合だけでなく、途中の例外でもソケットを回収するために必要です。
現場での姿
2026-09-13に確認したCloudflare Spectrumの求人情報は、Linuxソケット、TCP接続状態、接続ライフサイクルの問題のデバッグと、運用障害の分析を求めています。このモジュールは、そのうち、終了方向・リソース回収・境界条件の検証を練習します。企業の公式教育や採用対策を保証するコースではなく、GoやRust、および大規模分散システムの要求まで、このラボ1つで満たせるわけではありません。
次のラボですること
ステップ8でPeerの状態と多重化ループを作ります。最後の検査は、実際のTCP接続4つを開きます。最初のお客さんは沈黙し、次のお客さんは原稿を分けて送ったあとSHUT_WRだけを呼び出し、残りは空のリクエストと上限超過を作ります。正常なお客さんが、沈黙したお客さんの終了を待たずに全体の返信を受け取るか、すべてのソケットが回収されるかを確認します。部分sendとEAGAINは、別の決定的なテストで強制します。小さな応答がたまたまうまく届けられたという観察だけで、部分送信の分岐が検証されたとは言いません。