壊れた荷物の境界を復元する
目標
フレーミング・期限・半閉鎖を実装し、実際のTCPで確認します。
なぜ重要なのか
受け取った断片を1つのメッセージだと信じると、ローカルでは成功しても、実際の入力で壊れます。長さと全体の時間予算、ソケットの所有権をコードで表現して、データの欠落と無限の待機を区別します。標準ライブラリだけを使い、インターネット接続やインストールは必要ありません。
ステップ
- 封筒には文字数ではなくバイト数。codec.pyにMAX_PAYLOAD=4096とencode(payload)を作ってください。bytesの本文の前に、unsignedの4バイトbig-endianの長さを付けて、bytesを返します。空の本文は許可し、bytesでない場合や4096バイトを超える場合はValueErrorです。
- ヘッダーも半分に切れることがある。codec.pyにDecoderを追加してください。新しいインスタンスごとに受信状態が分離され、feed(chunk: bytes)は、完成した本文のlist[bytes]を返します。1つのフレームを、どのバイトで切って2回与えても復元し、未完成のデータは次の呼び出しまで保管します。
- 荷物3つが1つの袋で届いた。Decoder.feedを改善して、1つのchunkに複数のフレームがあれば、順番にすべて返してください。次のフレームの一部が残れば保管します。空の本文もメッセージ1つであり、空のchunkはEOFではありません。
- 到着完了と配送中の破損。Decoder.finish()を作ってください。未完成のヘッダーや本文があればEOFError、残りがなければNoneを返します。正常に解釈した長さ0の本文を、途中終了と誤解しないでください。
- 4 GiBの荷物の予告状を拒否する。Decoderは、4096より大きい長さのヘッダーを受け取った時点で、すぐValueErrorを出す必要があります。実際の本文が到着するまで待ったり、その長さのバッファを先に確保したりしないでください。4096バイトの本文は、引き続き許可します。エラーの後、その接続は破棄します。
- 1文字ずつ送られたら永遠に待つのか。transport.pyにrecv_frame(sock, timeout, clock=None)を実装してください。1つのフレームのヘッダーと本文全体が、同じ時間予算を共有します。clockは引数なしの、秒単位の時計関数で、デフォルトはtime.monotonicです。sockのgettimeout/settimeout/recvだけを使います。正常なフレームはbytes、新しいヘッダーを1バイトも受け取る前のEOFはNone、途中のEOFはEOFError、超過した長さはValueError、期限超過はTimeoutErrorです。正の有限なtimeoutだけを許可し、成功・失敗の両方で、以前のソケットのtimeoutを復元してください。
- 送ったという言葉は半分しか事実ではない。transport.pyにsend_frame(sock, payload)を追加してください。encodeした完全なフレームを送り、送信エラーを隠しません。sendが一部のバイトだけを処理する場合も、フレームを欠落させてはいけません。ソケットの送信timeoutは、呼び出し側が決めます。
- 実際の接続で返送伝票を受け取る。server.pyにhandle_connection(sock, timeout)を作ってください。受け取ったソケットの所有権を引き受け、フレームごとに同じbytesをエコーします。recv_frameの正常なEOF(None)で終了しますが、b""の本文はエコーします。すべての終了経路でソケットを閉じ、プロトコルエラーは呼び出し側に伝えてください。検査器が127.0.0.1の一時ポートで実際のTCP接続を作り、クライアントが送信だけを半閉鎖した後も、残りの応答を受け取る必要があります。
参考
すべてのファイルは/root/tcp-parcelの下に置きます。例のファイルには、正解ではない関数の枠があります。前のステップの関数も、引き続き維持してください。採点は提出されたコードを実行し、12秒を超えると中断します。send_frameの送信timeoutは、呼び出し側が決めます。ラボのセッション終了後、ファイルは保持されないので、別に保管してください。
封筒には文字数ではなくバイト数
codec.pyにMAX_PAYLOAD=4096とencode(payload)を作ってください。bytesの本文の前に、unsignedの4バイトbig-endianの長さを付けて、bytesを返します。空の本文は許可し、bytesでない場合や4096バイトを超える場合はValueErrorです。
struct.packの!Iは、ネットワークオーダーの32ビット符号なし整数です。UTF-8への変換は、呼び出し側が先に行います。
ヘッダーも半分に切れることがある
codec.pyにDecoderを追加してください。新しいインスタンスごとに受信状態が分離され、feed(chunk: bytes)は、完成した本文のlist[bytes]を返します。1つのフレームを、どのバイトで切って2回与えても復元し、未完成のデータは次の呼び出しまで保管します。
最低4バイトが集まる前に、ヘッダーを解釈しないでください。長さがわかっても、本文全体がまだないことがあります。
荷物3つが1つの袋で届いた
Decoder.feedを改善して、1つのchunkに複数のフレームがあれば、順番にすべて返してください。次のフレームの一部が残れば保管します。空の本文もメッセージ1つであり、空のchunkはEOFではありません。
完成したフレームを取り除いた後も、whileで次のヘッダーを検査してください。feedのたびにDecoderを新しく作ると、残りが消えます。
到着完了と配送中の破損
Decoder.finish()を作ってください。未完成のヘッダーや本文があればEOFError、残りがなければNoneを返します。正常に解釈した長さ0の本文を、途中終了と誤解しないでください。
接続の終了という外部の出来事と、今回受け取ったバイトがないというだけの入力は別です。finishは、実際にEOFを知ったときに呼びます。
4 GiBの荷物の予告状を拒否する
Decoderは、4096より大きい長さのヘッダーを受け取った時点で、すぐValueErrorを出す必要があります。実際の本文が到着するまで待ったり、その長さのバッファを先に確保したりしないでください。4096バイトの本文は、引き続き許可します。エラーの後、その接続は破棄します。
ヘッダーを読んだ直後に長さを検査してください。32ビット整数で表現できるという事実と、サービスが許可するということは別です。
1文字ずつ送られたら永遠に待つのか
transport.pyにrecv_frame(sock, timeout, clock=None)を実装してください。1つのフレームのヘッダーと本文全体が、同じ時間予算を共有します。clockは引数なしの、秒単位の時計関数で、デフォルトはtime.monotonicです。sockのgettimeout/settimeout/recvだけを使います。正常なフレームはbytes、新しいヘッダーを1バイトも受け取る前のEOFはNone、途中のEOFはEOFError、超過した長さはValueError、期限超過はTimeoutErrorです。正の有限なtimeoutだけを許可し、成功・失敗の両方で、以前のソケットのtimeoutを復元してください。
開始時にdeadline=clock()+timeoutを1回決めて、recvの前ごとに残りの時間をsettimeoutに与えます。recv(n)は、nバイトをすべて渡すという約束ではありません。
送ったという言葉は半分しか事実ではない
transport.pyにsend_frame(sock, payload)を追加してください。encodeした完全なフレームを送り、送信エラーを隠しません。sendが一部のバイトだけを処理する場合も、フレームを欠落させてはいけません。ソケットの送信timeoutは、呼び出し側が決めます。
sendallは、すべて送るか、例外を出します。自分で繰り返すなら、sendの返した長さの分だけ前に進み、0の返却を終了エラーとして扱ってください。
実際の接続で返送伝票を受け取る
server.pyにhandle_connection(sock, timeout)を作ってください。受け取ったソケットの所有権を引き受け、フレームごとに同じbytesをエコーします。recv_frameの正常なEOF(None)で終了しますが、b""の本文はエコーします。すべての終了経路でソケットを閉じ、プロトコルエラーは呼び出し側に伝えてください。検査器が127.0.0.1の一時ポートで実際のTCP接続を作り、クライアントが送信だけを半閉鎖した後も、残りの応答を受け取る必要があります。
with sockで寿命を結び付けてください。if not payloadは、Noneと空の本文を混同します。送信方向だけを閉じた相手に、まだ応答できます。