HTTPだけで1リクエスト複数レスポンスを実現する方法

この記事はAnthrotech Advent Calendar 2025の19日目の記事です。

昨日は @adoringonion さんによる Spineを雑に使ってイラストを使かす でした。

皆さんレベルの高い記事を書かれている中非常に恐縮ですが、この記事では、基礎的だけど、覚えておくととても助かる技術として、”HTTPプロトコル”のみでお手軽にリアルタイム通信をする方法をご紹介します。

皆さんが普段見ているWebページは、”HTTPプロトコル”によって支えられています。

プロトコルとは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

HTTPとは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

さて、HTTPには、原則として下記のような性質があります。

  • 原則として1リクエスト1レスポンスである
  • ステートレスである
  • 通信のスタートはクライアント(ブラウザ側)からである

順番に説明していきます。

原則として1リクエスト1レスポンスである

HTTPは1回のリクエストに対して1回レスポンスを返すと通信が終了する仕組みとなっています。

通常はレスポンスを受け取った時点で通信が切れてしまうため、以降再度クライアントから要求を行うまでは追加でサーバーからデータを受け取ることができません。

ステートレスである

“ステート”とは状態のこと。
つまり、”ステートレス”とは状態を持たないという意味です。

具体的な例を下記の図に示します。

ステートレスであるため、一度通信が終了すると、サーバー上のデータは破棄され、再接続しても同じブラウザから再接続されているか、それとも新規のアクセスなのかの判別を、サーバー側は行うことができません。

もちろん、Cookie・セッションを使用することで疑似的にステートフルにできますが、HTTPのみではできない仕様になっています。

通信のスタートはクライアント(ブラウザ側)からである

HTTPでは、原則として通信は必ずクライアント(ブラウザ側)から開始します。

いきなりサーバーからブラウザに通信を投げることはできません。

以上の性質があることで、Webアプリを作る際に困ったことが起きることがあります。

その一例として、”リアルタイム“かつ、”サーバーから通信を行いたい“場合があります。

Web上でこのわがままに応えるためには、サーバー側の状態を逐次何らかの方法でブラウザに送ってやる必要があります。

よく取られるアプローチとしては、下記のものがあります。

ポーリング

ブラウザから通信を一定間隔で行い、定期的にブラウザに情報を取りにいく方法です。

本当の意味でのリアルタイムではないのですが、技術的にも一定間隔で通信を走らせ続けるというかなりベーシックなものであるため、お手軽にリアルタイム通信を実装したい場合はとても良いです。

デメリットとしては、頻繁にブラウザからサーバーへ通信を行うため、常にサーバーに負荷をかけてしまう点、そしてページそのものを頻繁に再読み込みしたくない、と言った理由でAjaxで実装することが多いので、ページを表示するURLとは別に、ポーリング用のエンドポイント(URL)を設けてやる必要が出てくる場合が多いです。

Websocket

本当の意味でのリアルタイム通信を行いたいのであれば、 Websocketは非常に良い仕組みです。

一番最初にブラウザから通信をスタートするというところは通常のHTTPと変わりませんが、一度サーバーとの通信が行われると、以降通信が接続されっぱなしになるのが特徴です。

常時サーバーと接続が確立しているため、この間はブラウザからもサーバーからも自由なタイミングで好きなだけ通信することができます。

”リアルタイム通信”と言われて思い浮かべるものに、一番近い仕組みではないでしょうか。

ただし、高度な分実装にはやや手間が伴います。

どちらかが明示的に切断するまでは接続を常に維持することになるため、サーバー側では何らかの常駐プロセスを用意してやる必要があります。

また、Web側の仕組みと、Websocket側の仕組みは別になるため、これらの間で情報をやり取りする何らかの仕組みが必要になることが多いです。(RDBやRedis,memcachedなど)

長々と説明してしまいましたが、ここからが本題となります。

さて先日、お客さんから”複数枚画像をサーバーにアップロードした際、画像のアップロードと加工の進捗を表示して欲しい”という要望を受けました。

これぐらいのものであればポーリングなどで対応したくなるところですが、この案件では、インフラやDBの設計はすべてお客さんが管理しています。

さらにリリース間近ということもあり、今からDBやインフラの構成を変えるのもキツい….という状況でした。

DBのテーブル変更はできないし、Websocket用の常駐プログラムを設置することもできない….となると、上記の方法では対応不能となってしまいました。

そこで今回使用したのが、3つ目のリアルタイム通信方法….SSEです。

SSEとは何か?

