1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

curl を叩いたら裏側で何が起きている?ネットワーク通信の「なぜ」を全部調べてみた

1
Posted at

curl を叩けばデータが返ってくる。fetch() を書けば API が応答する。

でも、「その通信、裏側で何が起きてるか説明できる?」 と聞かれると、正直詰まりました。

TCP/IP は「なんかネットワークのやつ」。HTTPS の S は「セキュアの S でしょ?」くらいの理解。API がタイムアウトしたら、とりあえずもう一回叩く。それで動けば OK。

でも、それだと障害が起きたとき何もできないんですよね。そこで今回、curl https://example.com の裏側で何が起きているのかを1つずつ調べてみました。

TL;DR

この記事で得られること

  • curl https://example.com の裏で DNS → TCP → TLS → HTTP が順番に動いていることがわかる
  • 「なぜ TCP は遅いのか」「なぜ HTTPS が必要なのか」を設計判断の視点で理解できる
  • 「コネクションプール」「べき等性」「TTL」などのキーワードが腹落ちする

対象読者

  • curlfetch() で HTTP リクエストは書けるけど、裏で何が起きてるかは知らない方
  • 「TCP/IP」「DNS」「TLS」を聞いたことはあるけど「何がどう違うの?」という方
  • ネットワークの勉強を始めたいけど、何から手をつければいいかわからない方

まず全体像を掴む — 1回の通信で何が起きているのか

ネットワークの勉強って、TCP から? DNS から?何から始めればいいの?

自分もそう思ったのですが、調べてみてわかったのは、まず全体の流れを知ることが大事だということでした。個別の技術は、全体の中での位置づけがわかってから学んだ方が圧倒的に頭に入ります。

curl https://example.com の裏側

このコマンドを叩いた瞬間、裏側ではこれだけのことが起きています。

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  curl https://example.com を叩いた瞬間                         │
│                                                              │
│  ① DNS    「example.com の住所(IP)は?」                     │
│    │       → 93.184.216.34 だよ                               │
│    ▼                                                         │
│  ② TCP    「93.184.216.34 さん、話せますか?」                  │
│    │       → お互いに確認して、確実に届く通信路を作る               │
│    ▼                                                         │
│  ③ TLS    「盗聴されないように暗号化しよう」                      │
│    │       → 相手が本物か確認して、暗号化された通信路を作る          │
│    ▼                                                         │
│  ④ HTTP   「GET / をください」                                 │
│    │       → 200 OK + HTML が返ってくる                        │
│    ▼                                                         │
│  ⑤ 表示    結果が表示される                                     │
│                                                              │
│  → この ①〜④ が全部成功して初めて「通信」が成立する                │
│                                                              │
└──────────────────────────────────────────────────────────────┘

たった1行のコマンドの裏で、4つの技術が順番に動いています。この記事では、この ①〜④ を1つずつ調べていきます。

なぜ通信は「層」に分かれているのか

DNS とか TCP とか TLS とか、なんでこんなにたくさんあるの?1つにまとめればよくない?

調べてみると、通信は 4つの層(レイヤー) に分かれて設計されていました。

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  【プロトコルスタック(通信の4層構造)】                           │
│                                                              │
│  ┌──────────────────────────────────┐                        │
│  │  アプリケーション層(HTTP, DNS)     │    ← 私たちが普段触る    │
│  ├──────────────────────────────────┤                        │
│  │  トランスポート層(TCP, UDP)        │                        │
│  ├──────────────────────────────────┤                        │
│  │  インターネット層(IP)              │                        │
│  ├──────────────────────────────────┤                        │
│  │  リンク層(イーサネット, Wi-Fi)     │  ← ハードウェアに近い     │
│  └──────────────────────────────────┘                        │
│                                                              │
│  上の層は、下の層の複雑さを「隠して」くれている                      │
│                                                              │
└──────────────────────────────────────────────────────────────┘

「7層じゃないの?」と思った方へ

教科書でよく見る OSI 参照モデル(7層) とは別に、実際のインターネットで使われている TCP/IP モデル(4層) があります。この記事では、実際の通信の流れを追うので TCP/IP モデルを使っています。OSI の7層は「理論の整理」、TCP/IP の4層は「現実の実装」と捉えるとわかりやすいです。

