9.10. TCP -- 信頼性の高いバイトストリーム

TCP(Transmission Control Protocol)は、IP の上に位置するもう一方のトランスポート層サービスです。UDP が「パケットを送って祈る」と表現できるのに対し、TCP は「2 つのエンドポイント間に接続を開き、相手側が確実に、順序どおりに、かつ正確に 1 回だけ受け取る双方向のバイトのパイプとして扱う」ものです。インターネットのトラフィックの大半はこれを使っており、カメラがネットワーク上で行うことの大半もこれを使います。

9.10.1. TCP が IP に追加するもの

TCP は UDP よりもはるかに多くのことを行います。次のものを追加します。

  • 接続。 データが流れ始める前に、2 つのエンドポイントは短いハンドシェイクを交換して通信していることに合意します。接続には「open」「half-closed」「closed」といった状態があり、両側がそれを追跡します。

  • 信頼性のある配信。 送信側が投入したすべてのバイトは受信側によって確認応答されます。タイムアウト内に確認応答されなかったものは再送されます。アプリケーションがバイトの損失を目にすることは決してなく、プロトコルが再送する間の遅延として見えるだけです。

  • 順序どおりの配信。 バイトは送信された順序と同じ順序で到着します。たとえパケットが受信側に順序どおりでなく到着しても、TCP はアプリケーションが読み取る前にそれらを並べ替えます。

  • フロー制御。 受信側が遅い場合、送信側は速度を落とすよう指示されます。接続は遅い側の速度に適応します。

  • 輻輳制御。 途中のネットワークがパケットをドロップし始めると、送信側は状況が回復するまで送信を控えます。これにより、1 つの接続が飽和したリンクをダウンさせることを防ぎます。

これらはすべて自動です。アプリケーションが使う Python API は単に send(bytes)recv(n) だけで、それ以外はすべて TCP が内部で処理します。

9.10.2. ハンドシェイク

TCP 接続は、データの通過を許す前に 3 ウェイの交換で開かれます。

「client」と「server」というラベルの付いた 2 つの列を持つ図。client から server へ「SYN」とラベル付けされた矢印、続いて server から client へ「SYN-ACK」とラベル付けされた矢印、続いて client から server へ「ACK」とラベル付けされた矢印。その下に「data flowing both ways」とラベル付けされた太い矢印。

3 ウェイハンドシェイク。両側が確認応答すると接続が開かれ、データが流れることができます。

クライアントは接続を開くことを求める SYN(synchronise)パケットを送信します。サーバーは SYN-ACK(synchronise + acknowledge)で応答し、受け入れます。クライアントは確認のために最後の ACK を送信します。このラウンドトリップの後、両側が接続が開いていることに合意し、どのバイトが送受信されたかを追跡するためのカウンターを同期します。

ハンドシェイクには、最初の有用なバイトが通過するまでに 1 ラウンドトリップ時間のレイテンシのコストがかかります。ローカルネットワークではそれは 1 ミリ秒ですが、大陸間の接続ではおよそ 100 ms になります。これが TCP の主なコストであり、短い 1 回限りのメッセージが UDP を使ったほうがよい場合がある理由です。

9.10.2.1. クローズのハンドシェイク

TCP 接続もまた交換(各側からの FIN)でクローズされます。どちらの端も自分側の接続を独立してクローズできます。一方が送信を終えたがもう一方がまだ通信している half-closed 状態は合法ですが、まれです。アプリケーションは通常、単に close() を呼び出し、シャットダウンシーケンスはプロトコルに処理させます。

9.10.3. TCP が保証 しない もの

TCP が行うと思われがちだが実際には行わないことがいくつかあります。

  • メッセージの境界。 アプリケーションは バイトのストリーム を送るのであって、メッセージのストリームを送るのではありません。2 回の send(b"hello") 呼び出しは、b"hellohello" という 1 回の recv() として到着するかもしれませんし、さまざまなサイズの 2 回の recv()として到着するかもしれません。アプリケーションがメッセージのフレーミングを望むなら、フレーミング(改行、長さプレフィックス、なんであれ)を自分で追加する必要があります。たとえば TCP 上で JSON ドキュメントを送る場合、各ドキュメントを改行や何らかのマーカーで区切る必要があります。

  • 暗号化。 TCP はアプリケーションが渡したバイトを、平文のまま、最後まで運びます。内容を秘匿する必要がある場合、アプリケーションは接続を TLS でラップする必要があります(暗号化ソケットとTLS を参照)。

  • 認証。 TCP はバイトが無傷で到着したことを保証します。誰が それを送ったかについては何も述べません。認証もまた上位層の関心事です。

  • 静かな接続での生存確認。 どちらの側も長時間データを送らない場合、接続は技術的にはまだ開いていますが、相手側がクラッシュしたり消失したりしたことを検出できません。これが問題になる場合は、ソケットで Keepalive プローブを有効にしてこれを解決できます。

9.10.4. TCP を使うべき場合

TCP は、「クライアントがサーバーへの接続を開き、いくつかのバイトを交換し、完了したら接続をクローズする」という形に当てはまるほぼすべての通信に対する正しい答えです。HTTP と HTTPS のリクエスト、SSH セッション、データベースクエリ、ファイル転送、画像アップロード -- すべて TCP 上で行われます。

UDP に頼るのは、通信がその形に当てはまらない場合だけにしましょう。損失が許容できる独立した自己完結型のメッセージ、マルチキャストトラフィック、時刻同期、名前解決、あるいはハンドシェイクのコストが許容できないほどレイテンシに敏感な極端なケースなどです。

ポート、UDP、TCP がすべて出そろったことで、トランスポート層の話は完了です。その両方を公開する Python API は次のページにあります。ソケットオブジェクト