TCP/IPパケット通信の全過程
OSがパケットを作り別のPCへ届くまでの仕組みをASCII図で解説。ソフトウェアエンジニアが知っておくべき実践知識。
TCP/IP 4層モデル:概念の地図
ネットワーク通信は「関心の分離」で設計されている。各層が自分の責務だけを担い、上下の層にインターフェース経由で委譲する。
┌──────────────────────────────────────────────┐
│ アプリケーション層 (HTTP, FTP, DNS, SMTP) │ ← 「何を送るか」
├──────────────────────────────────────────────┤
│ トランスポート層 (TCP, UDP) │ ← 「確実に届けるか」
├──────────────────────────────────────────────┤
│ インターネット層 (IP, ICMP, ARP) │ ← 「どこへ届けるか」
├──────────────────────────────────────────────┤
│ ネットワーク I/F 層 (Ethernet, Wi-Fi) │ ← 「どの経路で隣へ渡すか」
└──────────────────────────────────────────────┘
OSI 7層モデルとの対応は以下のとおり(実装では TCP/IP 4層が使われる)。
| TCP/IP 4層 | OSI 7層 |
|---|---|
| アプリケーション層 | アプリケーション・プレゼンテーション・セッション |
| トランスポート層 | トランスポート |
| インターネット層 | ネットワーク |
| ネットワーク I/F 層 | データリンク・物理 |
各層の主要な識別子:
| 層 | 識別子 | 例 |
|---|---|---|
| アプリケーション | URL / ホスト名 | api.example.com |
| トランスポート | ポート番号 | 送信元: 54321, 宛先: 443 |
| インターネット | IPアドレス | 203.0.113.5 |
| ネットワーク I/F | MACアドレス | 00:1A:2B:3C:4D:5E |
カプセル化:データが層ごとに包まれていく
送信側では、データは上の層から下の層へ渡るたびにヘッダが「外側に付与」される。これをカプセル化という。
【アプリケーション層】
┌─────────────────────────────────────────────┐
│ HTTP Request Body(= アプリデータ) │
└─────────────────────────────────────────────┘
【トランスポート層:TCPヘッダを付与 → セグメント】
┌──────────────────┬──────────────────────────┐
│ TCP Header │ HTTP Request Body │
│ (送信元Port, │ │
│ 宛先Port, │ │
│ SEQ番号, etc.) │ │
└──────────────────┴──────────────────────────┘
【インターネット層:IPヘッダを付与 → パケット】
┌─────────────┬──────────────────┬────────────┐
│ IP Header │ TCP Header │ Data │
│ (src/dst IP │ │ │
│ TTL, etc.) │ │ │
└─────────────┴──────────────────┴────────────┘
【ネットワーク I/F 層:Ethernetヘッダ+FCSを付与 → フレーム】
┌──────────────┬─────────────┬────────┬──────┬─────┐
│ Eth Header │ IP Header │ TCP │ Data │ FCS │
│ (src/dst MAC │ │ Header │ │ │
│ EtherType) │ │ │ │ │
└──────────────┴─────────────┴────────┴──────┴─────┘
↑ NICが物理媒体に流すフレーム全体
各ヘッダの主要フィールド
TCPヘッダ(20バイト以上)
| フィールド | 役割 |
|---|---|
| 送信元ポート | アプリを識別(OS が動的に割り当て: 49152〜65535) |
| 宛先ポート | 相手サービスを識別(HTTP=80, HTTPS=443 等) |
| シーケンス番号 | バイトストリームの位置。再組み立てと順序保証に使う |
| 確認応答番号 | 「ここまで受け取った」を相手に通知 |
| フラグ (SYN/ACK/FIN/RST) | コネクション制御 |
| ウィンドウサイズ | フロー制御:「今これだけ受け取れる」 |
IPヘッダ(20バイト以上)
| フィールド | 役割 |
|---|---|
| 送信元 IP | 自分のグローバル/プライベート IP |
| 宛先 IP | 相手のIP(ルーティングの基準) |
| TTL | ホップ数の上限。ループ防止(通常 64 or 128) |
| プロトコル番号 | TCP=6, UDP=17, ICMP=1 |
| 識別子・フラグ・フラグメントオフセット | MTU超過時のフラグメント再組み立てに使う |
Ethernetヘッダ(14バイト)
| フィールド | 役割 |
|---|---|
| 宛先 MAC | 「次のホップ」のMAC(ルーターのMACになる場合が多い) |
| 送信元 MAC | 自分の NIC の MAC |
| EtherType | 中身が IP なら 0x0800、ARP なら 0x0806 |
3ウェイハンドシェイク:TCPコネクション確立
TCPは信頼性のある通信のため、データ送信前に「コネクション」を確立する。
Client Server
│ │
│──── SYN (seq=1000) ─────────────────────→│ 「接続したい。私のSEQは1000から始める」
│ │
│←─── SYN-ACK (seq=5000, ack=1001) ────────│ 「了解。私は5000から。あなたの1001を待つ」
│ │
│──── ACK (seq=1001, ack=5001) ───────────→│ 「了解。あなたの5001を待つ」
│ │
│ ≪ コネクション確立。データ送信開始 ≫ │
SEQ番号が重要な理由
SEQ番号はバイトストリームの「通し番号」。
- パケットが順序通り届かなかった場合でも受信側で並べ直せる
- 同じパケットが重複して届いても識別して捨てられる
- どこまで届いたかを ACK で確認するため、欠損を検知できる
送信側が 3000バイト のデータを 1000バイト ずつ分割して送る場合:
Segment 1: seq=1001, len=1000 → 次のseq=2001 を期待
Segment 2: seq=2001, len=1000 → 次のseq=3001 を期待
Segment 3: seq=3001, len=1000 → 次のseq=4001 を期待
受信側は "ack=4001" を返すことで「4001バイト目まで全部受け取った」と伝える
送信側OS内の処理:カーネルの中を追う
アプリケーションが write() システムコールを呼んでからNICにデータが渡るまでの流れ。
┌─────────────────────────────────────────────┐
│ アプリケーション │
│ socket.write("GET / HTTP/1.1\r\n...") │
└──────────────────────┬──────────────────────┘
│ write() syscall
↓
┌─────────────────────────────────────────────┐
│ ソケットバッファ(カーネル空間) │
│ 送信キューにデータを積む │
└──────────────────────┬──────────────────────┘
│
↓
┌─────────────────────────────────────────────┐
│ TCP スタック │
│ ・MSS単位にセグメント化(通常1460バイト) │
│ ・SEQ番号・ACK番号・チェックサムを計算 │
│ ・フロー制御(ウィンドウサイズ確認) │
└──────────────────────┬──────────────────────┘
│
↓
┌─────────────────────────────────────────────┐
│ IP スタック │
│ ・IPヘッダ付与(src/dst IP, TTL) │
│ ・ルーティングテーブルを参照 │
│ → 宛先IPに応じてどのNICから出すか決定 │
│ ・MTU超過なら分割(フラグメンテーション) │
└──────────────────────┬──────────────────────┘
│
↓
┌─────────────────────────────────────────────┐
│ ARP(Address Resolution Protocol) │
│ ・「次のホップ(デフォルトGW)のMACは?」 │
│ ・ARPキャッシュにあれば再利用 │
│ ・なければ ARP Request をブロードキャスト │
│ → Reply でMACアドレスを取得してキャッシュ │
└──────────────────────┬──────────────────────┘
│
↓
┌─────────────────────────────────────────────┐
│ NIC ドライバ │
│ ・Ethernetヘッダ + FCS を付与 │
│ ・送信リングバッファ(DMA)へ書き込む │
└──────────────────────┬──────────────────────┘
│
↓
物理 NIC
(電気/光/電波信号として送出)
ARPの詳細:IPアドレスからMACアドレスへ
IPアドレスは「論理的な住所」、MACアドレスは「物理的なハードウェア識別子」。同じネットワーク内での実際の配送にはMACアドレスが必要。
送信PC (192.168.1.10) ブロードキャスト GW (192.168.1.1)
│ │
│── ARP Request ─────────────────────────────→全員│
│ 「192.168.1.1 の MAC アドレスは誰?」 │
│ │
│←── ARP Reply ─────────────────────────────────│
│ 「私です。MAC は AA:BB:CC:DD:EE:FF」 │
│ │
│ ARPキャッシュに保存(通常20分程度で失効) │
ルーターでの中継:Hop-by-Hop ルーティング
パケットはルーターを「ホップ」しながら宛先へ近づく。各ルーターはIPアドレスだけを見て次のホップを決める。
送信PC Router A Router B 受信PC
192.168.1.10 (ISP境界) (バックボーン) 203.0.113.5
│ │ │ │
│──フレーム1──→ │ │ │
│ src MAC: PC │ │ │
│ dst MAC: GW │ │ │
│ src IP: 1.10 │ │ │
│ dst IP: 113.5 │ │ │
│ │ │ │
│ ルーティング │ │
│ テーブル参照 │ │
│ → Router B へ │ │
│ │ │ │
│ │──フレーム2──→ │ │
│ │ src MAC: RA │ │
│ │ dst MAC: RB │ │ ← MACアドレスは書き換わる
│ │ src IP: 1.10 │ │ IPアドレスは変わらない
│ │ dst IP: 113.5 │ │ (NATがない場合)
│ │ │ │
│ │ ルーティング │
│ │ テーブル参照 │
│ │ → 受信PC へ │
│ │ │ │
│ │ │──フレーム3──→ │
│ │ │ src MAC: RB │
│ │ │ dst MAC: 受信│
│ │ │ src IP: 1.10 │
│ │ │ dst IP: 113.5│
ルーティングテーブルの例(ip route コマンドの出力イメージ):
Destination Gateway Genmask Iface
0.0.0.0 192.168.1.1 0.0.0.0 eth0 ← デフォルトルート(どこか不明なら全部ここへ)
192.168.1.0 0.0.0.0 255.255.255.0 eth0 ← 直結ネットワーク(同セグメント)
10.0.0.0 10.0.0.1 255.0.0.0 eth1 ← 社内ネットワーク経由
TTLは各ルーターで1ずつ減らされる。0になるとルーターがパケットを破棄して ICMP Time Exceeded を送信元に返す(traceroute はこれを利用している)。
受信側の処理:デカプセル化
受信側ではカプセル化の逆順(剥がしていく)で各層が処理する。
物理 NIC
(電気/光/電波信号を受信)
│
↓
┌─────────────────────────────────────────────┐
│ NIC ドライバ │
│ ・フレームを受け取る │
│ ・FCS(チェックサム)でビット誤り確認 │
│ ・自分宛(MACアドレス一致 or ブロードキャスト)│
│ でなければ破棄 │
└──────────────────────┬──────────────────────┘
│ Ethernetヘッダを剥がす
↓
┌─────────────────────────────────────────────┐
│ IP スタック │
│ ・IPヘッダのチェックサム確認 │
│ ・宛先IPが自分でなければルーティング or 破棄 │
│ ・フラグメントなら再組み立て │
│ ・プロトコル番号を見てTCPスタックへ渡す │
└──────────────────────┬──────────────────────┘
│ IPヘッダを剥がす
↓
┌─────────────────────────────────────────────┐
│ TCP スタック │
│ ・チェックサム確認 │
│ ・SEQ番号で順序並べ替え・重複排除 │
│ ・ACKを返す │
│ ・ポート番号で対応するソケットを特定 │
└──────────────────────┬──────────────────────┘
│ TCPヘッダを剥がす
↓
┌─────────────────────────────────────────────┐
│ ソケットバッファ(受信キュー) │
│ アプリケーションが read() を呼ぶまで待機 │
└──────────────────────┬──────────────────────┘
│ read() syscall
↓
アプリケーション
(HTTPレスポンスとして処理)
ソフトウェアエンジニアへの実践的示唆
tcpdump / Wireshark:どのレイヤーが見えるか
# 特定ポートの通信をキャプチャ(L4まで見える)
tcpdump -i eth0 -n 'tcp port 443' -w capture.pcap
# 詳細ヘッダ表示(TTL, SEQ番号など)
tcpdump -i eth0 -v 'host 203.0.113.5'
# ARPのやり取りを確認
tcpdump -i eth0 arp
Wireshark では各フレームをクリックするとカプセル化された各層のヘッダが展開表示される。「ネットワーク遅延なのかアプリ処理遅延なのか」の切り分けに非常に有効。
ポート番号:プロセスへの仕分け機能
OS はパケットが届くと (宛先IP, 宛先ポート, プロトコル) の組み合わせで「どのプロセスのソケットか」を特定する。
# 開いているポートと対応プロセスを確認
ss -tlnp
# または
lsof -i :8080
ポートが被るとバインドエラーになる理由はこれ。 TIME_WAIT 状態の古いコネクションがポートを占有していると新しいバインドができない。
MSS / MTU:大きいデータを送る時の断片化
MTU (Maximum Transmission Unit):Ethernetフレームに乗せられるIPパケットの最大サイズ
通常 1500 バイト
MSS (Maximum Segment Size):TCPセグメントのデータ部の最大サイズ
MSS = MTU - IPヘッダ(20) - TCPヘッダ(20) = 1460 バイト
※ 3ウェイハンドシェイク時に SYN パケット内でネゴシエーションする
MSSを超えるデータはTCPがセグメント分割する。MTUを超えるパケットはIP層でフラグメント化される(DF ビット が立っていると経路上でフラグメントできず ICMP 到達不能が返る = Path MTU Discovery)。
# 経路MTUを確認(Linuxの場合)
ip route get 203.0.113.5
# → "cache expires Xs mtu 1500" のような出力
TCP再送タイムアウト:アプリが「固まる」原因を追う
ACK が返ってこないとTCPは再送する。再送間隔は指数バックオフ(1s → 2s → 4s → …)で増加し、最終的にコネクションをリセットする。
アプリが "応答なしで固まる" 原因の多くは:
1. 相手サーバのプロセスがクラッシュ(TCP FINが届かないのでタイムアウトまで待つ)
2. ファイアウォールがサイレントにパケットを破棄(RSTではなくDROPするため)
3. ネットワーク機器のバッファ溢れによるパケットロス
確認手順:
1. tcpdump でRetransmissionが出ていないか確認
2. ss -s でretransカウントを確認
3. アプリ側でconnect/read timeoutを適切に設定する
TIME_WAIT 状態と高負荷サーバの注意点
TCP コネクションを切る側(close() を先に呼んだ側)は TIME_WAIT 状態に入り、2MSL(通常 60〜120秒) 待機する。
TIME_WAIT が問題になるパターン:
- 短命なコネクションを大量に張るサービス(HTTP/1.0、外部APIコール等)
- ポートが枯渇して新規接続できなくなる(65535個の制限)
対策:
1. HTTP Keep-Alive(コネクション再利用)で接続回数を減らす
2. SO_REUSEADDR ソケットオプションで TIME_WAIT ポートを再利用
3. Linux の ip_local_port_range を広げる
sysctl net.ipv4.ip_local_port_range = "1024 65535"
4. tcp_tw_reuse を有効化(慎重に)
sysctl net.ipv4.tcp_tw_reuse = 1
確認コマンド:
ss -s # TIME-WAIT の数を確認
実際の通信に strace で追う
# アプリがソケットで何をしているか低レベルで追う
strace -e trace=network curl https://example.com
# 出力例(抜粋)
# socket(AF_INET6, SOCK_STREAM, ...) = 3
# connect(3, {sa_family=AF_INET, sin_port=htons(443), ...}, 16) = 0
# sendto(3, "GET / HTTP/1.1\r\nHost: ...", ...)
# recvfrom(3, "HTTP/1.1 200 OK\r\n...", ...)
まとめ:レイヤーごとの責務と観察ポイント
┌──────────────────┬─────────────────────────┬──────────────────────┐
│ 層 │ 責務 │ 観察ツール │
├──────────────────┼─────────────────────────┼──────────────────────┤
│ アプリケーション │ 意味のあるデータの交換 │ curl, browser devtools│
│ トランスポート │ 信頼性・順序・多重化 │ ss, netstat, tcpdump │
│ インターネット │ エンドツーエンドの経路 │ traceroute, ping │
│ ネットワーク I/F │ 隣のホップへの物理配送 │ arp, ip neighbor │
└──────────────────┴─────────────────────────┴──────────────────────┘
パケット通信を理解すると「なぜタイムアウトが起きるか」「なぜ特定の環境だけ遅いか」「なぜポートが枯渇するか」といったトラブルの根本原因に素早く辿り着けるようになる。デバッグの際は「どのレイヤーで問題が起きているか」を絞り込む視点が重要。
- 1. 📡TCP/IPパケット通信の全過程
- 2. 🔌物理層・データリンク層詳解
- 3. 🌐ネットワーク層(IP)詳解
出典: TCP/IP入門・図解入門