なぜ分けるのか。理由は関心の分離です。

Web アプリを作るとき、Wi-Fi の電波がどう飛ぶかなんて気にしたくないですよね。各層が下の層の複雑さを隠してくれるおかげで、普段はアプリケーション層だけに集中できるわけです。もし1つにまとめてしまったら、1箇所の変更が全部に影響してしまいます。分けることで、各層が独立して進化できるようになっています。

でも「隠された複雑さ」は漏れ出す

普段はフレームワークが全部隠してくれます。でも障害が起きたとき、下の層の知識がないと原因がわからないことがあります。

  • API が急に遅くなった → 原因は TCP の輻輳制御(ネットワークの混雑を避けるために速度を落とす仕組み) だった
  • 接続が突然切れた → 原因は TLS 証明書の期限切れ だった
  • 名前解決に失敗した → DNS サーバーがダウン していた

これを 「抽象化の漏れ(Leaky Abstraction)」 と呼びます。普段は隠してくれている下の層の複雑さが、障害時に「漏れ出してくる」イメージです。上の層で問題が起きたように見えて、実は下の層が原因だった、というケースです。

だから下の層を知る意味がある。 ということで、ここから DNS、TCP、TLS、HTTP を順番に見ていきます。

「分散コンピューティングの8つの誤謬」

「ネットワークは信頼できる」「レイテンシはゼロ」「帯域幅は無限」— これらは、エンジニアが陥りがちな間違った前提です(1994年、L. ピーター・ドイチュら)。この記事で調べた内容は、まさにこれらの「誤謬」がなぜ危険なのかに直結しています。

相手を見つける — DNS はなぜ「分散」なのか

example.com って打ったら繋がるけど、コンピュータはどうやって相手の場所を知るの?

コンピュータは「名前」を理解できません。通信には相手の IP アドレス(住所) が必要です。名前から住所を引く仕組み、それが DNS(Domain Name System) です。

そもそもなぜ DNS が必要なのか

人間は 93.184.216.34 のような数字の羅列を覚えられません。example.com という名前で呼びたい。でも機械は名前を理解できない。

つまり、名前 → IP アドレスの変換表がどこかに必要です。これが DNS の本質です。いわばインターネットの電話帳のようなものです。

なぜ1台のサーバーに全部持たせなかったのか

変換表を1台のサーバーに全部入れておけば簡単じゃない?

たしかに、それが一番シンプルです。でも現実には無理でした。

  • 全世界のドメイン名は数億件ある
  • 全世界からの問い合わせが1台に集中したら即パンクする
  • その1台が壊れたら全インターネットが止まる

設計判断:1台で全部は無理。だから分散・階層構造にする。

DNS は、以下のように「次に誰に聞けばいいか」だけを知っているサーバーが階層的につながっています。

DNS の名前解決の流れ

www.example.com にアクセスするとき、裏側ではこんなやり取りが起きています。

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  あなた: 「www.example.com の IP は?」                         │
│                                                              │
│  ① ブラウザのキャッシュ → ない                                  │
│  ② OS のキャッシュ → ない                                      │
│  ③ 自分の代わりに調べてくれる DNS サーバーに問い合わせ              │
│     │                                                        │
│     ├→ ルート NS(最上位の案内係)                               │
│     │   「.com のことは .com 担当に聞いて」                      │
│     ├→ .com TLD NS(.com 専門の案内係)                         │
│     │   「example.com は権威 NS に聞いて」                      │
│     └→ 権威 NS(example.com の正式な管理者)                     │
│         「93.184.216.34 だよ」                                │
│                                                              │
│  → 最悪4回の往復。でもキャッシュのおかげで普段は速い                 │
│                                                              │
└──────────────────────────────────────────────────────────────┘

③の「自分の代わりに調べてくれる DNS サーバー」は、正式には リゾルバ(Resolver) と呼ばれます。自宅のインターネット回線を提供しているプロバイダや、Google(8.8.8.8)などが運営しています。NS は Name Server(ネームサーバー) の略で、DNS の問い合わせに答えるサーバーのことです。

ポイントは、ルートサーバーは「全部を知っている」のではなく、「次に誰に聞けばいいか」だけを知っているということです。各サーバーが自分の担当範囲だけを知っていればいい。これが「分散」の力です。

