【図解】TCPの仕組みを徹底解説|3-way Handshake・ACK・再送・Flow Control・Congestion Control

はじめに

Web、SSH、Databaseなど、多くのApplicationはTCPを利用します。TCPは通信を始める前にConnectionを確立し、送信したDataが相手へ届いたか、正しい順序で並んでいるか、相手とNetworkがどれだけのDataを受け入れられるかを管理します。

TCPの特徴は、IP Network上で失われたり、重複したり、順序が入れ替わったりする可能性のあるPacketを扱いながら、Applicationへ順序どおりのByte Streamを提供することです。そのためにSequence Number、ACK、Retransmission、Receive Window、Congestion Controlなどを組み合わせます。

ただし、TCPが保証するのは、Connectionが維持されている間に受信確認されたByteを順序どおりApplicationへ渡すことです。相手ApplicationがDataを業務処理へ反映したことや、Diskへ永続化したことまでは保証しません。必要であればApplication Protocol側でResponse、Transaction、再実行制御などを設計します。

本記事では、TCP Segmentの構造、3-way Handshake、Sequence NumberとACK、再送、Flow Control、Congestion Control、Connection切断、TCP State、Linux上での確認方法を順番に解説します。

TCPとは

TCPはTransmission Control Protocolの略で、Connection-orientedかつReliableなTransport Layer Protocolです。ApplicationはSocketを通じてTCPを利用し、送信先と受信元のPort Numberによって通信相手のApplicationを識別します。

特徴TCPの役割
Connection-orientedData送信前に通信状態を確立する
Byte StreamApplication Dataを連続したByte列として扱う
Reliable CommunicationSequence Number、ACK、再送で到達と順序を管理する
Full Duplex両方向で同時にDataを送受信できる
Flow Control受信側のBufferを超えないよう送信量を調整する
Congestion ControlNetworkの混雑状況に応じて送信量を調整する

TCPはMessage境界を保持しません。Applicationが一回のsend()で100 Byteずつ二回送信しても、受信側のrecv()が100 Byteずつ二回返す保証はありません。200 Byteをまとめて受け取る場合も、複数回に分かれる場合もあります。Messageの長さや区切りはApplication Protocol側で表現します。

TCP Connectionは一般にSource IP Address、Source Port、Destination IP Address、Destination Portの組み合わせで区別されます。Protocolまで含めて5-tupleと呼ぶこともあります。Serverは一つのListen Portで、接続元の異なる多数のConnectionを同時に管理できます。

TCP Segmentの構造

TCPはApplication DataへTCP Headerを付け、TCP SegmentとしてIP Layerへ渡します。TCP HeaderにはPort、Sequence Number、Acknowledgment Number、Flags、Windowなど、ConnectionとData転送を制御する情報が含まれます。

Field主な役割
Source / Destination Port送信元と送信先のApplication Endpointを識別する
Sequence Number送信するByte Stream内の位置を示す
Acknowledgment Number次に受信したいSequence Numberを示す
Data OffsetTCP Headerの長さを示す
FlagsSYN、ACK、FIN、RSTなどの制御情報を示す
Window Size受信可能なData量を通知する
ChecksumHeaderとDataの破損を検出する
OptionsMSS、Window Scale、SACK Permitted、Timestampなどを扱う

主なFlagにはSYN、ACK、FIN、RST、PSHがあります。SYNはConnection確立とInitial Sequence Numberの同期、ACKはAcknowledgment Numberが有効であること、FINは送信方向の終了、RSTはConnectionの即時Resetを示します。

TCP Headerの基本長は20 Byteですが、Optionがあると長くなります。Connection確立時にはMSS(Maximum Segment Size)、Window Scale、SACK(Selective Acknowledgment)対応などのOptionが交換されることがあります。双方が通知した能力と値に基づき、そのConnectionの転送方法が決まります。

TCP Connectionの確立

TCPはDataを送る前に3-way Handshakeを行います。ClientとServerが互いに通信可能であることを確認し、両方向のInitial Sequence Numberを交換して、Connectionの状態を確立します。

① ClientがSYNを送信する

Clientがconnect()を実行すると、TCPはSYN Flagを設定したSegmentを送信します。Sequence NumberにはClient側のInitial Sequence Numberであるxを設定します。ClientのStateはSYN-SENTになります。

② ServerがSYN-ACKを返す

対象PortでListenしているServer側TCPは、SYNを受信するとSYNとACKを設定したSegmentを返します。Server側のInitial Sequence Numberをy、Acknowledgment Numberをx+1にします。この時点のServer側StateはSYN-RECEIVEDです。

