はじめに
実務においてCloudFrontの設計をしている際にHTTPバージョンについての理解が不足していたため、良い機会なのでそもそもHTTPとは? HTTP/2, HTTP/3とは?ということをアウトプットしていこうと思います。
そもそもHTTPとは
HTTP = Hyper Text Transfer Protocolの略で、クライアントとサーバが通信するときの決まり事のことです。これを"プロトコル"と言います。
よくある例えですが、片方が日本語で話していて、片方が英語で話している場合、お互いがその言語を理解しているなら別ですが、基本的に会話は成り立ちませんよね。
もう少しあり得ない例を考えてみましょう。
何かスポーツをやろうと思ったときに、片方は野球のルールでやろうぜ!、片方はサッカーのルールでやろうぜ、みたいなことになった場合、もはやわけがわからなくなります。
そこで、お互いに統一した決まり事でやり取りする"プロトコル"を決めてクライアントとサーバはやり取りをするのです。
その決まり事がHTTTP
というプロトコルを使って通信をします。
HTTPのプロトコルを使って、クライアントが「このWebページが閲覧したいです」とリクエストすると
サーバ側は「このページだよ」とレスポンスを返してくれます。
ただし、HTTPの通信はクライアントとサーバ間では暗号化されていません。
クラインとサーバ間を暗号化して通信するプロトコルをHTTPS
= Hyper Text Tranfer Protocol Secure
といい、後ろにSecure
がつき、通信を暗号化してやり取りしてくれます。
今回HTTPS
の細かい内容は趣旨から外れるため解説はしませんが、世の中のWebページを閲覧する際にはこのHTTPS
が使用されています。
Webページを閲覧するときに見る https://qiita.com/
のようなURLの頭についているこれです。
これはHTTPS
というプロトコルを使用して、qiitaのサーバに対して通信を行っています。
リクエスト
先ほど何気なくリクエスト
とレスポンス
という言葉を使いましたが、クライアントがWebサーバに対して送信を要求することをHTTPリクエスト
と言います。
HTTPリクエスト
は単に"このページくれよ"と言ってもらえるわけではなく、冒頭で説明したルールのフォーマットに沿ってHTTPリクエスト
をする必要があります。
実際にどういったフォーマットで送信しているのかを見てみましょう。
以下のコマンドを叩くとHTTPリクエストの中身を見ることができます。
※こちらのURLはご自身のQiitaのマイページのリンクや、Xのアカウントのリンクなどなんでも良いです。
curl -v https://qiita.com/triple321jhango/
途中色々と文字列が記述されていますが、以下の部分に注目します。
> GET /triple321jhango/ HTTP/2
> Host: qiita.com
> user-agent: curl/7.85.0
> accept: */*
なにやらよくわからない文字列が並んでいますね。
フォーマットの内容を交えながら説明していきます。
まずHTTPリクエストを送る際には以下のフォーマットで送信されます。
これをさきほどの文字列に当てはめると以下のようになります。
順番に見ていきましょう。
リクエストライン
リクエストラインの内容を分解すると以下のようになります。
GET
/triple321jhango/
HTTP/2
GET
まずGETですが、これはHTTPメソッドと言われるクライアントがWebサーバに対して、「このページくれよ」と要求することを表しています。
GET -> ゲットするということですので、例えばQiitaの記事を参照する場合、見たい記事をクリックして記事の内容をゲットします。
これはHTTP GETメソッドが使用されています。
下図はQiitaのトレンドの画面ですが、この中から面白そうだなぁと思った記事をクリックして閲覧すること、それがGETです。
他にもPOST, PUT, DLETEなどのメソッドがありますが、本記事では本題から外れてしまうため省略いたします。
/triple321jhango/
/triple321/jhango/
これはGETしようとしてる先のURIです。
これはリクエストした先によって異なるので、ご自身のQiitaのアカウントの場合は異なっているかと思います。
HTTP/2
これはHTTPプロトコルのバージョンを示しています。HTTPプロトコルにはいくつかバージョンが存在しますがこのバージョンは2です。
ここについては後ほど解説します。
ヘッダー
次にヘッダーですが、以下再掲します。
> Host: qiita.com
> user-agent: curl/7.85.0
> accept: */*
-
Host: qiita.com
こちらはリクエストした先のサーバのドメイン名(ホスト名)です。厳密にはHost: <host>:<port>
のようにポート番号を指定するのですが、ポート番号が指定されなかった場合には規定のポート(HTTPSであれば443、HTTPであれば80)とみなされます。 -
user-agent: curl/7.85.0
クライアントとWebサーバ間で通信を行う際に、クライアントがWebサーバにリクエストを送る際に、クライアントの素性を表す情報も付与して送信します。
その際に使われるのがUser-Agentヘッダ
と呼びます。
今回はクライアントはQiitaサーバに対してcurlでリクエストしてますよ、というのがわかるようにuser-agentヘッダを付与しています。
"/"以降はcurlのバージョン情報を示しています。
-
accept: /
acceptについてはクライアントが処理できるデータの種類をサーバに通知するヘッダ情報です。
クライアントがサーバからどのような形式 (メディアタイプ: MINEタイプ)でレスポンスを期待しているかを伝えるためのものです。
例をいくつかあげてみましょう。
以下の場合、クライアントがJSONデータを受け取りたい場合に指定します。
accept: application/json
以下の場合はクライアントがxmlデータで受け取りたい場合に指定します。
accept: application:xml
これが*/*
になった場合どうなるかというと、ワイルドカードを表しており、クライアントはすべてのメディアタイプを受け入れますよ、ということを示しています。
クライアントが特定のメディアタイプに依存せずにサーバが提供するどの形式でも対応できると伝えています。
今回は特にcurlコマンドを実行する際に特に指定をしなかったため、何でも良いですよ、ということになります。
curlコマンドで指定の形式で受け取りたい場合には、
-H "Accept: application/json"
のように指定するとJSON形式のデータを要求できるようになります。
curl -v -H "Accept: application/json" https://qiita.com/tripie321jhango/
> GET /tripie321jhango/ HTTP/2
> Host: qiita.com
> user-agent: curl/7.85.0
> accept: application/json
空行
空行がなぜあるかというと、ヘッダーとメッセージボディを明確に分けるために空行を入れています。
理由はそれだけです。
メッセージボディ
GETする際にはメッセージボディは取得されません。
メッセージボディが必要な時はどんな時かというと、例えばQiitaに記事を投稿する場合、記事を更新する場合などに必要になります。
Qiitaの記事を投稿する場合には、投稿内容が含まれているかと思います。
その投稿内容がメッセージボディの中に含まれています。
レスポンス
リクエストと対になるのがレスポンスです。
レスポンス (response) = 応答、返事、反応 などの意味がありますが、その通りの意味です。
リクエストを送るとレスポンスが返ってくるのですが、非常にながったらしいですが以下のような内容が返ってきます。
< HTTP/2 200
< date: Tue, 27 Aug 2024 23:21:30 GMT
< content-type: text/html; charset=utf-8
< server: nginx
< x-frame-options: SAMEORIGIN
< x-xss-protection: 1; mode=block
< x-content-type-options: nosniff
< x-download-options: noopen
< x-permitted-cross-domain-policies: none
< referrer-policy: strict-origin-when-cross-origin
< x-docker-server: true
< link: <https://cdn.qiita.com/assets/public/style-6b9ba356bcee8b24c27b0f401b29ffc3.min.css>; rel=preload; as=style; nopush,<https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,500,0..1,-25..0>; rel=preload; as=style; nopush
< vary: Accept
< etag: W/"9ec14ae6219ae5bfb377ef37ebb6bed8"
< cache-control: max-age=0, private, must-revalidate
< set-cookie: _qiita_login_session=OcdIaquJAyZVeN%2F857iPSHnz0H2zGLsO8IxFrByas8PQto%2FiHK7%2BXIbFYv1YrPETuDo%2BWRlELJrY8Huml%2F%2BtO4D55eWoLJolVS5xGobwYyhtWq%2F5215RyYr22oTsXz8FwTDKUx9MgpO9SI7OeJHaEOHXihioDMkJeQe3cu31ndifaM2NbFFamaxylmiQkhF1dZOrUxrdpx60as5agf%2F4pFuMSQ8E0MDTwmESuX6%2F9EQWLEwASdl1hlZeWBWESoZV2heEe8XF1NmWXw%2FljCTw0f0lAH%2FmTR4vwW5gZZn4Fbph57oKOj%2F4shAuS%2FN%2Bh%2FH9B1o%3D--1KJReyDrvnSDoK3M--zK9IZlRMBtQ7Ump%2FL4BJjg%3D%3D; domain=.qiita.com; path=/; expires=Wed, 27 Aug 2025 23:21:30 GMT; HttpOnly; SameSite=Lax
< x-request-id: 5c498062-532b-464c-9fb0-e0cc8d3977fd
< x-runtime: 0.170268
< strict-transport-security: max-age=2592000
<
<!DOCTYPE html><html lang="ja"><head><meta charset="utf-8" /><title>triple321jhango - Qiita</title><meta content="2024/1からAWS設計構築 | AWS SAA, DVA, SAP, LinuC Lv1 |" name="description" /><meta content="width=device-width,initial-scale=1,shrink-to-fit=no,viewport-fit=cover" name="viewport" /><meta content="#ffffff" name="theme-color" /><meta content="XWpkTG32-_C4joZoJ_UsmDUi-zaH-hcrjF6ZC_FoFbk" name="google-site-verification" /><meta content="telephone=no" name="format-detection" /><link rel="canonical" href="https://qiita.com/triple321jhango" /><link href="/manifest.json" rel="manifest" /><link href="/opensearch.xml" rel="search" title="Qiita" type="application/opensearchdescription+xml" /><link as="script" href="https://www.googletagservices.com/tag/js/gpt.js" rel="preload" /><link href="https://securepubads.g.doubleclick.net" rel="preconnect" /><script async="" src="https://www.googletagservices.com/tag/js/gpt.js"></script><meta name="csrf-param" content="authenticity_token" />
これは先ほどのリクエストのフォーマットと同様でレスポンスもフォーマットに準じて内容を返してきます。
レスポンスのフォーマットは以下のようなものです。 (便宜上、実際のリクエストの中身を省略して記述しています。)
ステータスライン
リクエストの時はリクエストラインだったのが、レスポンスの場合はステータスラインになっていますね。
HTTP/2
, 200
という文字列が並んでいますが、HTTP/2
はHTTPのバージョン、200
はステータスコードを表しており、クライアントからのリクエストに対してWebサーバがこのステータスコードを返すことによって、正常に処理できたのか、またはエラーになったのかを示しています。
200
の場合は問題なく処理できましたよ、ということを返信しています。
ヘッダー
レスポンスのヘッダー情報にcontent-type: text/html;
charset=utf-8
という文字列が記載されています。
これはメッセージボディにどのような形式のデータで返しているかを示しています。
この場合content-type: text/html;
charset=utf-8
なので、コンテンツをHTML形式で文字コードはUTF-8で返しますよ、といういことを示しています。
メッセージボディを見ていただくとわかるかと思いますが、<!DOCTYPE html>
から始まっていますので、HTML形式であることがわかります。
ではcontent-type: text/html;
ではなく他の形式もあるのか?と思った方。あります。
先ほどのcurl
のところでも説明させていただきましたが、例えばJSON形式のデータの場合はどうか?を考えてみます。
JSON形式の場合はcontent-type: application/json;
となり、メッセージボディの形式をJSONであることを示しています。
以下はJSON形式のレスポンス内容です (疑似的な内容です)
HTTP/2 200 OK
Content-Type: application/json
{
"name": "John Doe",
"age": 30,
"email": "johndoe@example.com"
}
content-type
がapplication/json
であることがわかり空行をあけて、メッセージボディがJSON形式であることがわかります。
リクエストで指定するAccept
はクライアントが期待しているレスポンス形式を指定するものですが、サーバ側がクライアントが期待している形式をサポートしていない場合はデフォルトのHTML形式でレスポンスを返すことがあります。
HTTPのバージョン
ここまで長々と説明してきましたが、本題のHTTPのバージョンについて話をしていきます。
curlコマンドを使ってHTTPリクエストをした際にHTTP/2
という文字列があったかと思いますが、これがHTTPのバージョンを示しています。
> GET /triple321jhango/ HTTP/2
> Host: qiita.com
> user-agent: curl/7.85.0
> accept: */*
HTTPにはさかのぼるといくつかバージョンがあります。
年代 | バージョン |
---|---|
1990年 | HTTP/0.9 |
1996年 | HTTP/1.0 |
1997年 | HTTP/1.1 |
2015年 | HTTP/2 |
2018年 | HTTP/3 |
HTTP/0.9
初めに完成したHTTP/0.9は当初バージョン番号はありまんでしたが、以降のバージョンと区別するために0.9と名付けられるようになりました。
この時のHTTPリクエストは非常にシンプルで先ほど説明したHTTPリクエストのフォーマットはリクエストラインのみで構成されてました。
当初はHTTPリクエストはGETメソッドのみで、他のHTTPメソッド(POST, PUT, DELETEなど)はサポートされていませんでした。
以下のようにクライアントからリクエストをすると、
GET /index.html
以下のようにHTMLドキュメントを取得してブラウザに表示するというものでした。
<html>
A very simple HTML page
</html>
GETメソッドを使用して、クライアントからサーバに返すことができるのはHTMLファイルのみで、画像や動画などのデータを扱うことができませんでした。
HTTP/1.0
0.9はHTTPメソッドにGETしか使用することができませんでしたが、1.0になることでPOST, HEADなどのメソッド、レスポンスヘッダー、ステータスコードなどが追加されるようになりました。
HTTP/1.1
1.1ではHostヘッダーの使用が必須となりました。
1.0と1.1での大きな違いはパフォーマンスが向上したことです。
クライアントとサーバがやりとりするにはTCPコネクションという、通信相手との仮想的な通信路を確立しやり取りする仕組みがあるのですが、この点が大きく改善されたことでパフォーマンスが向上しました。
TCPコネクションとは以下のようにクライアントとサーバが通信のやり取りをする際に3回やり取りしてコネクションを確立します。
これを3-way handshakeと言います。
3-way handshakeでコネクションを確立すると、クライアントとサーバはやり取りできる通信路ができます。
これでクライアントとサーバ間でパケットの通信が可能となります。
クライアント⇔サーバ間のやり取りを終了する場合にはFINという終了の合図を送って通信のやり取りを終了します。
ではTCPコネクションがわかった上で、HTTP/1.0と1.1でどうパフォーマンスが違うのかを説明しましょう。
HTTP/1.0の場合はクライアントからのリクエストの度にTCPコネクションを3-way handshakeを行い、データを受け取ったらコネクションを閉じる、という方式で、非常に非効率でしたが、HTTP/1.1に変わってからは1度TCPコネクションで接続を確立できれば、1度のリクエストでデータを受け取ったらコネクションを閉じることなく、持続的にコネクションをしたままデータのやり取りができるため、開いては閉じるの動作が省略された分、パフォーマンスが向上したということです。
HTTP/2
では2015年に登場したHTTP/2はさらにどのように進化を遂げたのでしょうか?
HTTP/1.1ではWebページを取得する際に、クライアントからサーバに対してリクエストし、サーバからのレスポンスでHTMLファイルを返します。
HTMLファイルの中にCSSのリンクが含まれる場合、CSSファイルを取得するためにリクエストします。
さらに、JavaScriptのリンクが含まれれば、JavaScriptのファイルを取得する、といった具合にどんどんリクエストしていきます。
一度のTCPコネクションの接続で連続して複数のリクエストを送信できることをKeep-Alive
と言います。
しかし、ここで発生する問題があります。
Head-of-Line-Blocking
という通信の待ち状態が発生します。
クライアント側は一度の接続で多くのリクエストを送信することができますが、リクエストされた側はリクエストを順番に返す必要があります。
仮にここでCSSのレスポンスの生成に時間がかかった場合、JavaScriptの処理を返すことができません。
これを解消したのがHTTP/2です。
HTTP/2では多数のリクエストに対して、一つ一つをレスポンスするのではなく、一度にまとめてレスポンスすることができるようになりました。
これにより通信の効率が改善し、ページの読み込みが早くなりパフォーマンスが向上しました。
HTTP/3
では最後にHTTP/3についてです。
HTTP/3はHTTP/2よりもさらに速く処理できるように改善されたバージョンです。
大きく変わったのはプロトコルの部分で、HTTP/2ではTCP
だったのが、HTTP/3では新たなプロトコルとしてQUIC
と呼ばれるプロトコルで通信することになりました。
QUICとはQuick UDP Internet Connectionsの略で、UDPを用いた通信プロトコルです。
TPCとQUICの違い
TCPでは通信の際にデータを送信したら、送信した相手からデータが返ってくることを確認しながら通信するプロトコルでいわゆるコネクションフルの通信であるため、パケットの欠落やエラーが発生した場合、再送してくれるため、通信の信頼性が高い、一方でパケットの欠落が発生した場合には、再送信しなければいけないため通信の速度が遅いというデメリットがあります。
QUICではUDPをベースにしたプロトコルであり、データを送信したら、送信した相手からデータが返ってくることをいちいち確認しません。
UDPの場合はデータを送ったら送りっぱなしのため信頼性に欠ける通信ですが、QUICは独自のTCPのような信頼性を持つ仕組みを構築しています。
エラーの検出、パケットの順序の管理、パケットの再送などを独自に管理しているため、信頼性が向上しています。
つまりUDPのスピードを維持しつつ、パケット欠落やエラーの対処をプロトコル自体が行うことで信頼性と速度の両方を改善しています。
信頼性の部分でいうと、QUICは常に暗号化された通信を行うため (エンドツーエンド暗号化)、データの改善や盗聴が防止されます。
まとめ
ここまでHTTP/3ではQUICと呼ばれるUDPベースの速度の速いプロトコルを使用し、かつ信頼性も兼ね備えた通信の仕組みを組み込んでいるため、まさに鬼に金棒になったということがわかりました。
まだまだHTTP/3は主流とは言えないようで、現状はHTTP/2が主流のようです。
HTTP/3の使用率は2023年中頃のデータでは、Chromeの約74%のトラフィックがHTTP/3で処理されており、Safariも約18%に達しています。一方、EdgeとFirefoxではHTTP/3の使用率がそれぞれ約40%と35%程度に留まっているようです。