キャッシュの TTL — 速さと鮮度のトレードオフ

毎回4往復するの?遅くない?

その通りです。だから DNS は結果をキャッシュします。一度調べた結果を覚えておけば、次からは即座に答えられます。

でも、ここに問題があります。キャッシュした情報が古くなったらどうするのか?

そこで TTL(Time To Live = 生存期間) を設定します。「この情報は○秒間有効」というルールです。

ただし、これはトレードオフです。

  • TTL が長い: DNS サーバーの負荷は低い。名前解決も速い。でも IP アドレスが変わったとき、反映が遅れる
  • TTL が短い: 変更がすぐ反映される。でも 毎回問い合わせが発生して遅くなる

完璧な正解はありません。状況に応じて「速度と鮮度のどちらを優先するか」を選ぶ必要があるのです。

実務のテクニック

普段は TTL を長めに設定しておき、サーバー移行の前だけ TTL を短くする、というテクニックがよく使われます。移行時には素早い切り替えが必要ですが、普段はキャッシュの恩恵を受けたいからです。

DNS がダウンしたらどうするか — 静的安定性

DNS サーバーが落ちたらどうなるの?全部止まる?

TTL が切れたタイミングで DNS に問い合わせたら、DNS サーバーがダウンしていた。さあどうなるか。

  • 選択肢 A: キャッシュを捨てる → 名前解決できない → サービス全停止
  • 選択肢 B: 古いキャッシュを使い続ける → IP が変わっていなければ問題ない → サービス継続

設計判断:完璧なデータがなくても、古いデータで動き続ける方がマシ。

サーバーの IP アドレスが頻繁に変わることは稀です(自宅の IP は動的に変わりますが、DNS で名前解決する対象はサーバー側なので、通常は固定 IP です)。それなら、DNS がダウンしたときに「確認できないから全部止める」より、「古い情報でも動き続ける」方がシステムとしては遥かに堅牢です。

この「完璧より継続」という考え方は、分散システム全体で繰り返し登場する重要なパターンです。

確実に届ける — TCP はなぜ「遅くなってでも」信頼性を選んだのか

DNS で IP アドレスがわかった。あとはデータを送ればいいだけでは?

そう簡単ではありません。インターネットでデータを送る仕組みである IP(Internet Protocol) は、届く保証をしてくれないのです。

IP だけだと何が起きるか

IP は「ベストエフォート(最善努力)」で動きます。「届けようとはするけど、届かなくても知りません」というスタンスです。

実際には、こんなことが日常的に起きています。

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  送信側が「A, B, C, D, E」の5つのパケットを送った場合:             │
│                                                              │
│  ✗ B が消えた        → A, C, D, E          (パケットロス)      │
│  ✗ C が2回届いた     → A, B, C, C, D, E    (重複)             │
│  ✗ 順番が入れ替わった → A, D, B, C, E       (順序逆転)          │
│  ✗ D が壊れた        → A, B, C, D(破損), E (データ破損)        │
│                                                              │
│  → この上でまともなアプリケーションは作れない                       │
│                                                              │
└──────────────────────────────────────────────────────────────┘

パケットが消える。重複する。壊れる。順番が入れ替わる。ルーターが過負荷になったらパケットを容赦なく破棄します。

設計判断:この上でまともなアプリは作れない。速度を犠牲にしてでも「確実に届く」仕組みが必要。それが TCP。

どうやって「届いたか」を確認するか — シーケンス番号と ACK

届いたかどうかなんて、どうやってわかるの?

TCP は書留郵便と同じ仕組みを使います。

まず、送信するデータをセグメントという小さな塊に分割し、それぞれに 連番(シーケンス番号) を付けます。受信側は番号を見て「3番が来てない!」と欠落を検知できますし、「2番がまた来たけど、もう持ってるから捨てよう」と重複にも対処できます。

そして受信側は、セグメントを受け取るたびに 「ここまで受け取ったよ」 という返事を送ります。これを ACK(確認応答) と呼びます。

ACK が返ってこなければ、送信側のタイマーが作動し、セグメントを再送します。さらに、チェックサムで中身が壊れていないかも検証します。

代償:ACK を待つ分、通信が遅くなります。再送が発生するとさらに遅くなります。 でもそれは「確実に届く」ために払う対価です。

