TCPの状態遷移とコネクション確立・切断の内部
SYNやTIME_WAITが何を守っているのかが腑に落ちる。確立から切断までのTCP状態機械を一本の流れで追い、各遷移の設計意図まで理解できる。
- 1.TCPは11個の状態を持つ状態機械で、SYN/ACK/FINの送受信を契機にLISTEN→ESTABLISHED→CLOSEDへ遷移する。
- 2.3ウェイハンドシェイクは初期シーケンス番号(ISN)を双方向で同期し合う手続き。2回では不十分で4回は冗長。
- 3.TIME_WAITは古い重複セグメントと最終ACKの消失に備える安全装置。CLOSE_WAITの滞留はアプリのclose漏れを示す。
TCPは「状態機械」である
TCPコネクションの本質は、両端のホストがそれぞれ保持する 有限状態機械(state machine) です。各端は現在の状態を1つ持ち、「セグメントの送受信」または「アプリからのシステムコール」をトリガに次の状態へ遷移します。RFC 9293(旧 RFC 793)は11個の状態を定義します。
CLOSED(仮想的な初期/終端状態)LISTEN(接続待ち受け)SYN_SENT/SYN_RECEIVED(確立の途中)ESTABLISHED(データ転送可能)FIN_WAIT_1/FIN_WAIT_2/CLOSING/TIME_WAIT(能動クローズ側)CLOSE_WAIT/LAST_ACK(受動クローズ側)
重要なのは、TCPは 全二重(双方向独立) だという点です。送る向きと受け取る向きは別々に閉じられます。切断が3ステップでなく原則4ステップになるのは、この「方向ごとに閉じる」性質の直接の帰結です。前提となる TCP/UDP の役割分担は /network/tcp-udp/、層構造は /network/tcp-ip/ を参照してください。
確立:3ウェイハンドシェイク
サーバーは LISTEN で待ち受けます。クライアントが接続すると、次の順で状態が動きます。
クライアント サーバー
CLOSED LISTEN
|-- SYN(seq=x) ------------------>|
SYN_SENT SYN_RECEIVED
|<- SYN(seq=y),ACK(ack=x+1) ------|
ESTABLISHED
|-- ACK(ack=y+1) --------------->|
ESTABLISHED
各セグメントが運ぶのは単なる合図ではなく、初期シーケンス番号(ISN, Initial Sequence Number) です。SYNはシーケンス空間で1を消費するため、相手は x+1 / y+1 をACKします。3ウェイが必要な理由は次の通りです。
- 送信側のISN
xを受信側が確認応答する(往路の同期) - 受信側のISN
yを送信側が確認応答する(復路の同期)
双方向の同期に最低3メッセージ要ります。2回では片方向しか確定せず、4回はSYN/ACKを1セグメントに相乗りさせて削れるので冗長です。
ISNは時刻ベースのクロックや乱数で初期化されます。固定値だと、前のコネクションの遅延セグメントが新しいコネクションに紛れ込む(古い重複)リスクや、攻撃者によるシーケンス番号予測(TCPスプーフィング)が容易になるためです。
SYN_RECEIVEDとバックログ
サーバーがSYNを受けてSYN/ACKを返した状態が SYN_RECEIVED です。ここで接続情報は「未完成キュー(SYNキュー)」に置かれ、最終ACKが届いて初めて ESTABLISHED の受け入れキュー(acceptキュー)へ移ります。
ACKを返さないSYNを大量に送るとSYNキューが埋まり、正規の接続を受けられなくなります。対策が SYN Cookie で、サーバー側状態をキューに持たず、ISNにハッシュを埋め込んでステートレスに最終ACKを検証します。
切断:4ウェイクローズ
全二重を一方向ずつ閉じるため、close() を呼んだ側(能動クローズ)と相手(受動クローズ)で経路が分かれます。
能動クローズ側 受動クローズ側
ESTABLISHED ESTABLISHED
|-- FIN(seq=u) ----------------->|
FIN_WAIT_1 CLOSE_WAIT
|<- ACK(ack=u+1) ----------------|
FIN_WAIT_2
| (アプリがcloseするまで送信継続可)
|<- FIN(seq=v) ------------------|
TIME_WAIT LAST_ACK
|-- ACK(ack=v+1) --------------->|
| (2MSL待機) CLOSED
CLOSED
ポイントは、最初のFINは「もう送るデータは無い」という片方向の宣言にすぎないことです。相手はまだ送信を続けられ、CLOSE_WAIT の間にバッファ内データを送り切ってから自分のFINを返します。だからACKとFINが別セグメントになり、4ステップになります(両者のデータ送信が同時に尽きていればACKとFINが相乗りして3ステップに縮むこともあります)。
両端が同時に close() するとFINが交差し、双方が FIN_WAIT_1 → CLOSING → TIME_WAIT をたどる 同時クローズ という対称経路も定義されています。
TIME_WAITとCLOSE_WAITの設計意図
切断で最も問われるのがこの2状態です。役割が正反対なので並べて押さえます。
| 観点 | TIME_WAIT | CLOSE_WAIT |
|---|---|---|
| どちら側 | 能動クローズ側(先にFINを送った方) | 受動クローズ側(FINを受けた方) |
| 入る契機 | 最後のACKを送った後 | 相手のFINを受信した後 |
| 抜ける契機 | 2MSL(最大セグメント生存時間の2倍)経過 | 自分のアプリがclose()を呼ぶ |
| 滞留が示すもの | 短時間に大量の能動クローズ(通常は正常) | アプリのclose漏れ・バグ(異常) |
| 主な制御点 | ポート再利用・OSパラメータ | アプリのソケットクローズ処理 |
なぜTIME_WAITで待つのか
TIME_WAIT で 2MSL だけ待つのには2つの明確な理由があります。
- 最終ACKの消失に備える:自分が送った最後のACKが失われると、相手は
LAST_ACKのままFINを再送してくる。すぐにCLOSEDにすると再送FINに応答できず、相手はRSTを受け取る。待機していれば再送FINに再度ACKを返せる。 - 古い重複セグメントの排除:同じ4タプル(送信元/宛先IP・ポート)で即座に新コネクションを張ると、ネットワーク内をさまよう前コネクションの遅延セグメントが新接続に誤配される恐れがある。最大寿命MSLの2倍待てば、往復分の遅延パケットが確実に消滅する。
高頻度に短命接続を能動クローズするサーバーでは、エフェメラルポート枯渇の一因になります。対策はコネクション再利用(Keep-Alive)が第一。Linuxの tcp_tw_reuse はタイムスタンプ前提で発側の再利用を許す機能で、安易な tcp_tw_recycle(既に廃止)とは別物です。サーバー側のLISTENポートはTIME_WAITになりません。
CLOSE_WAIT から抜ける条件はアプリの close() だけです。これが大量・長時間残るのは「相手は切ったのに自分がソケットを閉じていない」状態、つまりファイルディスクリプタのリークです。OSパラメータでは直りません。コードのクローズ処理を直すしかありません。
RSTとhalf-open:状態機械の例外経路
正規の遷移以外に、RST(リセット)による即時打ち切りがあります。閉じたポートへのセグメント、想定外のシーケンス番号、アプリの強制切断(SO_LINGERゼロ)などで送られ、相手は確認応答なしに即 CLOSED へ落ちます。
一方が再起動して状態を失い、もう一方が ESTABLISHED のままになるのが half-open です。生存側がデータを送るとRSTで弾かれて検知します。長時間アイドルな接続の死活確認には TCPキープアライブ を使いますが、既定の発火間隔は数時間と長いため、アプリ層のハートビートを併用するのが実務の定石です。
状態の観測
実際の遷移は標準ツールで確認できます。詳細なパケット解析は /network/packet-capture/ を参照してください。
# 状態ごとの接続数を集計(TIME_WAIT/CLOSE_WAIT の偏りを掴む)
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c
# 特定ポートの接続状態を継続監視
ss -tan state time-wait
ss -tan state close-wait
# ハンドシェイクとFIN交換を実際に見る
sudo tcpdump -i any 'tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0'
「3ウェイは何を同期するか」→ 双方向のISN。「TIME_WAITは誰が・なぜ・どれだけ」→ 能動クローズ側が、最終ACK消失と古い重複に備え、2MSL。「CLOSE_WAITが消えない原因は」→ アプリのclose漏れ。「half-openの検知方法は」→ 送信時のRST、またはキープアライブ。この4点を遷移図と結びつけて説明できれば上級。
まとめ
TCPは「状態を持たないIPの上に、状態機械で信頼性を載せる」プロトコルです。確立の3ウェイは双方向ISNの同期、切断の4ウェイは全二重を方向ごとに閉じる帰結、TIME_WAITとCLOSE_WAITはそれぞれ「過去の遅延パケットと再送ACK」「アプリのクローズ責任」を守る装置でした。ポート番号やソケットとの対応関係は /network/socket-port/ で補完すると、観測したコネクション状態を実装レベルで読み解けるようになります。
ネットワーク Article
TCPの状態遷移とコネクション確立・切断の内部を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
TCP
比較で見る軸
難易度: advanced / カテゴリ: ネットワーク / タグ数: 5
導入後に効く点
3ウェイハンドシェイクは初期シーケンス番号(ISN)を双方向で同期し合う手続き。2回では不十分で4回は冗長。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- ネットワーク
- タグ数
- 5
判断チェックリスト
- 自社の用途が「TCP / 状態遷移」に近いか確認する。
- 強みである「TCPは11個の状態を持つ状態機械で、SYN/ACK/FINの送受信を契機にLISTEN→ESTABLISHED→CLOSEDへ遷移する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。