- ブラウザURL入力今ここ
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- DB保存
宛先が決まったので接続を張り、バイト列を送ります。相手は同じ機械の中(loopback)なので、ルータという別の機械はここに現れません。
まず結論
ブラウザがサーバに送るリクエストは特別な形式のデータではありません。TCPの接続の上を流れる、CRLFで区切られたただのバイト列です。
TCPは中身を解釈せず、運ぶのはIPアドレスとポートだけでホスト名は運びません。だからHTTP/1.1はリクエストの側に `Host` を書くことを必須にしています。
サーバへ `GET / HTTP/1.1` の行と `Host:` の行だけを送り、そのあと何も送らないと何が起きるでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
接続は「合意」、その上を流れるのはバイト列
前のレッスン『名前がアドレスになる — localhost は DNS に行かない』で宛先が決まりました。ここから先は、そのアドレスへ接続してリクエストを送る段です。
TCPの接続は、ケーブルが引かれるのでも通り道が予約されるのでもなく、両端がそれぞれ「この相手と話している」という状態を持っただけです。この合意の作り方は第22週『なぜ手を3回握るのか — 「接続」という幻想の作り方』で扱います。
合意ができれば、あとはバイト列を流すだけ。TCPは中身を解釈しません。 ヘッダがどこで終わるかも本文がどこから始まるかも知りません。
だからHTTP/1.1は区切りをバイト列の中に自分で書きます。RFC 9112 §2.1 の規定です。
start-line が1行、ヘッダ行が0本以上、そして CRLF が1つ単独で置かれ(= 空行)、後ろに本文が任意で続きます。この空行が「ヘッダ部はここまで」の印です。
- ・区切りはすべてバイト列の中にある。TCPは区切りを持たない
- ・空行が来るまで、受け取る側はヘッダの途中と見なす
実際に流れた4行を見る
通し例題のサーバ(<h1>Hello</h1> を返すだけのNode)に curl -v で接続した生の出力です。> が送ったバイト列、< が受け取ったものです。
* Host localhost:3999 was resolved.* IPv6: ::1* IPv4: 127.0.0.1* Trying [::1]:3999...* Connected to localhost (::1) port 3999> GET / HTTP/1.1> Host: localhost:3999> User-Agent: curl/8.7.1> Accept: */*>* Request completely sent off< HTTP/1.1 200 OK< Date: Wed, 16 Sep 2026 02:15:52 GMT< Connection: keep-alive< Keep-Alive: timeout=5< Content-Length: 14<{ [14 bytes data]* Connection #0 to host localhost left intact<h1>Hello</h1>
注目は最後の > だけの行です。中身が空のこの行が図の「空行」にあたります。送った直後にcurlは "Request completely sent off" と表示しました。ヘッダ部を閉じるまでリクエストは完成しません。
この実機では localhost が ::1 と 127.0.0.1 の両方に解決され、curl は ::1 に繋ぎました。検証時のポートは 3999 です。行末が本当に2バイトなのかは xxd で確かめられます。
00000000: 4854 5450 2f31 2e31 2032 3030 204f 4b0d HTTP/1.1 200 OK.00000010: 0a44 6174 653a 2057 6564 2c20 3136 2053 .Date: Wed, 16 S00000020: 6570 2032 3032 3620 3032 3a31 353a 3532 ep 2026 02:15:5200000030: 2047 4d54 0d0a 436f 6e6e 6563 7469 6f6e GMT..Connection00000040: 3a20 6b65 6570 2d61 6c69 7665 0d0a 4b65 : keep-alive..Ke
4f 4b が "OK"、直後の 0d 0a が CR LF です。右側では . としか見えませんが、左を見れば2バイトあるとわかります。HTTPが「人に読める」ことと「文字列が運ばれている」ことは別です。
HTTP/1.1 で、ヘッダ部が終わったことをサーバはどう知りますか。
TCPはホスト名を運ばない
なぜ Host: の行がわざわざ必要なのでしょうか。
接続の相手は既に決まっています。しかしTCPの接続が持つ情報は両端のIPアドレスとポート番号の組だけで、前のレッスンで打った localhost は消えています。
一方サーバ側は、1つのIPアドレスとポートの組で複数のサイトを提供することがあります。届いたバイト列だけが判断材料なので、どのホスト宛てかはリクエストに書いてもらうしかありません。 RFC 9112 §3.2 の規定です。
送る側にMUST、受け取る側にもMUSTがあります。値はターゲットURIのauthorityと同一で、ポート番号を含みます。実機出力が Host: localhost:3999 とポート込みなのはこのためです。
もう一つ思い込みを外します。「1リクエスト = 1接続」ではありません。 §9.3 は持続接続が既定だと定めます。
実機出力の Connection #0 to host localhost left intact がそれで、応答を受け取ったあともcurlは接続を閉じていません。
空行を送らないとどうなるか、Hostを送らないとどうなるか
区切りがバイト列の中にあるなら、送らなければサーバは判断できません。nc で手書きのバイト列を送ります。まず正しい場合。
HTTP/1.1 200 OKDate: Wed, 16 Sep 2026 02:15:52 GMTConnection: keep-aliveKeep-Alive: timeout=5Content-Length: 14<h1>Hello</h1>
printf で組み立てた40バイトだけで応答が返りました。HTTPのリクエストとはこの程度です。
次に末尾の \r\n(空行)を1つ減らし、3秒だけ送信側を開けたままにします。
11:16:31.75437300011:16:34 HTTP/1.1 400 Bad Request11:16:34 Connection: close11:16:34exit=0 11:16:34
11:16:31 から 11:16:34 までの3秒間、応答は返っていません。 サーバは「ヘッダの途中」と見なして待ちました。nc が送信側を閉じた瞬間、ヘッダ部が閉じられないまま切れたとして 400 が返ります。確かめたのは3秒待ったことまでで、それ以上は測っていません。
もう一つ、Host: の行を落とすとどうなるか。
HTTP/1.1 400 Bad RequestConnection: closeDate: Wed, 16 Sep 2026 02:15:52 GMTTransfer-Encoding: chunked0
この実機のNode(v24.21.0)は 400 を返しました。§3.2 のMUSTと一致しますが、これは「この実装がそうだった」という観測で、どのサーバも必ず400を返すとは言えません。
最後に持続接続です。同じ接続に2つのリクエストを流します。
HTTP/1.1 200 OKDate: Wed, 16 Sep 2026 02:16:34 GMTConnection: keep-aliveKeep-Alive: timeout=5Content-Length: 14<h1>Hello</h1>HTTP/1.1 200 OKDate: Wed, 16 Sep 2026 02:16:34 GMTConnection: closeContent-Length: 14<h1>Hello</h1>
1つ目の本文の直後に、改行も挟まず2つ目の HTTP/1.1 200 OK が始まっています。1つ目がどこで終わったかは Content-Length: 14 が決めています。 RFC 9112 §6.3 の本文長の判定順のうち Content-Length による経路です。
HTTP/1.1 が `Host` ヘッダを必須にしているのはなぜですか。
curl の `>` 行(HTTPの視点)
curl -v は、HTTPメッセージとして組み立てた行を `>` 付きで表示します。request-line・ヘッダ・空行の構造が見える代わりに、いつカーネルへ渡ったかは見えません。
> GET / HTTP/1.1> Host: localhost:3999> User-Agent: curl/8.7.1> Accept: */*>
システムコールの視点(第13週・第26週)
アプリは自分でネットワークへ書き込めず、必ずカーネルに依頼します。接続を作る呼び出しの後、同じバイト列が書き込みの呼び出しの引数としてカーネルへ渡ります。Linux では strace でこの境界を覗けます(本コースの実機は macOS のため未取得)。
見る場所 見えるもの 見えないものcurl の > 行 HTTPの行の構造 カーネルへ渡した時点システムコール バイト列と渡した時点 HTTPとしての意味
curl が表示する `>` の行と、システムコールのトレースに現れる書き込みの中身は、同じものを見ているのでしょうか。
それぞれが「どの層に立って」観測しているかを考えてください。
この先の学びにどう繋がるか
リクエストが届きました。次のレッスン『サーバは何を返すか — Content-Type が無いと何が起きるか』では、返ってくる側のバイト列を見ます。上の実機出力に Content-Type が無かったことに気づいたなら、それが次の主題です。
接続そのものは第22週『なぜ手を3回握るのか — 「接続」という幻想の作り方』で扱います。RFC 9293 §3.5 は、データを載せたセグメントで接続を同期させることを "perfectly legitimate" と書いています(同節 Figure 6 自体は、ACKのみの行とデータを伴う行を別行として示します)。
語彙の網羅は既存のクイズ群「HTTP/1.1(RFC 9112)」「HTTP/1.1用語(単語帳)」「TCP用語(単語帳)」に譲ります。本コースが追うのは、1行のHTMLが画面に出るまでの継ぎ目だけです。
この章のまとめ
- HTTP/1.1 のリクエストはCRLFで区切られたバイト列。§2.1 の形は
start-line CRLF *( field-line CRLF ) CRLF [ message-body ]で、真ん中の単独のCRLFがヘッダ部の終わりを示す空行。 - TCPは中身を解釈しない。 区切りはバイト列の中に書くしかない。空行を送らずにいると、3秒のあいだ応答が返らなかった。
- 行末は実際に
0d 0aの2バイト。 ただし §2.2 により、受け取る側が単独のLFを行末と認めるのは MAY。 Hostが必須なのは、TCPがIPアドレスとポートしか運ばないから。 §3.2 はクライアントにMUST、サーバに400のMUSTを課す。値はポートを含む。- 「1リクエスト = 1接続」ではない。 §9.3 により持続接続が既定。境目は
Content-Lengthが決める。 - 実装の観測と規格の条文は別物。根拠は条文に取りに行く。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
「HTTP/1.1 はテキストのプロトコルなので、バイト列ではなく文字列として送られる」の評価はどれですか。
用語集(5語)
- request-line
- リクエストの1行目。RFC 9112 §3 は `request-line = method SP request-target SP HTTP-version` と定める。SPは半角スペース。
- CRLF
- 復帰(CR, 0x0d)と改行(LF, 0x0a)の2バイト。HTTP/1.1 のstart-lineとヘッダ行の終端。ヘッダ部の終わりはCRLFだけの行で示す。
- Host ヘッダ
- リクエストがどのホスト宛てかを示すヘッダ。RFC 9112 §3.2 でHTTP/1.1の全リクエストに必須(MUST)。値はターゲットURIのauthorityと同一で、ポートを含む。
- 持続接続 (persistent connection)
- 1本のTCP接続に複数のリクエストと応答を載せる使い方。RFC 9112 §9.3 により HTTP/1.1 の既定。
- Content-Length
- メッセージ本文の長さをオクテット数で宣言するヘッダ。RFC 9112 §6.3 の判定順の第6項で、受け取る側はこれで本文の切れ目を決める。