なぜ通信の前に「握手」するのか — 3ウェイ・ハンドシェイク

なぜいきなりデータを送らないの?準備に時間がかかるだけでは?

TCP では、データを送る前にまず3回のやり取りで「お互いに聞こえているか」を確認します。これを 3ウェイ・ハンドシェイク と呼びます。

なぜ3回なのか?

  • 2回だと:サーバーは「自分の返事がクライアントに届いたか」を確認できません
  • 4回だと:3回で十分な確認ができるので無駄です
  • 3回が「双方向の疎通確認」に必要な最小回数です

ハンドシェイクではシーケンス番号も交換している

実は SYN を送るとき、先ほど説明したシーケンス番号の「開始値」もお互いに伝え合っています。この開始値は毎回ランダムに決められます。もし毎回 0 から始めると、前の接続の残骸パケットが新しい接続のデータと混同されるリスクがあるためです。

代償:データ送信前に1往復分の時間(RTT = ラウンドトリップタイム)が無駄になります。 サーバーがクライアントから地理的に離れているほど、この「コールドスタート・ペナルティ」は大きくなります。

接続の開閉はなぜ高コストなのか — コネクションプール

通信が終わったら接続を閉じればいいんじゃないの?

TCP の接続は、開くのも閉じるのもコストがかかります。

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  接続の開閉にかかるコスト                                        │
│                                                              │
│  開く: SYN → SYN/ACK → ACK          (1.5 RTT)               │
│  閉じる: FIN → ACK → FIN → ACK      (2 RTT)                  │
│    ※ FIN = 「もう送るデータはありません」という終了合図              │
│                                                              │
│  → データを送る前後だけで 3.5 RTT も消費する                       │
│                                                              │
│  毎回これをやるのはあまりに非効率                                  │
│                                                              │
│  プールなし(API 10回):                                        │
│    [開く][データ][閉じる] × 10 = 5秒以上が準備と後片付け            │
│                                                              │
│  プールあり(API 10回):                                        │
│    [開く][データ1][データ2]...[データ10][閉じる]                  │
│    → 開閉は1回だけ = 準備と後片付けは 0.5秒だけ                    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

たとえば東京からアメリカ西海岸のサーバーに接続する場合、RTT は約 150ms です。3.5 RTT = 525ms。データを1バイトも送っていないのに、開閉だけで0.5秒以上かかります。API を10回呼ぶなら、それだけで 5秒以上がただの準備と後片付けに消えます。

設計判断:接続の開閉コストが高い。だからコネクションプールで使い回す。

「なぜデータベース接続をプールするのか?」「なぜ HTTP クライアントで Keep-Alive を使うのか?」の根本理由がここにあります。TCP の接続コストを知っていれば、これらのベストプラクティスが「なるほど」と腹落ちするはずです。

TIME_WAIT — 閉じた後も残るコスト

実は接続を閉じた後も、ソケットは数分間 TIME_WAIT という待機状態で残ります。遅れて届いたパケットが次の接続のデータと混同されるのを防ぐためです。大量の接続を高速に開閉すると TIME_WAIT が溜まり続け、新しい接続が作れなくなることもあります。これもコネクションプールが重要な理由の1つです。

相手を溢れさせない — フロー制御

送る側が速ければ速いほどいいんじゃないの?

直感的にはそう思います。でも、送信側が全力で送り続けたら、受信側が処理しきれません。受信バッファが溢れてデータが捨てられ、再送が発生し、かえって遅くなります。

設計判断:送信側が「相手のペースに合わせる」仕組みが必要。

TCP では、受信側が ACK を送るたびに 「あとXバイト受け入れられる」 と送信側に伝えます。送信側はその範囲内でだけデータを送ります。バッファが一杯になったら、送信側は送信を停止します。

イメージとしては寿司屋のカウンターです。大将(送信側)がどんどん握っても、お客さん(受信側)の目の前の皿が一杯なら、もう置く場所がありません。お客さんが食べて皿が空いたら、次のネタを握る。これがフロー制御です。

ネットワーク全体を守る — 輻輳制御

フロー制御で相手を守れるなら、それで十分では?

フロー制御は「相手の限界」を守る仕組みでした。では、ネットワーク全体の限界は誰が守るのか。

