プロジェクト

全般

プロフィール

TCPについて

TCPについて、TCPを利用するアプリケーションプログラムを設計する観点で調べたことをメモします。

はじめに

TCPは、現在ではHTTPをはじめとする数多くのネットワーク通信の基盤となるプロトコルです。TCPを使ったプログラムを作成するには、大抵はソケットAPIを使用します。ネットワークプログラミングといえばソケットAPIという位メジャーなAPIです。入門本や記事も多く、ソケットAPIを使ってネットワーク通信を行うプログラムを作成するのはそれほど難しくはありません。

ところが、通信プロトコルのTCPの特性を知っているプログラマは少ないので、正常系でうまくいっている(ように見える)間はいいのですが、なにか困ったことが発生したときに解決できず、そのときになってTCPを理解しようとしてその複雑さに驚愕する(あるいは本をぱたっと閉じて見なかったことにする)ことになります。

  • TCPクライアントはコネクションが張れているのに、TCPサーバー側はacceptしていない、あれ?
  • TCPサーバーが動いてなければTCPクライアントからのコネクションはすぐエラーになるのでは、あれ?
  • コネクションが切れたら(途中のネットワークが断絶、相手機器の停止、など)すぐエラーにならないの、あれ?
  • ソケットをクローズしたのにOSのコマンドで調べると残ったままになっているのは、あれ?
  • Keep-Aliveを有効にしているのに相手プログラムが落ちても気付かないのは、あれ?
  • ネットワークパケットを解析していると、ときどきエラーが起きているのは、あれ?
  • ACKが返ってこなかったらどんなタイミングで再送するのかな?
  • ネットワークの帯域はあるはずなのに転送レートが思ったより高くないのは、あれ?
  • 受信したデータサイズが送信したデータサイズより小さい、あれ?

また、ネットワークプログラムを作る際に、通信プロトコルにはTCPと並んでUDPがありますが、このどちらを使うかを設計するには、両者のプロトコルの特性をしっておく必要があります。

  • TCPは信頼性があるから、UDPは使う必要ないよね
  • TCPでも1回コネクションを張ってしまえば、UDPと同等のリアルタイム性の高いデータ送信もできるよね

そこで、ネットワークプログラミングをしているときに生じる可能性のあるTCPの特性、挙動を知っておくことで困った事態を避けられるように、あるいは困った事態に陥っても原因究明と対処ができるようにしていきます。

参考資料

「詳解TCP/IP Vol.1 プロトコル(新装版)」(W・リチャード・スティーヴンス=著、井上尚司=監訳、ピアソン・エデュケーション刊、2000年)
現時点では入手困難、また、内容が古いかもしれません。

TCPとは

書籍「詳解TCP/IP Vol.1」より引用

TCPはコネクション指向の、信頼性のあるバイト・ストリーム・サービスを提供する

TCPの主要特性

  • TCPを利用する2つのアプリケーションがデータを交換する前に相互にTCPコネクションを確立する。
  • アプリケーションデータは、TCPによって通信に最適なサイズに分割される。
  • セグメント(ネットワーク上を流れる1つのパケットに収まるデータ)を送るときにタイマーをセットし、相手からそのセグメントに対する確認応答(ACK)を待つ。タイマーが期限になっても確認応答がなければセグメントを再送する。
  • セグメントを受け取ったら、確認応答を返すが、一般的にはすぐに返すのではなくしばらく(数秒?)遅らせる。
  • ヘッダーとデータとはチェックサムによる検証をする。誤りを検出したら破棄する。
  • セグメントはIPデータグラムとして送る。IPは順序保証をしないし重複もあるので、TCPが受け取ったセグメントを正しい順序で並べ直す。
  • フロー制御を行い、バッファの容量を超えない範囲のデータを受け入れる

コネクション

コネクションの確立

コネクション要求元からSYNを送り、SYN/ackが返ってきたらackを返すことで確立します。(3wayと呼ばれる)
SYNを送ってもSYN/ackが返ってこないと、タイムアウトによる再送をします。一例では、6秒後、30秒後に再送し、75秒後にコネクション確立失敗と判断します。

 A         B
 | SYN     |
 +-------->|
 | SYN/ack |
 |<--------+
 | ack     |
 +-------->|
  • コネクション要求先(図ではホストB)が存在しない場合、タイムアウト待ちでエラーとなります
  • コネクション要求先のホストは稼動しているがポート番号を待つプログラムがいない場合、RSTが返ってタイムアウトを待たずにエラーとなります

コネクションの終了

TCPは全二重のため、各方向(read/write)それぞれ独立にシャットダウンします。
通信の両者がデータの送信を終えるときにFINを送ります。(シャットダウン直後ではなく)

  • FINを送った側はアクティブクローズ
  • FINを受けた側はパッシブクローズ

FINは、アプリケーションプログラムへはEOFとして通知されます。

           A         B
アプリから | FIN     |
 close()   +-------->| -> アプリへEOF
           | FIN/ack |
           |<--------+
           :         : [ハーフクローズ期間]
           | FIN     |  アプリからclose()
