uhyohyo.net

JavaScript初級者から中級者になろう

十三章第二回 Server-Sent Events

このページの最終更新日:

今回紹介するのはServer-Sent Events (SSE)と呼ばれる仕組みです。これはJavaScriptからある種のHTTP通信を行う方法です。前回紹介したXMLHttpRequest (XHR)は、ある程度の制限こそあれ、自由にHTTP通信を行うことができるAPIでした。XHRは“生”のHTTP通信を行うことができます。つまり、自分でリクエストを作って送り、レスポンスの中身をそのまま渡されてあとは自分で処理するというものです。

一方、今回のSSEはXHRに比べてハイレベルなAPIです。SSEで行うことができるHTTP通信の形は決まっていますが、より抽象化された形で通信を扱うことができます。また、実はSSEによるHTTP通信はXHRで真似することはできません。

回りくどくなりましたが、SSEを使うとpush型のデータ通信が可能になります。push型というのは、「サーバーから能動的にデータを送ってくる」ということです。

前回のHTTP通信はどうだったかというと、サーバーからデータをもらうには、こちらからリクエストを送り、それに応じてサーバーがレスポンスを返すという手続をとる必要がありました。これは、こちらからデータをもらいに行かないとデータがもらえないということです。

例えばJavaScriptでチャットを作りたいとしましょう。新しい発言があったかどうかを知るためには、一定時間ごとにこちらからサーバーにリクエストを送ってそれで確かめる必要があるのです。(このような方法はpollingと呼ばれます。)

それに対しpush型ならば、新しい発言があった時点でサーバー側からデータを送ってもらうということが可能です。前者の場合には一定時間ごとにしか最新の発言を得るチャンスはありませんでしたが、後者ならばリアルタイムで発言を取得することが可能です。

そのようなpush型の通信をJavaScriptから扱うことができるのがServer-Sent Eventsなのです。

Server-Sent Eventsの仕組み

実は、SSEにおいてもHTTP通信を使用します。通常のHTTP通信には、こちら側からサーバー側へリクエストを送り、サーバーからレスポンスが帰ってくるとそれで終了します。SSEでは、HTTP通信の範疇でpush型の通信を実現するために次のような方法を用います。SSEではまず、通常通りクライアント側からリクエストを送ります。それに対してサーバー側はレスポンスを返す必要がありますが、サーバーはレスポンスを送信中の状態を維持します。そして、サーバーは送りたいデータがあるたびにその接続でデータを送ります。これは形としては、サーバーからのレスポンスを時間をかけて送り続けているという体裁になります。

ちなみに、これだとサーバーから一方的に送るだけで、逆にこちらからサーバーからへは送れません。なぜなら、HTTP通信はあくまで「リクエスト→レスポンス→終了」という流れをとるため、一度サーバーがデータを送り始めたらもうこちら側からデータを送れなくなるからです。

ちなみに、それを克服した仕組みとしてWebSocketがあります。これも機会があったら紹介します。これはもはやHTTPではない別のプロトコルを使うので、双方向にリアルタイムの通信が可能です。

さて、SSEを使うには、サーバー側がSSEに対応した形式のデータを送ってくれる必要があります。

SSEにおける通信は先に述べたようにHTTP通信であり、サーバーはtext/event-streamというMIMEタイプでレスポンスを返します。サーバーから送られてくるデータの1単位はイベントといいます。

イベントは文字列であり、例えば次のような形です。


data: foo

この場合「foo」という中身のイベントがサーバーから送られてきます。このように、サーバーから送られてくるイベントは行の最初に「data: 」と書かれてある必要があります。また、内容は複数行になっても可能です。この場合、すべての行の最初にdata:が必要です。


data: foo
data: bar

これは、「foo(改行)bar」という2行の内容からなる1つのイベントです。

また、複数のイベントは、空行によって区切られます。例えば、


data: foo
data: bar

data: test

とした場合、「foo(改行)bar」というイベントと「test」というイベントの2つが送られてきていることを意味します。

さて、「push型」というくらいですから、一度に全てのイベントが送られてくる必要はありません。サーバーが送りたいときに、新しいイベントを追加で送ってきてもいいのです。むしろ、それこそがSSEの意味であるとも言えるでしょう。XHRではSSEの真似はできません。なぜなら、XHRではレスポンスが全部到着しないとそれを見ることができないからです。

具体例を見せましたが、ここで大事なことは実は、イベントは複数行にわたって送られてきてもよく、イベントを区切るには空行をはさむということです。また、送られてくるイベントデータの一行一行をフィールドといいます。

イベントの中身

フィールドは、


フィールド名: フィールドの中身

という構成になっています。実は、今までに見た例は全てフィールド名が「data」であった場合となっていました。フィールド名がdataであった場合、そのフィールドは、イベントの内容を表します。上の例のように、dataフィールドは2つ以上あっても構いません。その場合、1つ1つがイベントの中身の1行1行を表すことになりますね。

それでは、フィールド名には他に何があるかというと、eventというフィールドがあります。つまり例えば、