全員が一斉にデータを送りまくったら、途中のルーターが溢れます。ルーターはパケットを大量に破棄し始めます。すると全員が再送を始めて、さらにネットワークが混雑する。これが 輻輳崩壊 です。

設計判断:個々の接続が「自分の取り分」を動的に調整する仕組みが必要。

TCP は 混雑ウィンドウ(Congestion Window) を管理します。これは「ACK を待たずに送れるデータ量の上限」です。

  • ACK が順調に返ってくる → ウィンドウを拡大(もっと送って大丈夫そうだ)
  • パケットが消えた → ウィンドウを縮小(混雑してるから控えよう)

新しい接続が始まったとき、ウィンドウは小さな値からスタートし、徐々に大きくしていきます。これをスロースタートと呼びます。「このネットワークがどれくらい耐えられるか」を慎重に探っているわけです。

帯域幅の公式:Bandwidth = WinSize / RTT

帯域幅(1秒間に送れるデータ量)は、ウィンドウサイズ(一度に送れる量)を RTT(往復時間)で割った値です。つまり、通信速度はネットワークの遅延に大きく左右されるということです。

  • RTT 100ms + ウィンドウ 64KB → 最大 640KB/s
  • RTT 10ms + ウィンドウ 64KB → 最大 6.4MB/s

同じウィンドウサイズでも、RTT が10倍短ければ帯域幅は10倍。これが CDN(サーバーをユーザーの近くに置く)の根本理由です。サーバーが近ければ RTT が短くなり、同じ仕組みでもより多くのデータを送れます。

信頼性を「捨てる」という選択 — UDP

TCP がこんなに頑張ってるなら、全部 TCP でよくない?

TCP の代償をあらためて整理してみます。

  • ハンドシェイク → 接続開始が遅い
  • ACK + 再送 → パケットロス時にさらに遅延
  • フロー制御 + 輻輳制御 → 帯域幅を最大限使えない

もし、これらを全部捨てたらどうなるか。それが UDP(User Datagram Protocol) です。

UDP には接続の概念がありません。ACK も再送もフロー制御も輻輳制御もありません。「送りっぱなし。届いたかは知らない。」という極めてシンプルなプロトコルです。

なぜそんなものが必要なのか? ビデオ通話を例に考えてみます。

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  【TCP でビデオ通話した場合】                                    │
│                                                              │
│  フレーム1 → 届いた → 表示                                      │
│  フレーム2 → 消えた → 再送を要求... 待つ... 待つ...                │
│  フレーム3 → 届いてるけどフレーム2を待って保留...                   │
│  → 映像がフリーズする                                           │
│                                                              │
│  【UDP でビデオ通話した場合】                                    │
│                                                              │
│  フレーム1 → 届いた → 表示                                      │
│  フレーム2 → 消えた → 無視(一瞬乱れるだけ)                       │
│  フレーム3 → 届いた → すぐ表示                                   │
│  → 多少乱れても映像は流れ続ける                                   │
│                                                              │
└──────────────────────────────────────────────────────────────┘

リアルタイム通信では、古いデータを再送するより、最新のデータを即時表示する方が大事です。1秒前のフレームが遅れて届いても、もう意味がありません。ゲームのリアルタイム対戦も同じ理由で UDP が使われています。

HTTP/3 は UDP ベース

最新の HTTP/3 は、TCP ではなく UDP 上に構築された QUIC というプロトコルを使っています。TCP では1本の通り道でデータを順番に送るので、途中の1つが詰まると後ろが全部待たされます(行頭ブロッキング)。QUIC は複数の通り道(ストリーム)を並行して使えるので、1つの通り道が詰まっても、他の通り道はそのまま流れ続けます。

これは「信頼性 vs リアルタイム性」のトレードオフです。 どちらが正しいかではなく、用途に応じて選ぶもの。TCP が常に正解ではないし、UDP が常に正解でもありません。

盗み見させない — TLS はなぜ「遅くなってでも」安全性を選んだのか

TCP で確実に届くようになったなら、もう通信は大丈夫じゃない?

届くようにはなりました。でも、届いた中身が誰かに読まれていたら? TCP は暗号化してくれません。

なぜ暗号化が必要なのか

