Dev Study
← HTTPとTLSの仕組み
レッスン 8 / 12·約 9 分·第18期 HTTPとTLSの仕組み

HTTP/1.1 の応答には番号が無い — だから受け取った順に返すしかない

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力今ここ
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口今ここ
  6. アプリ処理
  7. DB保存

前回までで TLS を見ました。ここからは運び方です。1本の接続に要求を続けて送ったとき、応答がどの要求への答えかを両端がどう決めているかを見ます。

まず結論

通信の仕組みは、両端が何を共有しているかで決まります。HTTP/1.1 の要求と応答は、互いを結ぶ番号を共有していません。

共有しているのは届いた順序だけなので、RFC 9112 はサーバに「応答は要求を受け取った順に送れ」と課します。先に出来上がった応答も、前の遅い応答が終わるまで線に出られません。

? 読む前に予測

Node の HTTP サーバに、1秒待ってから応答を作る /slow と、すぐ作る /fast を用意しました。1本の接続で、応答を待たずに GET /slow と GET /fast を続けて送ります。/fast の応答はいつ届くでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

応答は、どの要求への答えかを名乗らない

HTTP/1.1 の接続は持続が既定で、1本に複数の要求と応答を載せられます。では、前の応答を待たずに次の要求を送ったらどうなるか。RFC 9112 §9.3.2 はこれを認めています。

A client that supports persistent connections MAY "pipeline" its requests (i.e., send multiple requests without waiting for each response). A server MAY process a sequence of pipelined requests in parallel if they all have safe methods (Section 9.2.1 of [HTTP]), but it MUST send the corresponding responses in the same order that the requests were received.

1文の中に許可と義務が並んでいます。処理は並列でよい(MAY)。送る順は受信順(MUST)。

なぜ順序だけは譲れないのか。応答の先頭は HTTP/1.1 200 OK で、どの要求への答えかを示す欄がありません。両端が共有しているのは「何番目に届いたか」だけなので、クライアントは1つ目の応答を1つ目の要求への答えとして読むしかありません。順序が番号の代わりをしています。

これは教材側の読みです。RFC は理由を述べず、順序を課しているだけです。

処理は並列でよいが、送り出す順は受信順
GET /slow1つ目に届く
GET /fast2つ目に届く
処理は並列でよい(MAY)
/fast の応答が完成先に出来上がる
/slow の応答が完成約1秒後
送出は受信順(MUST)
/slow の応答先に線へ出る
/fast の応答完成していても待つ
  • ・応答には、どの要求への答えかを示す番号が無い
  • ・受け取る側は順番で対応づけるので、送る側は順番を守るしかない
具体実物

先に出来上がった応答が、後ろで待っていた

curl はパイプラインを 7.65.0 で削除しているので、手元の curl では再現できません。そこで nc で2つの要求をそのまま流します。サーバは次の Node のコードです(macOS / Node v24.21.0。検証時のコードから起動時の表示1行を省き、空白と改行を整えたもの。この版でも同じ結果を再現済み)。

server.js — /slow は1秒後、/fast はすぐ応答を作る設定ファイル
const http = require('http');
const t0 = () => Date.now() % 100000;
http.createServer((req, res) => {
console.log(`${t0()} recv ${req.url} socket=${req.socket.remotePort}`);
const send = () => {
const body = `${req.url} generated_at=${t0()}\n`;
console.log(`${t0()} end ${req.url}`);
res.writeHead(200, {'content-type': 'text/plain', 'content-length': Buffer.byteLength(body)});
res.end(body);
};
if (req.url === '/slow') setTimeout(send, 1000); else send();
}).listen(18080, '127.0.0.1');

時刻は Date.now() のミリ秒の下5桁です。クライアント側の出力です。

nc の出力(生のまま)コマンド出力
$ (printf 'GET /slow HTTP/1.1\r\nHost: localhost\r\n\r\nGET /fast HTTP/1.1\r\nHost: localhost\r\n\r\n'; sleep 3) | nc 127.0.0.1 18080
HTTP/1.1 200 OK
content-type: text/plain
content-length: 25
Date: Thu, 24 Sep 2026 01:55:15 GMT
Connection: keep-alive
Keep-Alive: timeout=5
 
/slow generated_at=15854
HTTP/1.1 200 OK
content-type: text/plain
content-length: 25
Date: Thu, 24 Sep 2026 01:55:14 GMT
Connection: keep-alive
Keep-Alive: timeout=5
 
/fast generated_at=14853

先に届いたのは /slow です。ところが後ろの /fast は、Date が1秒早く(01:55:14)、生成時刻も 14853 と先です。出来上がった順と届いた順が逆になっています。

同じ回のサーバ側ログです。

サーバのログ(生のまま)コマンド出力
14853 recv /slow socket=51800
14853 recv /fast socket=51800
14853 end /fast
15854 end /slow

2つの要求は同じ接続(socket=51800)で同時に受信され、/fast は即座に、/slow は 1001ミリ秒後に完成しています(4回試して4回とも差は 1001ミリ秒)。処理は並列に進んでいました。 Node は /fast の応答を、/slow が送り終わるまで手元に持っていたことになります(観測した挙動で、Node の文書では確かめていません)。

✓ ここまでの確認

実演で、先に作られた /fast の応答が /slow の後ろに届いた理由として、ログと RFC 9112 §9.3.2 に合うのはどれですか。

抽象原理

番号を共有していなければ、順序が番号になる

