TT Lab
はじめる
学ぶ 学習パス コース

TCPの荷物がばらばらに届いた

半閉鎖した接続に最後の応答を返す

TT Labで続きを見る

一言でいうと

送信呼び出しの成功、相手がメッセージを受け取った事実、相手の業務が完了した事実は、それぞれ別です。

なぜ必要なのか

sendが3を返したのに、送ろうとしたフレームは20バイトでした。例外がなかったからと完了として記録すると、残りの17バイトは捨てられます。自分で繰り返すときは、返されたサイズの分だけ進める必要があります。sendが0を返したら、進展がないので、終了エラーとして扱う必要があり、そのまま繰り返すと無限ループになります。Pythonのsendallを使えば、全部送るか例外かというAPIで、この繰り返しを任せられます。

だからといって、sendallが「相手の注文処理の完了」を意味するわけではありません。ローカルの送信経路にバイトを渡すことと、リモートのプログラムがフレームを解釈することは別です。このコースでは、実際のエコーの応答が返ってくるかを確認します。エコーでさえ、決済・保存のような業務の完了を意味するわけではありません。決済のリトライと冪等性は、別の契約が必要です。

どう動くのか

リスニングソケットは、接続を受け付けます。acceptが返した接続ソケットが、データを読み書きします。最後の検査器は、127.0.0.1とポート0でリスニングソケットを作り、オペレーティングシステムが選んだポートをクライアントに渡します。特定のポートが空いていると仮定しないので、他の試験と衝突しにくくなります。loopbackはこのPodの内部であり、外部のインターネットの許可は必要ありません。

学習者のhandle_connectionは、接続ソケットを引き継ぎます。1つのフレームを読み、同じbytesを応答することを繰り返します。Noneなら、新しいフレームなしで正常終了したということです。b""は長さ0の有効なメッセージなので、そのままフレームを作って返します。if not payloadと書くと、この2つを混同して、空のメッセージの後のデータまで失います。値の真偽の代わりに、APIが定義した終了の目印を比較してください。

クライアントは複数のフレームを送った後、shutdown(SHUT_WR)を呼びます。これでもう送りませんが、応答は引き続き受け取れます。サーバーは、すでに受け取ったフレームの応答を送った後、正常なEOFに出会って閉じます。ソケットが双方向の通信であるため、「相手が送り終えた」と「自分が応答できない」は同じではありません。半閉鎖を理解していないと、最後の応答を捨てるバグが生まれます。

with sockは、成功と例外の両方で、接続ソケットを閉じます。呼び出し側が所有していたリスニングソケットまで閉じるわけではありません。どの関数がどのソケットの寿命を所有するかを、インターフェースに書く理由です。開いたファイルディスクリプターは、限られたリソースです。例外のときだけ漏れるコードは、正常なリクエストの試験では問題なくても、障害が繰り返されるとリソースを枯らします。

送信の時間も重要です。このコースのsend_frameは、呼び出し側が決めたソケットのtimeoutを使います。send_frame自体が、新しい送信予算を決めることはありません。recv_frameは、自分の作業が終わると既存の値を復元するので、サーバーの呼び出し側は、接続を渡す前に、全体のポリシーに合ったデフォルトのtimeoutを設定する必要があります。受信の時間制限を実装したからといって、すべての送信待ちまで制限したことにはなりません。複数のクライアントの遅い消費者の問題とキューの上限は、次の段階で別に扱います。

現場での姿

単体試験は、正確に切った断片をパーサーに与えられます。実際のTCPの試験は、connect・accept・半閉鎖・ソケットの回収まで確認しますが、recvの断片のサイズを強制することはできません。2つの試験を一緒に残してこそ、境界の計算と実際の通信の両方を証明できます。このローカルのエコーのラボで、TLS、認証、インターネットの遅延、パケット損失からの復旧、大規模なスループットを検証したとは主張しません。

クライアントの観測を「リクエストの送信を終えた」「レスポンスヘッダーを受け取った」「レスポンス本文が完成した」「正常なEOFを受け取った」に分ければ、失敗した場所を絞れます。単に接続成功というログ1つだけを残すと、サーバーがリクエストを処理したかわかりません。ログには、メッセージIDやバイトの長さのように必要な情報だけを残し、実際のユーザー本文を無条件に出力しない習慣も重要です。このコースの試験データは、機密情報のない自前の例です。

レスポンスが来る前に接続が切れると、サーバーが業務をしなかったのか、業務はしたがレスポンスだけを失ったのか、クライアントには区別しにくくなります。そのため、実際の注文システムのリトライには、同じ作業を識別するキーと、結果の照会ルールが必要です。バイトを再送するのは簡単ですが、業務を重複して処理しないことは、別の設計です。エコープログラムを完成させた後、APIの冪等性のコースにつなげられる理由です。

最後の検査器は、1つの接続を担当するhandlerを実行します。これは、無制限の同時接続のサーバーの設計ではありません。接続をスレッドに分けるかイベントループで扱うか、待ち行列と接続数をどう制限するかは、次のコースで、実際の負荷とともに比べます。単一の接続で、境界と後始末が間違った状態のまま並行性から増やすと、同じエラーがより複雑な形で現れるので、まず小さな契約を正確に作ります。

次のラボですること

検査器は、小さなフレーム・空のフレーム・UTF-8の本文を1つの接続で送り、送信を半閉鎖します。応答のバイトが完全に一致するかと、サーバーの終了後にソケットが閉じているかを確認します。学習者の関数は実際に実行されるので、出力だけをそれらしく書いたファイルでは通りません。試験の時間を超えたら、無限の待機から点検してください。一時的なラボのセッションが終わるとファイルは保持されないので、必要なコードは、終了前に別に保管します。