TCP の通信内容は 平文(クリアテキスト) です。ネットワーク上のどこかで、誰かが通信を見ている可能性があります。

  • 同じ Wi-Fi に接続している他人
  • ISP(インターネットプロバイダ)
  • 経路上にいる悪意のあるルーター

パスワード、クレジットカード番号、個人情報。これらが平文で流れていたら致命的です。

設計判断:CPU コストを払ってでも、通信を暗号化する価値がある。それが TLS。

TLS は大きく分けて暗号化認証整合性の3つを提供します。そして、この3つには「なぜ3つとも必要なのか」という明確な理由があります。

鍵をどう共有するか — 公開鍵暗号の巧妙さ

暗号化するには鍵が必要。でもその鍵をネットワークで送ったら盗まれない?

まさにその通りです。暗号化には送信側と受信側で共通の鍵が必要ですが、その鍵をネットワーク経由で送ったら盗聴される可能性があります。

設計判断:「鍵そのものを送らずに共有する」仕組みが必要。それが公開鍵暗号。

ここで登場する鍵は2種類あります。役割がまったく違うので、先に整理します。

鍵の種類 何に使うか 例えるなら
🔓公開鍵と🔑秘密鍵(ペアで使う) 「安全に渡す」ためだけに使う 開いた南京錠(🔓)は誰でも閉められるが、開けるには鍵(🔑)が必要
🤝共通鍵 実際のデータの暗号化に使う 二人だけが知っている合言葉

🔓🔑はペアで、🤝を安全に渡すためだけに使います。なぜ分かれているのか? 🤝は高速だけど、最初に相手に安全に渡す方法がない。 そこで、🔓🔑を「🤝を安全に渡すための仕組み」として使います。

盗聴者は🔓と施錠された箱を見ることはできます。しかし、🔓で閉じた箱は🔑でしか開けられません。🤝そのものがネットワーク上を流れることはない、これが安全の根拠です。

ただし、🔓🔑を使う暗号化は処理が重いのです。🤝での暗号化に比べて何十倍も遅い。

設計判断:最初だけ🔓🔑で🤝を安全に渡し、以後は高速な🤝で通信する(ハイブリッド方式)。

安全性と速度を両立させるための、巧妙な判断だと思いました。

暗号化だけでは足りない — 認証の必要性

暗号化したら安全でしょ?

いいえ。通信相手がニセモノだったら、暗号化しても意味がありません。

これを 中間者攻撃(Man In The Middle = MITM) と呼びます。

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  あなたが思っている通信:                                        │
│    あなた ←──暗号化──→ 銀行サーバー                              │
│                                                              │
│  実際に起きていること(MITM):                                   │
│    あなた ←──暗号化──→ 攻撃者 ←──暗号化──→ 銀行サーバー            │
│                                                              │
│  攻撃者はあなたとの通信を復号して中身を見て、                        │
│  銀行サーバーとは別に暗号化通信している                             │
│                                                              │
└──────────────────────────────────────────────────────────────┘

暗号化していても、「相手が本物の銀行サーバーである」ことを確認しなければ、攻撃者に全て筒抜けです。

設計判断:「相手が本物であること」を証明する仕組みが必要。それが証明書。

TLS では、サーバーが証明書を提示します。証明書には、所有者情報、公開鍵、そして認証局(CA)の署名が含まれています。

ブラウザは証明書チェーンを辿り、最終的に自分が信頼しているルート CAに行き着けば「本物」と判断します。OS ベンダーによって、主要なルート CA の情報はあらかじめ組み込まれています。

実務の教訓

証明書の期限切れは非常によくある障害原因です。これだけでサービス全体がダウンし、ユーザーが一切接続できなくなります。Let's Encrypt のような自動更新の仕組みへの投資は、絶対に惜しまないでください。

暗号化+認証でもまだ足りない — 整合性

暗号化して、相手も本物って確認した。もう完璧では?

もう1つだけ問題が残っています。暗号化されたデータでも、中間者が 「暗号化されたまま」ビットを書き換える 可能性があるのです。

設計判断:メッセージが「一文字も改ざんされていない」ことを検証する仕組みが必要。それが HMAC。

HMAC(Hash-based Message Authentication Code)は、メッセージと秘密鍵を組み合わせて 指紋のようなもの(ハッシュ値) を計算する仕組みです。受信側が同じ計算をして、送られてきたハッシュ値と一致すれば「改ざんなし」と判断します。