1秒の待ちは /slow の仕事ではなく、/fast が払わされています。RFC 9112 §9.4 はこれを head-of-line blocking と呼び、よくある逃げ道も書いています。

Multiple connections are typically used to avoid the "head-of-line blocking" problem, wherein a request that takes significant server-side processing and/or transfers very large content would block subsequent requests on the same connection. However, each connection consumes server resources.

接続を分ければ列も分かれます。代わりに接続ごとの資源が要ります。その本数について、RFC 2616(1999年)は A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy と書きました。RFC 9112 §9.4 はこれを改めています。

Previous revisions of HTTP gave a specific number of connections as a ceiling, but this was found to be impractical for many applications. As a result, this specification does not mandate a particular maximum number of connections but, instead, encourages clients to be conservative when opening multiple connections.

「HTTP/1.1 は2本まで」は以前の版(RFC 2616)の数字で、現行の RFC 9112 が改めています。 現行は上限を定めず、控えめにと促すだけです。

RFC 9113 §1 は HTTP/1.1 のパイプラインを still suffers from application-layer head-of-line blocking と評します。原因がアプリケーション層、つまり HTTP の「受信順に返す」という約束にある、という名前です。

一般化すると、番号を共有していれば到着順に意味を持たせなくてよく、共有していなければ順序そのものが番号の役を負います。 同じ TCP の上に、番号を共有するプロトコルがあります。

具体実物

同じ TCP の上で、番号で照合する DNS

第2期『名前からアドレスへ』で、DNS は TCP も必須だと見ました(RFC 7766 §5)。同じ RFC 7766 は、1本の TCP 接続に問い合わせを続けて送ることを扱っています。DNS の運用を扱うコースは後で読みます。ここでは HTTP/1.1 との違いだけを見ます。

§6.2.1.1 は、サーバの側に起きそうなことを見通しとして書きます。

It is likely that DNS servers need to process pipelined queries concurrently and also send out-of-order responses over TCP in order to provide the level of performance possible with UDP transport.

§7 は、受け取る側の義務を定めます。

Since pipelined responses can arrive out of order, clients MUST match responses to outstanding queries on the same TCP connection using the Message ID.

形は HTTP/1.1 のパイプラインと同じです。違うのは、問い合わせと応答が Message ID という番号を共有していることだけです。番号で照合できるので順不同を許せ、HTTP/1.1 は番号が無いので受信順を課すしかありません。

✓ ここまでの確認

DNS も1本の TCP 接続に問い合わせを続けて送れますが、RFC 7766 は応答を順不同で返すことを認めています。それを可能にしているのは何ですか。

⇄ 2つを見比べる
具体例 A

HTTP/1.1 のパイプライン

応答に要求を指す番号が無いので、クライアントは受信順で対応づけます。RFC 9112 §9.3.2 はサーバに受信順での送出を課し、先に出来上がった /fast の応答も /slow の後ろで待ちました。

両端が共有しているもの表
項目 HTTP/1.1 パイプライン
応答に付く番号 無い
対応づけの手がかり 届いた順序
遅い1件の影響 後ろの応答が待つ
具体例 B

TCP の累積 ACK(第2期)

TCP のセグメントにはシーケンス番号があり、ACK は「この番号の手前まではすべて連続して受け取った」ことを表します。どこまで揃ったかを、届いた順ではなく番号で伝えられます。

両端が共有しているもの表
項目 TCP
データに付く番号 シーケンス番号
揃った範囲の伝え方 ACK 番号(累積)
途中が欠けたとき 同じ ACK 番号を返し続ける

HTTP/1.1 は順序を崩せず、TCP は番号で揃った範囲を伝えます。両者の違いを生んでいるものは何でしょうか。

受け取る側が「どれのことか」を判断するときに、何を手がかりにしているかを比べてください。

この章のまとめ

  • RFC 9112 §9.3.2: 処理は並列でよい(MAY)、応答は受信順(MUST)。詰まるのは処理ではなく送出順。
  • 実演では /fast が 1001ミリ秒先に完成し、それでも /slow の後ろに届いた。Date も1秒早いまま後ろに並ぶ。
  • 応答には要求を指す番号が無い。両端が共有しているのが順序だけなので、順序が番号の役を負う(教材側の読み)。
  • RFC 9113 §1 はこれを application-layer head-of-line blocking と呼ぶ。
  • 逃げ道は複数接続。「2本まで」は RFC 2616 の SHOULD NOT で、RFC 9112 §9.4 が撤回した。
  • DNS over TCP は Message ID を共有するので順不同を許せる(RFC 7766 §7 で照合が MUST)。
  • curl は 7.65.0 で HTTP/1.1 のパイプラインを削除している。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

「HTTP/1.1 のパイプラインでは、サーバは前の要求の処理が終わるまで次の要求を処理できない」という説明の扱いとして正しいのはどれですか。

用語集(3語)
パイプライン(HTTP/1.1)
持続接続の上で、前の応答を待たずに次の要求を送ること(RFC 9112 §9.3.2)。CPU のパイプライン(命令の段を重ねる仕組み)とは名前が同じだけの別物。
head-of-line blocking
列の先頭が詰まると後ろ全体が待たされること。RFC 9112 §9.4 は、処理の重い要求や大きな転送が同じ接続上の後続を待たせる問題として説明する。
Message ID(DNS)
DNS の問い合わせと応答が共有する番号。RFC 7766 §7 は、TCP 上で順不同に届く応答をこれで照合することをクライアントに課す(MUST)。