③ ClientがACKを返す

ClientはSYN-ACKを受信すると、Acknowledgment Numberをy+1にしたACKを返します。これにより両側がESTABLISHEDとなり、通常のData送受信を行える状態になります。SYNはPayloadを持たない場合でもSequence Numberを一つ消費します。

Serverの対象PortがListenしていない場合、一般にServerはRSTを返します。途中でPacketが破棄される場合は応答がなく、ClientはSYNを再送しながらTimeoutを待つことがあります。「Connection refused」と「Connection timed out」は、異なる状況を示す手掛かりになります。

TCPでDataを送信する仕組み

Connection確立後、Applicationはsend()やwrite()を使ってSocketへDataを渡します。Linux KernelはDataをSocket Send Bufferへ格納し、TCPの状態、MSS、Window、Congestion Controlなどに基づいて送信処理を進めます。

send()が成功しても、相手ApplicationがDataを読み取ったことを意味しません。多くの場合、Local KernelがApplicationからDataを受け取ったことを示します。その後、TCPがSegmentを送信し、相手TCPからACKを受け取るまで、確認されていないDataは送信Buffer内で管理されます。

TCPはByte Streamであるため、Applicationが渡したDataとTCP Segmentの境界は一致しません。大きなDataは複数Segmentに分かれ、小さなDataは送信Timingや設定によってまとめて扱われることがあります。また、TSOやGSOなどのOffloadが有効な環境では、Kernel内で観察する単位とWire上のSegmentが異なる場合があります。

Sequence NumberとACK

TCPのSequence NumberはSegmentの通し番号ではなく、Byte Stream内の位置を表します。あるSegmentのSequence Numberが1001でPayloadが500 Byteなら、そのSegmentはSequence Number 1001から1500までのByteを運びます。

Acknowledgment Numberは、次に受信側が期待するSequence Numberを示します。TCPのACKは基本的にCumulative ACKです。ACK=3001なら、途中に欠落がない限り3000までの連続したByteを受信したことを示します。

途中のSegmentが失われ、後続Segmentだけが届いた場合、受信側は最後に連続して受信できた位置に基づくACKを繰り返すことがあります。SACKが双方で有効なら、連続していない範囲のうち、どのBlockを受信済みか追加情報として通知できます。

ACKは必ずSegmentごとに即時送信されるとは限りません。Delayed ACKによって短時間待ち、複数Segmentをまとめて確認することがあります。ACKのTimingはOS、通信状態、実装などによって異なるため、Capture結果を単純に「一つ送信したら一つACK」と考えないようにします。

再送の仕組み

IPはPacketの到達を保証しません。Packet Lossが起きると、TCPはACKの状態とTimerを使って未確認Dataを検出し、必要なDataを再送します。代表的な再送にはRTOによる再送と、Duplicate ACKなどを手掛かりにしたFast Retransmitがあります。

RTOによる再送

RTOはRetransmission Timeoutの略です。送信したDataに対するACKが一定時間内に届かなければ、TCPは未確認Dataを再送します。RTOは固定値ではなく、観測したRTTとその変動をもとに計算されます。

Fast Retransmit

途中のDataが欠落したまま後続Dataが届くと、受信側は同じAcknowledgment Numberを繰り返して返すことがあります。送信側はDuplicate ACKなどから欠落を推測し、RTOを待たずに再送できる場合があります。これがFast Retransmitです。

LinuxではSACKやRACKなどのLoss Recovery機構も利用されるため、実際の再送判定は単純に「3 Duplicate ACK → 再送」だけではありません。

再送はDataを確実に届けるために必要ですが、Applicationから見るとResponseの遅延になります。Packet Loss、RTT増加、再送の繰り返しはThroughputとLatencyを悪化させます。tcpdumpのRetransmission表示だけで断定せず、両EndpointのCapture、TCP Counter、Interface Counter、Network機器の状態を組み合わせて確認します。

SACKとRACKによるTCPのLoss Recovery

ここまでRTOやFast Retransmitによる基本的な再送を説明しました。ここからは少し踏み込み、実際のLinux TCPでLoss Recoveryを効率化するSACKやRACKについて見ていきます。

特にRHEL 9ではSACKがデフォルトで有効です。

SACKとは

SACK(Selective Acknowledgment)は、受信側が「どの範囲のDataを受信できているか」を送信側へ通知するTCP Optionです。