アプリへ<- |<--------+ 
EOF        | FIN/ack |
           +-------->|
  • ハーフクローズ期間は、BからwriteしAでreadすることが可能
  • AがFINを受け取ってからFIN/ackを返すまでは、TIME_WAIT状態(2MSL待ち)であり、最大セグメント寿命(MSL)の2倍の時間待つ
    最大セグメント寿命の一般的な実装値は、30秒、1分、2分など
  • プロセスを終了してすぐに再実行したときに、ポートが使用中で初期化できないことがあるが、これがその理由(SO_REUSEADDR)
  • TCPクライアントは特定ポートではなくエフェメリスポートを使うので影響なし

コネクションの中断

コネクションの終了時に、FINではなくRSTを送って中断する機能があります。
すべてのキュー・データは破棄され、RSTを直ちに送ります。

SO_LINGER(Linger on close)で、通常のクローズ(終了)から中断に変更することができます。

RST(リセットセグメント)

例えば、コネクション要求が到着したときに、そのコネクションのあて先ポート上にプロセスが待機していないときにRSTを返します。

ハーフオープンコネクション

例えば、2台のホストのいずれかがクラッシュしたときに生じる状態です。データの転送を行わないと、他方のクラッシュを検知できません。

遅延ACK

データを受け取ったら直ちにACKを返すのではなく、ACKを送るタイミングを遅らせ、そのACKと同じ方向のデータと組み合わせることでネットワーク上を流すセグメントを少なくします(piggy-back)。
一般的には200ミリ秒の遅延を用いることが多いようです。

バッファサイズ

ソケット毎に、送信バッファと受信バッファが用意されます。

送信バッファサイズ

  • 相手側の受信バッファと同じサイズ以上
  • 一度に大量のデータを送信する場合、「帯域幅と遅延の積」以上

受信バッファサイズ

TCPのウィンドウサイズに影響します。
64KB以上の受信バッファ(ウィンドウサイズ)を使用する場合、Window Scalingオプションを有効にします。

  • 接続の最大セグメントサイズ(MSS)の少なくとも3倍を確保する
  • 大量のデータを受信する場合、物理ネットワークの「帯域幅と遅延の積」以上

各OSのバッファサイズデフォルト

2007年調べ

プラットフォーム バッファサイズ
送信 受信
FreeBSD 32KB 56KB
Linux 16KB 42KB
Solaris 52KB 52KB
Windows 8KB 8KB

最大セグメントサイズ(MSS)

相手アドレスがローカルならデフォルトで1460(Ethernetのサイズ限界)とするとか、非ローカルなら536とか、らしいです。

ウィンドウサイズ

一般的なデフォルトの4096バイトだと、Ethernetでは最適ではないようです。

Nagleのアルゴリズム

セグメントサイズよりも小さなデータを多量に送ると、WANでの輻輳の原因となるので、

  • 確認応答(ACK)が来るまで次のセグメントは送らない
  • 確認応答(ACK)が来た時点で、それまで溜められていた小さなデータが1つのセグメントに集積されて送られる

スライディングウィンドウ

ACKを受け取るまでに送信できるデータの範囲

スロースタートと輻輳ウィンドウ(cwnd)

コネクション確立後、輻輳ウィンドウはセグメント1つ分(他方のエンドから広告されたサイズ)に設定され、
ACKを受け取るたびに1セグメント分のサイズを増やしていきます(指数的に増大していく)。

いつか帯域を越えるので、途中のネットワークでパケットが破棄されます。
この場合、cwndが大き過ぎたと判断し、cwndを小さくします。

輻輳回避のアルゴリズム

参考資料の「詳解TCP/IP Vol.1」で説明されているアルゴリズム以外にもいくつか登場しているようです。キーワードだけ列挙します。

  • TCP Tahoe
  • TCP Reno
  • TCP New Reno
  • TCP Vegas
  • CTCP(複合TCP)

以下URLを参考にしました。
http://www.net.c.dendai.ac.jp/~yutaro/#2-1

輻輳発生時のcwndの変更

輻輳が発生した場合(タイムアウトや重複ACK)、現行ウィンドウサイズ(cwndと受け手が広告するウィンドウとどちらか小さい値)をssthreshに保存した後、

  • タイムアウトの場合、cwndを1に戻す(スロースタート実施)
  • タイムアウトでない場合
    • cwnd ≦ ssthresh の場合、スロースタート実施
    • そうでない場合、輻輳回避モードのまま(?)

送信データの最大は?

TCPは、cwndのサイズと受け手が広告するウィンドウサイズとのどちらか小さい値を超えては送信しません。

Keep Alive

TCP仕様にはない、使用の是非に議論のある機能です。
通常はサーバー側がKeep Aliveを有効にします。

あるコネクションで、2時間以上活動がなかったときに、サーバーがクライアントに検査セグメントを送ります。
検査セグメントに対して

  • 正常応答あり:タイマーを2時間後にセット
  • 正常応答なし:75秒タイムアウトで10回まで検査セグメントを送る
  • RST応答あり:サーバーはコネクションを終了する