背景
サーバプッシュ機能について学んでいたら、結果的にWeb技術の進化の歴史を紐解くことになりました。年表も作ったので、誰かの役に立つかもと思いシェアします。
サーバプッシュ機能
サーバプッシュ機能は、クライアント側からの明示的なリクエストがなくとも、サーバ基点で何かしらコンテンツをクライアント側へ送信することができる所謂push型の機能です。
この機能の誕生には、Web技術の歴史が関係していると感じます。
1990年代から現代で、Webは用途の拡張が繰り返されました。それに伴いUXの期待値も上がっていきます。従来SSRで静的なMPAアプリケーション・pull型が基本だった時代から、リアルタイム性やサーバの負荷軽減が求められ、結果的にCSR・SPA・Push型への進化を遂げました。
今私たちが当たり前のように使用しているSPAやサーバプッシュ機能は、以前は存在しなかったものです。それが時代とともに拡張されるWebのUX要求を受け止める器として、できていったんだということがわかります。
歴史を紐解く
年表にしてみると、WebのUX要求に答えるように、機能が進化してきたことがよくわかります。作成した年表↓にそって、吹き出しで表現しているサーバプッシュ機能の歴史を順を追って解説したいと思います。

①Meta Refresh
メタリフレッシュは、一定時間後にページを自動的に再読み込み、もしくは別のURLへ遷移させる仕組みです。
技術的にはクライアント側でタイマーを保持し、指定された秒数が経過するとブラウザ自身が再リクエストを行うもので、完全にクライアントサイドの挙動となります。
この仕組みが登場した1995年頃は、JavaScriptはまだ普及途上であり、AJAXも存在しませんでした。そのため、サーバ側で更新された情報をユーザーに反映させる手段は非常に限られており、メタリフレッシュは定期的にページを再表示するための解決策として利用されていました。
ただし、実際に更新されたかに関わらず定期的にサーバへのリクエストを発生させるため、サーバに負荷がかかります。利用者が多かったり、より即時性を求められる場合に向きません。
引用:https://developer.mozilla.org/ja/docs/Web/HTML/Reference/Elements/meta/http-equiv
②AjaxによるPolling
Ajax Pollingというのは、クライアントが一定間隔でサーバへリクエストを送り、新しいデータがあるかを確認し続ける仕組みです。
サーバから能動的に通知することはできないため、常に主導権はクライアント側にあります。
メタリフレッシュとの大きな違いとして、XMLHttpRequestというAPIを使用してJavaScriptの中で送受信を完結させることによって、画面がクリアされることなく最新の情報をサーバから取得することが可能です。
このように、画面クリアをせずにWebページを読み込んだり、時間やタイミングをずらして何度も更新できるアーキテクチャのことを、Ajax(Asynchronous JavaScript and XML)と呼びます。
ただし、実際に更新されたかに関わらず定期的にサーバへのリクエストを発生させるため、サーバに負荷がかかります。利用者が多かったり、より即時性を求められる場合に向きません。
こちらの問題は解決されませんが。。。
それでも、Ajaxの登場により、フロントエンドのJavaScriptの仕事はどんどん増えていきます。ロジックは複雑化し、ソース量も増えていきます。当初はjQueryでちょっとしたDOM加工をしたり、jQuery UIでUIを作ったりしていたくらいでしたが、フロント側に状態管理の機能をきちんと作り込むためのBackbone.jsやKnockout.jsといったクライアントサイドMVCが生まれ、徐々に発展していったようです。
③CometによるLong Polling
Ajax Pollingでは、更新があるかどうかに関わらず、クライアントが一定間隔でサーバへリクエストを送り続ける必要がありました。
この無駄なリクエストが大量に発生するという問題を改善するのが Long Pollingです。Pollingでは一定時間ごとに聞きに行くのに対し、Long Pollingでは、何か起きるまで待たせます。
仕組みとしては、一度クライアントからサーバーにリクエストが送られますが、その場でレスポンスを返すのではなく、返事を保留にしたままにします。HTTPの通信では、サーバー側から通信を完了させるかリクエストがタイムアウトになるまでは、クライアント側にレスポンスが返ってくることはありません。これを応用し、サーバーからのレスポンスを自由なタイミングで返すことで、サーバーからのリクエストに見せかけて情報を送信します。リバースAJAXと呼ばれることもありました。当時は、サーバからクライアントへイベントが流れ込んでくるように見えることから、彗星(Comet)のようにデータが飛んでくるという比喩でこの名前が使われました。
Long PollingはPollingの弱点を大きく改善しましたが、制約は残りました。例えば、一度サーバーからメッセージを送信すると、再びクライアント側からセッションを張り直さないと通信を送れません。このため更新頻度が高い場合には、pollingと変わらないか、それ以上にサーバに負荷をかけてしまいます。
④SSE(Server-Sent Events)
ここまで紹介した手法は、既存技術の応用で、正式に仕様化されたものではありませんでした。しかしここで紹介するSSEは、HTML5の機能の一つとして新たに制定されたものです。
SSEは、CometによるLong Pollingと、HTTP/1.1のチャンク形式による通信機能を組み合わせて応用し、1度のリクエストに対して、サーバから複数のイベント送信を実現しました。
チャンク形式の通信機能は、リクエストやレスポンスをいくつかのチャンク(塊)に分割して送信する機能です。クライアントは、すべてのデータを待つことなく、チャンクの単位でデータを受け取って処理することができます。このため、総量がわからないデータの転送などに役立ってきました。
このチャンク一つ一つをイベントと呼び、イベントの連なりによって構成される回答全体をイベント・ストリームという概念にしたのが、SSEです。
イベントはサーバで用意出来次第のpushされるため低レイテンシで、イベントのオーバーヘッドは最低限に抑えることができ、イベントの解析はブラウザが行うため、無限のバッファは必要ありません。パフォーマンスの観点でも進化できたのです。
しかし、SSEには2つの大きな制限があります。まず、サーバからクライアントへの一方向通信であるということです。そのため、リクエストのストリーミングには使用できません。例えば、サイズの大きなファイルのアップロードのストリーミングなどには使えません。次に、イベントストリームフォーマットはUTF-8文字列の転送のために設計されたプロトコルであるということです。バイナリデータのストリーミングも可能ではありますが、非効率です。
ただし、後者のUTF-8 の制限はアプリケーションで解決することもできます。例えば、サーバで新しいバイナリデータが準備できたことをSSE経由でアプリケーションに通知し、その通知を受け取ったアプリケーションがXHRリクエストを発行してデータを取得することもできます。この方法では 1 往復分のレイテンシが追加発生しますが、レスポンスキャッシュ、圧縮など、XHRが提供する多くのサービスを利用できるという利点も併せ持ちます。リソースがストリームで送信される場合、ブラウザにはキャッシュされません。
引用:
- https://developer.mozilla.org/ja/docs/Web/API/Server-sent_events
- https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest
⑤Web Socket
CometやSSEは、これまでHTTPでは実現できないとされてきたサーバからクライアントへのプッシュ配信を実現する技術でした。一方で、クライアントからサーバへの通信は従来通りのHTTPリクエストを使用しなければなりません。時代の流れとともに、Webの期待されるUXは拡張されていき、チャットやオンラインゲームの用途においてブラウザ/サーバ間で、これまでより高頻度で大量のデータを相互にやり取りする必要が出てきます。このような中でHTTPの双方向通信ができないという特性が足枷となります。また、HTTPでは通信を支えるTCPの接続処理や、リクエストヘッダやレスポンスヘッダの送受信がオーバーヘッドとなっており、通信効率が悪いという欠点がありました。このような状況を踏まえて誕生したのがWebSocketです。
WebSocketは、サーバ/クライアント間で、オーバーヘッドの小さい双方向通信を実現する(事実上の)独自プロトコルです。振る舞いとしてはTCPソケット通信に非常に近いものです。
TCPソケット通信とは、 ソケットを開いたら、バイト列を好きなタイミングで送信でき、相手からも同様に任意のタイミングでデータが届くというものです。そこにはリクエスト/レスポンスといった概念はなく、通信の主導権や送信タイミングは完全にアプリケーション側に委ねられています。非常に低レベルで、自由度の高い通信形態です。
WebSocketは、HTTPを使って安全に接続を確立したのちに、通信を切り替えて独自プロトコルを使用します。主なコンポーネントは2つで、接続パラメータをネゴシエートする開始ハンドシェイクと、テキストデータとバイナリデータのメッセージベース配信を小さなオーバーヘッドで行うバイナリメッセージフレームの仕組みです。HTTPとして通信がスタートしたら、その中でプロトコルのアップグレードを行い、WebSocketへスイッチします(WebSocketを希望する場合はヘッダにそのための情報を入れ込みます)。通信が確立すると、サーバ/クライアント間で一対一の通信を行い、双方から自由にデータを送受信できます。通信はメッセージフレーム単位で、 ヘッダーが超軽量のため低レイテンシです。相手が決まっているので送信先の情報などは持たず、HTTPの基本要素のうちボディのみを送ってるようなイメージです。
革新的ですが、独自プロトコルであることはさまざまな制約を生みます。状態管理、圧縮、キャッシュなどの、ブラウザが提供する便利なサービスを利用することができなくなります。そこはアプリケーション実装者が責任を持って補完しなければいけません。要は、WebSocketはSSEやXHRの単なる置き換えではないということです。最高のパフォーマンスを実現するためには、それぞれの通信方法を適切に利用することが重要です。
また、HTTPがステートレスなのに対し、WebSocketはステートフルです。HTTPでは、ロードバランサを使ってサーバを複数台に分散した際に、リクエストのたびに別のサーバが応答しても問題なく動きますが、WebSocketではそうはいきません。WebSocketでは一度確立した接続が特定のサーバに紐づくため、接続中に別サーバへ切り替えることは不可能で、一旦接続が切れるようなことがあれば、再接続は前と同じステートを持つサーバである必要があります。またスケールインでサーバが消えれば、もちろんステートは失われます。よって単純なHTTPベースのロードバランサを使用する際は注意が必要で、ステートを考慮した設計(スティッキーセッションやステートの外部化)が必要になります。
まとめ
Webの普及と発展の歴史を紐解くことで、これまでのウェブではできなかったような多様で高度なリクエストに答える機能が追加されてきた背景を理解できました。これらの機能により、Webの体験はさらにリッチなものになり、その副作用として通信部分の負荷が高くなりました。
Webアプリの開発者である我々は、これらの個々の機能の特徴と違いを理解した上で、要件に対する最適な設計をする技術が求められるなと、改めて認識しました。