通常のACKは、基本的に「次に期待するByte」を通知します。たとえば途中のDataが欠落すると、次のようになります。

上記の図の場合、

ACK = 1501 → 1500まで連続して受信済み
SACK = 2001-2501 → 2001〜2500は受信済み

と通知できます。

つまり送信側は、1501〜2000が欠落している一方、その後の2001〜2500は届いていることを把握できます。

これによりSenderは、どの範囲がすでに受信済みで、どの範囲が欠落しているかを把握しやすくなります。SACK自体が再送を行うわけではなく、この受信情報をLoss Recoveryに利用することで、すでに届いているDataの不要な再送を抑え、欠落したDataを効率的に再送できます。Red Hatも、SACKを利用することでLossした部分を選択的に再送できることをRHEL 9のドキュメントで説明しています。

RHEL 9では次のKernel Parameter(sysctl net.ipv4.tcp_sack)で確認できます。デフォルトでは、net.ipv4.tcp_sack = 1 となっており、SACKが有効です。

RACKとは

RACK(Recent ACKnowledgment)は、ACKが返ってきた時間関係を利用してPacket Lossを検出する仕組みです。

従来のFast Retransmitでは、

Duplicate ACK × 3回 → Lossを推測 → Fast Retransmit 

という考え方が基本でした。

一方、RACKでは「何個Duplicate ACKが届いたか」だけではなく、後から送ったDataがACKされたのに、それより前に送ったDataが一定時間ACKされていないといった時間ベースの情報をLoss検出に利用します。

この方式は、ACKされたPacketの送信時刻を基準にLossを判断するため、Duplicate ACKの回数だけに依存する方式よりも、PacketのReorderingが発生するNetworkでLossを適切に判断しやすくなります。

Linux KernelではRACKがTCPのLoss Detectionに利用されており、tcp_recoveryで関連する動作が定義されています。Linux Kernel Documentationでは、RACKについて「lost retransmissionsやtail dropsを素早く検出し、reorderingにも強いLoss Detection」と説明されています。

Flow Control

Flow Controlは、Receiverが処理できる量にSenderの送信量を合わせる仕組みです。

ReceiverはReceive Bufferの空き容量をもとに、TCP HeaderのWindow Sizeを使ってReceive Window(rwnd)をSenderへ通知します。

反対に、Receiver Applicationの処理が遅くなるとReceive Bufferが埋まり、rwndが小さくなります。

Receive Bufferに空きがなくなると、ReceiverはZero Window(rwnd = 0)を通知します。Senderは通常のData送信を一時停止し、その後Windowが再び開けば送信を再開します。

つまり、Flow Control = Receiverを守る制御です。

なお、TCP HeaderのWindow Size Fieldは16 bitですが、3-way Handshake時にWindow Scale OptionをNegotiationすることで、より大きなReceive Windowを利用できます。

Congestion Control

Congestion Controlは、途中のNetworkが混雑しないようにSenderの送信量を調整する仕組みです。

Sender側TCPはCongestion Window(cwnd)を管理します。

たとえばNetworkが順調であれば送信量を増やし、Packet LossなどからCongestionが疑われれば送信量を抑えます。

つまり、Congestion Control = Networkを守る制御です。

LinuxではCUBICやBBRなどのCongestion Control Algorithmが利用できます。

以下、AlmaLinux9のデフォルトの設定です。

このLinuxでは、TCPの送信量を調整するデフォルトのCongestion Control AlgorithmとしてCUBICを使用していることが分かります。CUBICはLinuxで広く利用されている方式で、cwnd(Congestion Window)を3次関数に基づいて調整し、Networkの状態に応じてTCPの送信量を制御します。

Flow ControlとCongestion Controlの違いまとめ

最も重要な違いは以下です。

項目Flow ControlCongestion Control
守る対象ReceiverNetwork
Windowrwndcwnd
管理主体Receiverが通知Senderが管理
主な判断材料Receive Bufferの空きACK / Loss / RTTなど

SenderがACKを待たずにNetwork上へ送り出せる未確認Data量は、単純化すると、

未確認のまま送信できるData量 ≈ min(rwnd, cwnd)
rwnd ─ Receiver側の制約
cwnd ─ Network側の制約

と考えると理解しやすいです。

TCP Connectionの切断

TCP Connection切断の概要は以下の通りです。

TCPはFull Duplexであり、二つの送信方向を別々に終了できます。通常の切断ではFINとACKを交換します。片側が先にclose()などを実行する例を単純化すると、4-way Handshakeになります。