「TCP もチェックサムで整合性を守っているのでは?」と思うかもしれません。確かに TCP もチェックサムを使いますが、秘密鍵を使わないので、攻撃者がデータとチェックサムの両方を書き換えれば突破できます。HMAC は秘密鍵がないと正しい値を計算できないため、意図的な改ざんを防げます。

TLS ハンドシェイクのコスト

TLS を使うには、TCP ハンドシェイクに加えて TLS ハンドシェイクも必要です。

  • TCP ハンドシェイク:1.5 RTT
  • TLS ハンドシェイク(TLS 1.3):1 RTT
  • 合計:2.5 RTT

RTT が 100ms なら、データ送信を始めるまでに 250ms かかります。

設計判断:だからこそ「接続の再利用」が重要。 TCP のセクションで学んだコネクションプールは、TLS のコストも含めて考えると、さらに重要性が増します。

やり取りする — HTTP/API はなぜ「ステートレス」を選んだのか

ここまでで安全な通信路はできた。あとはデータを送ればいいだけでは?

その「データの送り方」にもルールが必要です。「どんな形式で送るか」「エラーのときどうするか」を決めないと、お互い理解できません。

人間同士の会話でも「日本語で話す」「敬語を使う」というルールがあるように、機械同士にも共通語のルールが必要です。それが HTTP であり、その上に構築される API の設計ルールです。

なぜステートレスなのか

前のリクエストの文脈を覚えてくれた方が便利じゃない?

ステートフルな通信(電話のようなもの)では、前の会話の文脈を覚えています。「昨日の件ですが」と言えば、何の件か分かります。

一方、ステートレスな HTTP では、各リクエストが完全に独立しています。前のリクエストのことを一切覚えていません。毎回「私は○○です。△△のデータをください」と全情報を送る必要があります。

一見不便に思えますが、調べてみるとここには明確な判断がありました。

設計判断:柔軟性を犠牲にしてでもステートレスにし、スケーラビリティを確保する。

もしサーバーが「このクライアントとの会話の文脈」を覚えていたら:

  • そのサーバーが落ちた → 文脈が消える。会話の最初からやり直し
  • リクエストを別のサーバーに振り分けたい → 文脈がないから処理できない

ステートレスなら、どのサーバーでもリクエストを処理できます。ロードバランサーで自由に振り分けられるし、1台が落ちても別のサーバーが代わりに処理できます。これがスケーラビリティの源泉です。

なぜ REST というルールを設けたのか

HTTP の使い方って自由にすればいいのでは?

自由すぎると、人によって設計がバラバラになります。「商品の削除」を POST /deleteProduct にする人もいれば、GET /products/remove?id=42 にする人もいる。

REST は、 リソース(URL)+ メソッド(動詞) という統一ルールを提供します。

  • GET /products/42 → 商品42番を取得
  • POST /products → 新しい商品を作成
  • PUT /products/42 → 商品42番を更新
  • DELETE /products/42 → 商品42番を削除

URL を見ただけで「何に対する操作か」がわかり、メソッドを見れば「どんな操作か」がわかります。

ここで重要なのが安全性べき等性という概念です。

メソッド 安全 べき等 意味
GET Yes Yes 何度呼んでも副作用なし。データを変更しない
PUT No Yes 何度呼んでも結果が同じ。「42番を○○に更新」を3回やっても結果は1回と同じ
DELETE No Yes 何度呼んでも結果が同じ。「42番を削除」を3回やっても、削除されるのは1回
POST No No 呼ぶたびに結果が変わりうる。「商品を作成」を3回やったら3つできる

この「べき等性」が、次のセクションで非常に重要になってきます。

なぜ「べき等性」が重要なのか — ネットワークは必ず失敗する

リクエスト送って返事が来なかったら、どうすればいいの?

EC サイトで「購入」ボタンを押したとする。リクエストは送信された。でもレスポンスが返ってこない。

  • サーバーは注文を処理してからレスポンスを返す途中で接続が切れたのか?
  • サーバーがリクエストを受け取る前に接続が切れたのか?

クライアントにはわかりません。

ここでリトライしたらどうなるか。API がべき等でなければ、2重注文になります。商品が2つ届く。

