TCP/IP
TCP/IP ・ てぃーしーぴーあいぴー
インターネット通信の基本プロトコル群。届く保証のない IP の上に、TCP が番号と確認応答で信頼性を作る。
✦ 30秒で試す — 航海届く保証のない道の上に、必ず届く通信が組み立てられているACK 5が落ち、ACK 6は届いた。送信側が送り直すのは?予想してみる概要
TCP/IP は、インターネットで使われる通信規約(プロトコル)群の総称です。中心となるのは、IPアドレスを頼りにパケット(データの小包)を宛先まで運ぶ IP と、その上で「送ったデータが順序どおり・欠けずに届いたこと」を保証する TCP の2つです。HTTP での Web の閲覧も、メールも、データベースへの接続も、ほぼすべての通信がこの土台の上に載っています。
ふだんの Web 開発で TCP/IP を直接書くことはまずありません。しかし「接続がタイムアウトする」「大量アクセスでコネクションが枯渇する」「通信が遅い原因はどの層か」といったトラブルの切り分けは、最終的にこの層の理解に支えられます。ネットワークの語彙の系譜をたどると、多くの線がここに集まってきます。
なぜ生まれたか
1970年代、コンピュータネットワークは組織や規格ごとに分かれていて、異なるネットワーク同士をつなぐ共通の方法がありませんでした。1974年、ヴィントン・サーフとロバート・カーンは、ネットワーク同士を相互につなぐためのプロトコルを論文にまとめます。途中のネットワークがどんな方式でも、パケットを次へ次へと中継すれば相手に届く、という発想です。これが TCP/IP に育ち、1981年に IP(RFC 791)と TCP(RFC 793)の仕様がまとまって、1983年1月1日に ARPANET の標準プロトコルが一斉に TCP/IP へ切り替わりました。これが現在のインターネットの直接の祖先です。
設計上の重要な割り切りは、「ネットワーク自体は信頼できないものとする」ことでした。パケットは途中で失われるかもしれないし、順序が入れ替わるかもしれない。その前提を受け入れたうえで、信頼性の確保は末端のコンピュータ同士(TCP)の責任とする。ネットワークの中身を単純に保ち、賢さを端に置くこの方針(エンドツーエンド原理)のおかげで、多様な回線や機器を際限なくつなぎ足せる拡張性が生まれました。
ただし、端に置いた賢さには1つ見落としがありました。1986年、ネットワークが混み合うと各 TCP が再送を繰り返してさらに混み合い、通信量が1000分の1近くまで落ち込む「輻輳崩壊」が起きます。1988年にヴァン・ジェイコブソンが、ロスを混雑のサインとみなして送信ペースを自分で落とす輻輳制御を TCP に加え、この問題を解きました。「信頼性」だけでなく「譲り合い」も端で行う、という形がここで完成します。
詳細
役割分担の層構造
TCP/IP は役割の異なる層の積み重ねとして整理されます。下から、ケーブルや無線で隣の機器と信号をやり取りするリンク層、IP がパケットを宛先の機器まで運ぶインターネット層、TCP(または UDP)が機器の中のアプリケーションまで届けて通信の品質を担うトランスポート層、そして HTTP などのアプリケーション層です。各層は自分の仕事にだけ責任を持ち、上の層は下の層の詳細を知らずに済みます。Wi-Fi でも光回線でも同じ HTTP がそのまま動くのは、この分業の成果です(7つの層で整理した OSI参照モデルも参照)。
IP の仕事は「ベストエフォートでの配達」までです。届く順序も、そもそも届くことすらも保証しません。その穴を埋めるのが TCP で、宛先のポート番号への振り分けに加えて、順序の復元・欠落の検出と再送・送る量の調整を引き受けます。
コネクションの確立: 3ウェイハンドシェイク
TCP は通信に先立って、双方が「送れること・受け取れること」を確認し合い、お互いの通し番号(シーケンス番号)の初期値を伝え合います。この論理的な通信路をコネクションと呼び、確立の手順を3ウェイハンドシェイクと呼びます。
クライアントは1往復待ってから(3つめの ACK と同時に)データを送り始められます。物理的に遠いサーバほどこの往復に時間がかかるため、接続の確立のコストは Web の高速化で常に意識されるポイントです(レイテンシとスループット)。HTTPS ではさらに TLS のハンドシェイクが重なるので、確立済みの接続を使い回す keep-alive やコネクションプールが効いてきます。通信の終わりにも、FIN と ACK のやり取りで双方が合意して閉じる手順があります。
信頼性の仕組み: 番号と確認応答と再送
TCP は送るデータの1バイトごとに通し番号を振り、受信側は「ここまで受け取った、次はこの番号がほしい」という確認応答(ACK)を返します。ACK は累積的で、「ACK 4」は「3番まではすべて受け取った」という意味です。途中のセグメント(TCP が運ぶデータの単位)が1つ抜けると、その後ろがいくら届いても ACK は抜けた番号のまま進みません。これを重複 ACK と呼びます。
送信側は、送ったセグメントごとに再送タイマーを持っていて、一定時間(往復時間の実測から計算する RTO)内に ACK が進まなければ、失われたとみなして送り直します。重複 ACK が3回続いた場合は、タイマーを待たずに抜けた分だけを送り直します(高速再送)。受信側は、先に届いた後ろのセグメントをバッファに置いて待ち、穴が埋まったところでまとめて番号順に並べ直し、アプリケーションに渡します。アプリケーションから見ると、途切れのない正しいバイトの流れがただ届くように見えます。
スライディングウィンドウ — 返事を待たずに送る
1つ送るたびに ACK を待っていては、1往復で1セグメントしか送れません。そこで TCP は、ACK を待たずに送ってよい量(ウィンドウ)を決め、その範囲をまとめて送ります。ACK が返ってくるたびに、ウィンドウは番号の列の上を前へ滑っていきます。これがスライディングウィンドウです。ウィンドウが大きいほど、往復時間の間に回線へ流し込める量が増え、スループットが上がります。
下の部品で確かめてください。「送信開始」を押したあと、回線の上を飛んでいるセグメントを押すと、そのセグメントが途中で失われます。受信側に穴が空いて ACK が止まり、再送されて並べ直される様子が見えます。ウィンドウの幅を変えて、全部届くまでの時間も比べてみてください。
「送信開始」を押してください。セグメント(金の札)が飛び始めたら、回線の上の1つを押して落としてみましょう。
流量制御と輻輳制御 — 窓の大きさを決める2つの理由
ウィンドウの大きさは、2つの別々の理由で制限されます。1つは受信側の都合です。受信側はバッファの空き容量を「受信ウィンドウ」として ACK のたびに知らせ、送信側はそれを超えて送りません。受信側のアプリの読み出しが遅くバッファがあふれそうなときに、送信側を待たせる仕組みで、これを流量制御と呼びます。
もう1つはネットワークの都合です。途中の回線やルータが混み合っているのに全員が全力で送れば、パケットが捨てられ、再送がさらに混雑を生みます。そこで送信側は、自分の推定で「輻輳ウィンドウ」を持ち、実際に送る量は受信ウィンドウと輻輳ウィンドウの小さいほうにします。これが輻輳制御です。接続の始めは小さな窓(いまは10セグメント程度)から始めて、ACK が返るたびに倍々で広げ(スロースタート)、ロスを検知したら窓を縮めて、そこからは少しずつ広げます。世界中の通信が帯域を奪い合っても、インターネット全体が崩壊しないのは、各 TCP が行うこの自律的な譲り合いのおかげです。Linux の既定の CUBIC や、Google が開発した BBR など、窓の広げ方・縮め方にはいくつもの流儀があります。
流量制御は「相手を溺れさせない」、輻輳制御は「道路を詰まらせない」ための仕組み、と覚えると区別しやすくなります。遠くて帯域の太い回線では、帯域と往復時間の積(帯域遅延積)に見合うだけの窓がないと回線を使い切れないので、どちらの窓も十分に大きくできることが性能の条件になります。
実務での接点と落とし穴
アプリケーションから TCP/IP を使う窓口はソケットです。「コネクションが確立できない」のか(ファイアウォールやポートの設定の問題か)、「確立はするがデータが来ない」のか(アプリの問題か)の切り分けは、この層を意識するとぐっとやりやすくなります。curl -v や ss、パケットをそのまま記録する tcpdump は、その切り分けの定番の道具です。
落とし穴としては、TCP の保証の範囲の誤解が代表的です。TCP が保証するのは「コネクションが生きている間の順序と欠落のなさ」であって、相手のアプリケーションが処理を終えたことまでは保証しません。応答を受け取る前に接続が切れたら、処理されたかどうかは分からないので、再送しても安全な設計(冪等性)が必要になります。また、1つのセグメントが失われると後ろのデータが全部待たされる性質(ヘッドオブラインブロッキング)は、1本の接続で多数のリクエストを流す HTTP/2 で表に出てきました。これを避けるために、HTTP/3とQUIC は UDP の上に信頼性の仕組みを作り直しています。リアルタイム通信や1往復で済む問い合わせのように、TCP の保証そのものが重荷になる用途では、あえて保証を捨てた UDP が選ばれます。
試問
送信側から受信側へ、#1〜#10 のセグメント(金の札)を送ります。受信側は届くたびに「次は何番がほしい」という ACK を返し、送信側は ACK を待たずに送ってよい数(ウィンドウ)を 1 / 4 / 8 から選べます。「送信開始」のあと、回線の上を飛んでいる札を押すと、その札は途中で失われます。送信側には、ACK が一定時間進まないと送り直す再送タイマーがあります。
「送信開始」を押してください。セグメント(金の札)が飛び始めたら、回線の上の1つを押して落としてみましょう。
