Server Sent Events(SSE)の使いどころと使い方

Flameの箱を捨ててしまったためどうやって送り返すか困っています。@kyo_agoです。

今日は2014年6月にβ公開したGREEチャットで通信に使用しているSSEを紹介したいと思います。

SSEとは

SSEとはServer-Sent Eventsの略でW3Cで提案されているhtml5関連APIの一種です。

これはサーバとの通信やJavaScript APIを中心としたもので、サーバからPush通信を行うための仕様です。

サーバからPush通信に関してはこれまでもCometやWebSocketが存在しましたが、SSEは互換性や効率などの点でそれ以外の技術に対する特徴があります。

ここからは具体的な仕様や、実際に使用した場合の感想などを紹介したいと思います。

通信方式

SSEはHTTP/1.1を使用し、Content-Type: text/event-streamで通信を行います。

基本的な通信内容は以下のとおりです。

特徴はContent-Lengthが指定されていないことと、サーバから通信が切断されないことです。

この特徴によりクライアントはサーバから断続的に送信されてくるデータを随時受け取ることができます。

JavaScript API

SSEはJavaScriptから通信内容を取得するAPIとしてEventSource objectを提供しています。

基本的な実装は以下のとおりです。

new EventSourceの第一引数はSSE形式でデータを返すURL文字列です。

new EventSourceで生成したインスタンスはaddEventListenerでイベントを設定でき、'open', 'message', 'error'やその他サーバが指定したイベントを捕捉できます。

addEventListenerで捕捉したイベントはcallback関数の引数のdataプロパティで受信したデータを取得できます。

Cometとの違い

SSEはCometと同じような問題を解決するために策定され、技術的にも近いものになっています。

しかし、SSEは後発なこととW3C上で議論されていることなどから、Cometに存在した問題点がいくつか改善されています。

具体的には以下の様な違いがあります。

  • Cometはサーバから最初にデータを受信できた時点で切断し、次のデータは再接続して取得するため、SSEとくらべて再接続のコストがかかる。
  • SSEは専用のJavaScript APIが存在し、切断時の再接続、取得できたデータの履歴管理、カスタムイベントの提供が標準で行われる。
  • SSEは通信方式が仕様化されているため、サーバ側の各言語やフレームワークでライブラリ等が提供されていることが多い。
  • Cometは通常のHTTP通信に近い動作を行うため、ブラウザの互換性が高い(SSEはAndroid標準ブラウザ等で動かすために特殊な対応が必要な場合がある)

WebSocketとの違い

WebSocketはSSEとくらべて広い利用方法を想定して定義された仕様ですが、利用方法として「サーバからのPush通信を行う」ことも含まれているためSSEとかぶるところがあります。

本来、SSEが既存の技術の組み合わせで作られていることに対して、WebSocketは既存の技術のしがらみ無しに作られていることから単純な比較が難しい部分もありますが、合わせて語られることが多いためここで比較したいと思います。

WebSocketとSSEは以下の様な違いがあります。

  • WebSocketはHTTPではなく専用のプロトコルを使うため、パフォーマンスが高い
  • SSEはHTTPを使うため、通信の互換性が高い。
  • SSEはHTTPのため、既存のセキュリティモデルを流用できる(既存のセキュリティモデルに引っ張られる)
  • SSEはHTTPなので、同じOriginの別URLでコンテンツを提供できる(htmlやjs, css、画像、他のSSE API等を同じドメインとポートで提供できる)

ブラウザ互換性

SSEは仕様上JavaScript APIが定義されているため、ブラウザがサポートしていない場合JavaScriptからAPIを呼び出すことができません。

ただ、これに関してはXHRを使ったPolyfillが公開されているため、IE等のSSE未サポートブラウザでも大半の動作は同じように動かすことが可能です。

(ちなみに、このPolyfillはSSEをネイティブでサポートしているブラウザのバグを潰す目的でも開発されているため仕様との互換性はネイティブのEventSourceより高い場合もあります。ただし、内部でXHRを使っているためAndroid標準ブラウザや古いIEなどのXHR自体に問題のあるブラウザでは動作がおかしい場合もあるため注意してください)

各通信方法との比較

それぞれを簡単に比較すると以下のとおりです。

SSE WebSocket Comet
通信コスト
JavaScriptAPI
サーバサイドサポート
通信互換性
仕様安定性
ブラウザ互換性

通信コストはWebSocketが効率的です。SSEは接続を維持するためCometよりは効率的に通信が可能です。

JavaScriptAPIはSSE、WebSocketは仕様化されているものが存在しますが、Cometの場合独自に定義する必要があります。

サーバサイドサポートに関してはSSE、WebSocketは仕様化されているため各種言語でのライブラリが提供されていますが、Cometは仕様化されていないこともあり存在しない場合もあります。

通信互換性に関して、WebSocketは独自プロトコルのため接続できない場合もあります。SSE、CometはどちらもHTTPを使用しますが、Cometの方が通常のHTTP接続の形式に近く接続性は高いと思っています。