FINは「この方向では今後Dataを送らない」ことを示します。FINを受信しても、反対方向の送信は継続できます。この状態をHalf-closeと呼びます。FIN自身もSequence Numberを一つ消費します。

Active Closeを行った側は、通常最後のACK送信後にTIME-WAITへ入ります。これは遅れて到着した古いSegmentが後続のConnectionへ影響することを防ぎ、最後のACKが失われた場合に再送されるFINへ応答できるようにするためです。

RSTによる切断

RSTはConnectionを通常のFIN交換ではなく即時にResetするために使われます。ListenしていないPortへの接続、存在しないConnectionに対応するSegment、ApplicationによるAbortive Closeなどで見られます。

RSTを受け取った側のApplicationではConnection reset by peerなどのErrorになることがあります。CaptureでRSTを確認したら、どちらが送信したか、その直前にどのSegmentが交換されていたか、Application Logに何が記録されているかを確認します。

TCP State

TCPはConnectionの進行状況をStateとして管理します。ssでStateを確認すると、Connection確立中、Data送受信中、切断処理中のどこにあるかを判断できます。

State意味
LISTENServerが接続要求を待っている
SYN-SENTSYNを送り、SYN-ACKなどを待っている
SYN-RECEIVEDSYNを受けてSYN-ACKを送り、ACKを待っている
ESTABLISHEDConnectionが確立し、Dataを送受信できる
FIN-WAIT-1FIN送信後、そのACKまたは相手のFINを待っている
FIN-WAIT-2自分のFINへのACKを受け、相手のFINを待っている
CLOSE-WAIT相手のFINを受け、Local Applicationのcloseを待っている
LAST-ACKLocal側もFINを送り、そのACKを待っている
CLOSING双方がほぼ同時にFINを送り、ACKを待っている
TIME-WAITActive Close後、古いSegmentの消滅などを待っている
CLOSEDConnectionが存在しない状態

CLOSE-WAITが長時間大量に残る場合、Remote側は切断していますが、Local ApplicationがSocketをcloseしていない可能性があります。TIME-WAITはTCPの正常な動作でも発生するため、存在すること自体を障害と判断しません。接続頻度、Local Portの利用状況、Application設計と合わせて評価します。

SYN-SENTが続く場合はSYNまたはSYN-ACKが届いていない可能性があります。SYN-RECEIVEDが多い場合はHandshake最後のACKが届いていない、接続要求が集中しているなどの可能性があります。Stateだけで原因を断定せず、tcpdumpとCounterを組み合わせます。

TCPの全体フロー

TCPは一つの機能だけでReliable Communicationを実現しているわけではありません。3-way Handshakeで両方向の状態を確立し、Sequence NumberでByteの位置を管理し、ACKで受信済み範囲を通知し、欠落時には再送します。

さらに、Receive Windowによって受信側を守り、Congestion WindowによってNetworkへ過剰なDataを送り込まないよう調整します。最後にFINとACKで両方向を終了し、必要に応じてTIME-WAITで古いSegmentの影響を防ぎます。

まとめ

TCPは、3-way HandshakeでConnectionを確立し、Sequence NumberでByte Stream内の位置を管理し、ACKによって連続して受信できた範囲を確認します。Dataが欠落した場合はRTOやFast Retransmitなどによって再送し、SACKやRACKといった仕組みもLoss Recoveryに利用されます。

また、Flow ControlではReceive Window(rwnd)によってReceiverの処理能力に送信量を合わせ、Congestion ControlではCongestion Window(cwnd)によってNetworkの状態に応じて送信量を調整します。TCPはこれらの仕組みを組み合わせることで、IP Network上でApplicationへ順序どおりのByte Streamを提供します。

一方で、TCPが保証する範囲とApplicationが保証すべき範囲は異なります。TCPのACKは相手Applicationでの業務処理完了やDiskへの永続化を保証するものではありません。実際のSystemでは、TCPの仕組みに加えてApplication Protocol側のResponse、Transaction、Timeout、再実行制御まで含めて設計することが重要です。

Linuxにおけるネットワークの仕組みについては、以下の書籍にまとめています。興味がありましたら、ぜひ読んでみてください。

Linux入門・図解

Linuxのネットワークの仕組みを
図解で理解したい方へ

Socket・TCP・ルーティング・NIC 現場で役立つ通信処理の基礎知識など Linuxにおけるネットワークの仕組みを図解を中心にわかりやすく解説しています。

Kindleで書籍を見る →

コメント