SSEとはServer-Sent Eventsの略で、サーバー => クライアント の通信をリアルタイムに行うことができる技術です。

このSSEの一番の特徴は、HTML5対応ブラウザとHTTPプロトコルのみでリアルタイム通信が実現できることにあります。

仕組みとしては、ブラウザからサーバーに対してリクエストを行い、サーバーがレスポンスを行う….ところまで通常のHTTPの通信と同じですが、サーバーがブラウザに対してレスポンスを返した後も、接続を維持し続ける点が、通常とは異なってきます。

接続を維持しているということは、通信を継続することができることを意味します。

つまり、最初のデータ続いて、後から何度でも、好きなタイミングで、サーバーはブラウザに対して任意のデータを任意の回数送り続けることができます。

ここで、

「HTTPは1リクエスト1レスポンスだったはず。なぜ一度レスポンスを受け取った後も通信を切らないままでいることができるのか。」

という疑問を持つ方もいるかもしれません。

実はこの仕組みは、HTTP1.1で追加された、”Keep-Alive”という機能を活用しています。

一つ前のHTTPバージョン、HTTP1.0では、1度リクエストを行い、1度レスポンスを受け取ったら自動で通信が切れる仕組みでした。

一方、HTTP1.1で実装されたKeep-Alive機能を使用することで、レスポンスを受け取った後も接続を維持することができるようになりました。

SSEではこの維持された接続を再利用して、複数データをブラウザに送ることができていたわけです。

要するに、1リクエスト・1レスポンスの原則ではあるものの、その1レスポンスを終わらせさえしなければずっとデータを送り続けることができるよね?という、HTTPのプロトコルの性質を考えるとなんとも頓智を効かせたような仕組みになっています。

実装例の1つとして、PHPを使った簡単なサンプルを下に示します。

sse.php

<?php
// SSE用ヘッダ
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');

// バッファリング無効化(重要)
while (ob_get_level() > 0) {
    ob_end_flush();
}
ob_implicit_flush(true);

// 無限ループで定期的にデータ送信
$counter = 0;

while (true) {
    $counter++;

    $time = date('Y-m-d H:i:s');

    // SSE形式で出力
    echo "data: {$time} - カウント: {$counter}\n\n";

    // クライアント切断チェック
    if (connection_aborted()) {
        break;
    }

    // 1秒待機
    sleep(1);
}

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>PHP SSE Sample</title>
</head>
<body>
<h1>PHP SSE リアルタイム通信サンプル</h1>

<div id="log"></div>

<script>
    var eventSource = new EventSource("sse.php");

    eventSource.onmessage = function (event) {
        var log = document.getElementById("log");
        var p = document.createElement("p");
        p.textContent = event.data;
        log.appendChild(p);
    };

    eventSource.onerror = function () {
        console.log("SSE connection error");
    };
</script>
</body>
</html>

上記の2つのファイルをWebサーバーのドキュメントルート上に、次のように配置します。

/
 ├─ index.html     (クライアント)
 └─ sse.php        (SSEサーバー)

実際に動作させるとこのようになります。(クリックで再生します。)

開発者ツールで観察すると、最初のステータス200のレスポンスを受け取った後も定期的にサーバーから通信を受け取っているのが分かります。

さて、このSSEですが、もう一つメリットがあります。

先に挙げたWebsocketやポーリングの通信は、ページを表示させる通信とは別の通信として行われます。

通信が異なる…ということは、処理も別スレッドになってしまう、ということになります。

先ほど例に挙げた画像加工を行う処理とその進捗をブラウザに返す例を考えてみます。

Websocketやポーリングでは、画像加工を行っているスレッドとは別スレッドで動作することになるため、何らかの方法で画像加工を行っているスレッドの進捗状況を外部から取得する必要があります。

別スレッドの情報を直接取得することが難しいため、画像加工スレッドが自身の進捗状況を他のスレッドからも見える場所(DBやNoSQLなど)に書き出す必要が出てしまいます。

一方、SSEを使用すれば単一スレッドで画像加工と進捗の出力の両方ができるため、わざわざ外部に進捗情報を書き出す必要もなく、非常にシンプルに実装ができます。

このようにリアルタイム通信を非常にシンプルに実装できるSSE。

かなり有用なユースケースはたくさんあると思います。

Webでリアルタイム通信が必要になった際、ぜひ選択肢の一つに加えてみてはいかがでしょうか。


明日の Anthrotech Advent Calendar 2025の20日目、担当は@kznrlukさんの回です。お楽しみに!!!