event: test

これは何を表すかというと、イベント名です。つまり、イベントには、中身とは別に名前をつけられるのです。ちなみに、今までの例のような、eventフィールドがない場合、イベントの名前はデフォルトである「message」になります。

今のところ、このdataフィールドとeventフィールドを覚えておくと良いでしょう。

JavaScriptからの利用

では、JavaScriptからServer-Sent Eventsを利用してサーバーからデータを受信する方法を紹介します。それは、EventSourceというオブジェクトを使います。


var stream= new EventSource("/path/to/source");

第1引数には、SSEに対応したデータを配信するURL(またはパス)を指定しています。

このとき、前回も登場した同一オリジンポリシーが適用されます。Access-Control-Allow-Originを使えば異なるオリジンでも通信可能という点はXHRと同じです。

こうしてEventSourceのインスタンスができましたが、これは3つのプロパティを持ちます。urlは接続先のURLです。また、withCredentialsを持っています。これは前回のXHRと同じ意味です。また、readyStateも持っています。readyStateは前回のXHRよりも単純で、値は3つしかありません。また、readystatechangeイベントもありません。

0 (CONNECTING)
また接続が確立されていないことを表します。
1 (OPEN)
現在接続中で、イベントを受信する準備ができていることを表します。
2 (CLOSED)
接続が終了した状態です。

イベントを受信するには、例によってイベントを使います。SSEの場合、サーバーからイベントを受信するたびに1回イベントが発生します。イベント名には、eventフィールドを利用して設定されたイベント名がそのまま使用されます。例えば、デフォルトのmessageという名前のイベントのときは次のようにして検知します。


stream.addEventListener("message",function(e){

});

例えば、サーバーが次のようなデータを送ってきた場合(各イベントの間で時間があいても構いません)、messageというイベントが3回発生します。

data:Hello

data:Hello2

data:foobar

今回のイベントオブジェクトはMessageEventといいます。イベントの中身は、このdataプロパティに文字列で入っています。他にはoriginというプロパティがあり、これはイベントの配信元のURL(EventSourceの第1引数に指定したURLと同じ)が入っています。

接続と切断

注意すべき点として、EventSourceはサーバーから接続が切断された場合再接続を試みるという点があります。ただし、404が返ってきたとか、サーバーに繋がらないとか、そういうときは再接続はしません。一度はつながったのに切れてしまった場合には、再接続を試みるのです。なお、上のreadyStateの2 (CLOSED)というのは、もう再接続もできず終了してしまったという状態を指します。

これは、不慮の事態によって接続が切断されてしまった場合には便利ですが、サーバーからの配信を終了したいという場合には困ります。サーバーが接続を切断してもこちらから再接続しに行ってしまいます。

それに対処するために、EventSourceオブジェクトで発生する2つのイベントを紹介します。それはopenerrorです。openは単純で、配信元との接続が確立して準備が完了したときに発生します。そしてerrorというのは、接続が切断され、再接続を試みるときに発生します。

ですから、errorが発生したときに、再接続を阻止してやればいいのです。しかし、DOMのときと違って、preventDefault()すればいいわけではありません。ここで使うのが、EventSourceのcloseメソッド(引数無し)です。これを呼び出すと、接続を切断して、再接続も行わなくなります。つまり、以下のようになります。

stream.addEventListener("error",function(e){
  stream.close();
});

このcloseメソッドは別にerrorイベントとは関係なく使うことも可能です。例えばイベントが終了したとき、サーバーから適当なイベントを送ってやってそれに応じてcloseメソッドを呼び出すような使い方ができます。

また、再接続に関して、再接続の待機時間(接続が切断されてから、次に再接続する間に待機する時間)を設定することができます。これは、サーバー側から設定します。

どのように設定するかというと、retryフィールドを使います。つまりこうです。


retry: 1000
data:Hello

(以下略)

この1000というのはミリ秒なので、1秒という意味になります。接続が切断されたら1秒待ってから再接続しろということです。retryフィールドがあるイベントを受信すると、EventSourceは再接続までの時間をそのとおりに設定します。

ちなみに、今までdataフィールド、eventフィールド、retryフィールドを紹介しましたが、もう一種類だけあります。それがidフィールドです。これは、そのイベントに対してIDを付加したい場合に使用できます。例えばこんな感じです。


data:Hello
id: abc

data:Hello2
id: foo

このidはMessageEventのlastEventIdプロパティで参照できますが、もうひとつ意味があります。

接続が切れて再接続したとき、一番最後に受信したイベントのIDがLast-Event-IDというHTTPヘッダとなってサーバーへ送られます。これにより、サーバー側はこのクライアントがイベントをどこまで受信したのか知ることができます。サーバー側をうまく作れば、再接続時は続きからイベントを配信するというようなことも可能でしょう。

以上でServer-Sent Eventsの説明は終了です。まとめると、HTTPでサーバー側から一方的なpush型の通信が可能だということです。あまり使い所がないかもしれませんが、もし機会があったら使うのもよいでしょう。