仕様安定性に関して、SSEはプロトコル自体が簡単な事もあり仕様バージョンで混乱することはないと思います。WebSocketは過去に若干通信バージョンが混乱しましたが、現在では比較的安定しています。Cometはそもそも決められた仕様が存在しないため細かい部分で調整が必要になる可能性があります。

ブラウザ互換性に関して、Cometだと古いIEやAndroidでも比較的容易に接続できますが、SSEだとPolyfillを使っても素直に接続できない場合があります。WebSocketはFlashなどを使ってPolyfillを作成することも可能ですが、Androidの標準ブラウザはFlashが動作せず、WebSocket自体もサポートされていないため対応が困難です。

SSEの問題点

ここからはSSEを実際に使ってわかった問題点を紹介したいと思います。

  1. ブラウザの実装にバグが多い(Polyfillがブラウザ実装無視してオブジェクト乗っ取るレベル)
  2. デバッグ辛い(Polyfillのコードがかなり複雑なのと、そもそもStream通信のデバッグは辛い)
  3. Polyfillのパフォーマンスは高くない(初期化コストはネイティブオブジェクトとくらべてiPhoneだと体感できるレベル)
  4. Android 2系の対応は辛い(特にCORSと絡めるとほんとうに辛い)

(1)に関しては基本的にPolyfillを使うことで回避できますが、最新のブラウザだと改善されている項目も多いため、UA判断でネイティブオブジェクトを使用しても問題ないと思います。

ちなみに、ブラウザ上のバグはPolyfillでテストケース化されています

(2)はおそらく仕様上解析が大変な部分があることと、パフォーマンス的な意味でPolyfillのコードが複雑なため、通信中のデバッグはかなり困難でした。

ただ、そもそもネイティブオブジェクトでデバッグを行う場合内部状態が外から参照できないため、コードが複雑であってもPolyfillを使用して開発したほうが楽な部分はあります(SSEのイベント発火前にbreak pointを入れたりできるので)

(3)は、実際SSEを使用する場合Polyfillは必須に近い存在ですが、やはりネイティブオブジェクトと比較するとパフォーマンス的に若干ペナルティがあります。

そのため、できるだけ速度を稼ぎたい場合はUA等で切り出してネイティブオブジェクトを使ったり、そもそもSSEではなく素のXHRで対応するほうが良いと思います。

(4)のAndroid 2系に関しては、今回htmlとSSE APIのドメインが違ったことで特に対応が困難でした。

Android 2系は元々一回のデータ受信が4KBを超えないとXHRからデータを取得できないというバグがあり、PolyfillもXHRを使用しているためこの影響を受けます。

更にAndroid 2系はXHR Level2をサポートしておらずCORSが使えないため、対応するためにiframe経由のpostMessageでデータのやり取りを行いました。

これ自体SSEの問題というわけではありませんが、実際対応をする場合には注意してください。

本当のSSE

ここからは一般的に言われるSSEの評価と使ってみての実感を紹介します。

SSEは一般的にEventSource, frankly, sucksと言われますが、Polyfillを使用すればEventSource自体はそこまで大きな問題と感じませんでした。

ただ、Polyfillは実装が複雑でデバッグが難しく、Android等の問題があるブラウザ上での開発は難しいと感じました。

速度的にはPolyfillを使用しても十分な速度ではありますが、高速なデータのやり取りをする場合ネイティブオブジェクトを直接使用するほうが高速に動作します。

また、Polyfillはブラウザがネイティブ実装を持っている場合でもPolyfill objectでの動作を優先しますが、最近のブラウザではネイティブオブジェクトの動作も改善しているため場合によってはPolyfillなしでも実装は可能だと感じました。
(それでもPolyfillの方が仕様への準拠度が高いことと、各ブラウザの動作を統一したかったためPolyfillを使用しました)

SSE自体に関してはプロトコル、APIともに比較的単純なため、簡単なサンプル実装や、本格的に実装する上で困難になる部分はありませんでした。

SSEの今後

ここまでSSEの過去、現在を紹介してきましたが、最後にSSEの今後に関して紹介したいと思います。

SSEは仕様的に現在「勧告候補(Candidate Recommendation)」ですが、各言語のバインディングやブラウザ上の実装も進んでいることから、今後仕様レベルで大きな変更が行われる可能性は低いのではないかと思っています。

では、今後SSEが大きく使われていくかというと個人的には疑問があります。理由として、SSEはあくまでもCometの発展形であり、既存のHTTP上でこれまであったXMLHTTPRequestでPolyfillが書ける範囲の機能しか提供しないためです。

もちろんサーバから簡単にPush通信を送る場合には便利な方法ではありますが、もしWebSocketが使えるのであればWebSocketを使う方が良いと思います。

このため、ネットワーク上の互換性が進み、WebSocketがHTTP並に接続できるようになればあえてSSEを使う理由は小さくなっていくと思います。