設計判断:ネットワークは必ず失敗する。だから「リトライしても安全」な設計が必要。

具体的な解決策として、 べき等キー(Idempotency-Key) があります。

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  1回目のリクエスト:                                            │
│    POST /orders                                              │
│    Idempotency-Key: "abc-123"                                │
│    Body: { item: "本", qty: 1 }                              │
│                                                              │
│    → サーバー: キー"abc-123"は初めて                             │
│    → 注文を作成し、結果をキーと一緒に保存                          │
│    → レスポンスを返す... が、途中で接続が切れた!                   │
│                                                              │
│  2回目のリクエスト(リトライ):                                  │
│    POST /orders                                              │
│    Idempotency-Key: "abc-123"    ← 同じキー                   │
│    Body: { item: "本", qty: 1 }                              │
│                                                              │
│    → サーバー: キー"abc-123"は処理済み                           │
│    → 新しい注文は作らず、前回の結果をそのまま返す                   │
│    → 2重注文なし!                                             │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Stripe などの決済 API では、この仕組みが実際に使われています。お金が絡む処理で2重実行されたら大問題ですから。

ステータスコードで「リトライすべきか」を判断する

  • 2xx(成功): 処理完了。リトライ不要
  • 4xx(クライアントエラー): リクエスト自体に不備がある。リトライしても無駄(404 Not Found、400 Bad Request など)
  • 5xx(サーバーエラー): サーバー側の一時的な問題。リトライする価値あり(500 Internal Server Error、503 Service Unavailable など)

API の進化 — 破壊的変更との戦い

API は変化します。新しいフィールドが追加されたり、レスポンスの構造が変わったりします。でも、古いクライアントを壊してしまう破壊的変更は最も避けるべきことです。

たとえば、レスポンスの name フィールドを first_namelast_name に分割したら、name を参照していた古いクライアントは動かなくなります。

設計判断:新機能の追加速度より、既存クライアントの安定性を優先する。

/v1/products//v2/products/ のようにバージョンを付けたり、古いフィールドを残しながら新しいフィールドを追加する(後方互換性の維持)のは、このためです。

まとめ

ここまで調べてきた内容を振り返ってみます。

curl https://example.com を叩いたとき、裏側では4つの技術が順番に動いていました。そしてその1つ1つに、誰かが「何を諦めて何を守るか」を選んだ判断がありました。

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  curl https://example.com                                    │
│                                                              │
│  DNS  →「1台で全部は無理 → 分散する」                            │
│    ▼     「速度 vs 鮮度 → TTL で調整」                          │
│  TCP  →「速度を犠牲にしてでも確実に届ける」                        │
│    ▼     「でも信頼性が邪魔な場面もある → UDP」                   │
│  TLS  →「コストを払ってでも暗号化・認証・整合性を守る」              │
│    ▼                                                         │
│  HTTP →「柔軟性よりスケーラビリティ → ステートレス」                │
│    ▼     「必ず失敗する → べき等性」                             │
│                                                              │
│  → すべての層で、誰かが「何を諦めて何を守るか」を選んだ               │
│  → その判断の積み重ねが、今の通信の仕組みを作っている                 │
│                                                              │
└──────────────────────────────────────────────────────────────┘

各技術の設計判断まとめ

技術 守ったもの 諦めたもの
DNS の分散構造 スケーラビリティ・耐障害性 即座の一貫性(TTL 分だけ古い情報が残る)
TCP データの確実な到達 速度(ハンドシェイク、ACK、再送のコスト)
UDP 速度・リアルタイム性 信頼性(届かなくても知らない)
TLS 安全性(暗号化・認証・整合性) 処理速度(ハンドシェイク、暗号化のコスト)
HTTP ステートレス スケーラビリティ 効率性(毎回すべての情報を送る)
べき等性 リトライの安全性 実装の複雑さ(キー管理、結果保存)

今回調べてみて一番印象に残ったのは、ネットワーク通信の仕組みが「誰かが何を諦めて何を守るかを選び続けた結果できている」ということでした。

完璧な技術は存在しない。あるのはトレードオフだけ。

「なぜ」を知っていれば、暗記しなくても仕組みを再構成できる。これは自分がシステムを設計するときの判断力にもなるはずです。

1
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?