はじめに
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-oriented | Data送信前に通信状態を確立する |
| Byte Stream | Application Dataを連続したByte列として扱う |
| Reliable Communication | Sequence Number、ACK、再送で到達と順序を管理する |
| Full Duplex | 両方向で同時にDataを送受信できる |
| Flow Control | 受信側のBufferを超えないよう送信量を調整する |
| Congestion Control | Networkの混雑状況に応じて送信量を調整する |
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 Offset | TCP Headerの長さを示す |
| Flags | SYN、ACK、FIN、RSTなどの制御情報を示す |
| Window Size | 受信可能なData量を通知する |
| Checksum | HeaderとDataの破損を検出する |
| Options | MSS、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 Control | Congestion Control |
|---|---|---|
| 守る対象 | Receiver | Network |
| Window | rwnd | cwnd |
| 管理主体 | 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 | 意味 |
|---|---|
| LISTEN | Serverが接続要求を待っている |
| SYN-SENT | SYNを送り、SYN-ACKなどを待っている |
| SYN-RECEIVED | SYNを受けてSYN-ACKを送り、ACKを待っている |
| ESTABLISHED | Connectionが確立し、Dataを送受信できる |
| FIN-WAIT-1 | FIN送信後、そのACKまたは相手のFINを待っている |
| FIN-WAIT-2 | 自分のFINへのACKを受け、相手のFINを待っている |
| CLOSE-WAIT | 相手のFINを受け、Local Applicationのcloseを待っている |
| LAST-ACK | Local側もFINを送り、そのACKを待っている |
| CLOSING | 双方がほぼ同時にFINを送り、ACKを待っている |
| TIME-WAIT | Active Close後、古いSegmentの消滅などを待っている |
| CLOSED | Connectionが存在しない状態 |
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のネットワークの仕組みを
図解で理解したい方へ
Socket・TCP・ルーティング・NIC 現場で役立つ通信処理の基礎知識など Linuxにおけるネットワークの仕組みを図解を中心にわかりやすく解説しています。
Kindleで書籍を見る →


コメント