2020年11月30日

サーバー送信イベント

The サーバー送信イベント 仕様書は、サーバーとの接続を維持し、サーバーからイベントを受信することを可能にする組み込みクラスEventSourceについて説明しています。

WebSocketと同様に、接続は永続的です。

しかし、いくつかの重要な違いがあります。

WebSocket EventSource
双方向:クライアントとサーバーの両方がメッセージを交換できます。 一方向:サーバーのみがデータを送信します。
バイナリデータとテキストデータ テキストのみ
WebSocketプロトコル 通常のHTTP

EventSourceは、WebSocketよりもサーバーとの通信能力が低い方法です。

なぜこれを使うべきなのでしょうか?

主な理由は、それがよりシンプルであるということです。多くのアプリケーションでは、WebSocketのパワーは少し多すぎます。

サーバーからデータストリームを受信する必要があります。チャットメッセージや市場価格などです。それがEventSourceの長所です。また、自動再接続をサポートしており、WebSocketでは手動で実装する必要があります。さらに、新しいプロトコルではなく、従来のHTTPです。

メッセージの受信

メッセージの受信を開始するには、new EventSource(url)を作成するだけです。

ブラウザはurlに接続し、接続をオープンに保ち、イベントを待ちます。

サーバーはステータス200とヘッダーContent-Type: text/event-streamで応答し、接続を維持し、次の特別な形式でメッセージを書き込む必要があります。

data: Message 1

data: Message 2

data: Message 3
data: of two lines
  • メッセージテキストはdata:の後に続きます。コロンの後のスペースはオプションです。
  • メッセージは二重改行\n\nで区切られます。
  • 改行\nを送信するには、すぐに別のdata:を送信できます(上記の3番目のメッセージ)。

実際には、複雑なメッセージは通常JSONでエンコードされて送信されます。改行は\nとしてエンコードされるため、複数行のdata:メッセージは必要ありません。

例えば

data: {"user":"John","message":"First line\n Second line"}

…したがって、1つのdata:が正確に1つのメッセージを保持すると仮定できます。

このようなメッセージごとに、messageイベントが生成されます。

let eventSource = new EventSource("/events/subscribe");

eventSource.onmessage = function(event) {
  console.log("New message", event.data);
  // will log 3 times for the data stream above
};

// or eventSource.addEventListener('message', ...)

クロスオリジンリクエスト

EventSourceは、fetchやその他のネットワークメソッドと同様に、クロスオリジンリクエストをサポートしています。任意のURLを使用できます。

let source = new EventSource("https://another-site.com/events");

リモートサーバーはOriginヘッダーを取得し、続行するにはAccess-Control-Allow-Originで応答する必要があります。

資格情報を渡すには、次の追加オプションwithCredentialsを設定する必要があります。

let source = new EventSource("https://another-site.com/events", {
  withCredentials: true
});

クロスオリジンヘッダーの詳細については、Fetch:クロスオリジンリクエストの章を参照してください。

再接続

作成時に、new EventSourceはサーバーに接続し、接続が切断されると再接続します。

これは非常に便利です。気にしなくて済みます。

再接続間には短い遅延があり、デフォルトでは数秒です。

サーバーは、応答でretry:を使用して推奨される遅延(ミリ秒単位)を設定できます。

retry: 15000
data: Hello, I set the reconnection delay to 15 seconds

retry:は、いくつかのデータと一緒に、またはスタンドアロンメッセージとして送られる場合があります。

ブラウザは、再接続する前にそのミリ秒数待つ必要があります。または、ブラウザが(OSから)現在ネットワーク接続がないことを知っている場合など、より長く待つ場合があります。接続が表示されるまで待ってから再試行します。

  • サーバーがブラウザに再接続を停止させたい場合は、HTTPステータス204で応答する必要があります。
  • ブラウザが接続を閉じたい場合は、eventSource.close()を呼び出す必要があります。
let eventSource = new EventSource(...);

eventSource.close();

また、応答のContent-Typeが正しくない場合、またはHTTPステータスが301、307、200、204と異なる場合、再接続は行われません。このような場合、「error」イベントが送信され、ブラウザは再接続しません。

ご注意ください

接続が最終的に閉じられた場合、「再開」する方法はありません。再度接続したい場合は、新しいEventSourceを作成してください。

メッセージID

ネットワークの問題により接続が切断された場合、どちら側もどのメッセージが受信され、どのメッセージが受信されなかったかを確認できません。

接続を正しく再開するには、各メッセージにidフィールドが必要です。

data: Message 1
id: 1

data: Message 2
id: 2

data: Message 3
data: of two lines
id: 3

id:付きのメッセージを受信すると、ブラウザは

  • プロパティeventSource.lastEventIdをその値に設定します。
  • 再接続時に、そのidLast-Event-IDヘッダーで送信し、サーバーが後続のメッセージを再送信できるようにします。
data:の後にid:を配置します。

ご注意ください:lastEventIdがメッセージ受信後に更新されるように、サーバーによってメッセージdataの下にidが付加されます。

接続状態:readyState

EventSourceオブジェクトにはreadyStateプロパティがあり、3つの値のいずれかを持ちます。

EventSource.CONNECTING = 0; // connecting or reconnecting
EventSource.OPEN = 1;       // connected
EventSource.CLOSED = 2;     // connection closed

オブジェクトが作成されたとき、または接続が切断されたときは、常にEventSource.CONNECTING(0に等しい)です。

このプロパティをクエリして、EventSourceの状態を確認できます。

イベントタイプ

デフォルトでは、EventSourceオブジェクトは3つのイベントを生成します。

  • message – 受信したメッセージ。event.dataとして使用できます。
  • open – 接続が開いています。
  • error – 接続を確立できませんでした(例:サーバーがHTTP 500ステータスを返しました)。

サーバーは、イベントの先頭にevent: ...を使用して別のタイプのイベントを指定できます。

例えば

event: join
data: Bob

data: Hello

event: leave
data: Bob

カスタムイベントを処理するには、onmessageではなくaddEventListenerを使用する必要があります。

eventSource.addEventListener('join', event => {
  alert(`Joined ${event.data}`);
});

eventSource.addEventListener('message', event => {
  alert(`Said: ${event.data}`);
});

eventSource.addEventListener('leave', event => {
  alert(`Left ${event.data}`);
});

完全な例

これが、123、そしてbyeを送信し、接続を切断するサーバーです。

その後、ブラウザは自動的に再接続します。

結果
server.js
index.html
let http = require('http');
let url = require('url');
let querystring = require('querystring');
let static = require('node-static');
let fileServer = new static.Server('.');

function onDigits(req, res) {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream; charset=utf-8',
    'Cache-Control': 'no-cache'
  });

  let i = 0;

  let timer = setInterval(write, 1000);
  write();

  function write() {
    i++;

    if (i == 4) {
      res.write('event: bye\ndata: bye-bye\n\n');
      clearInterval(timer);
      res.end();
      return;
    }

    res.write('data: ' + i + '\n\n');

  }
}

function accept(req, res) {

  if (req.url == '/digits') {
    onDigits(req, res);
    return;
  }

  fileServer.serve(req, res);
}


if (!module.parent) {
  http.createServer(accept).listen(8080);
} else {
  exports.accept = accept;
}
<!DOCTYPE html>
<script>
let eventSource;

function start() { // when "Start" button pressed
  if (!window.EventSource) {
    // IE or an old browser
    alert("The browser doesn't support EventSource.");
    return;
  }

  eventSource = new EventSource('digits');

  eventSource.onopen = function(e) {
    log("Event: open");
  };

  eventSource.onerror = function(e) {
    log("Event: error");
    if (this.readyState == EventSource.CONNECTING) {
      log(`Reconnecting (readyState=${this.readyState})...`);
    } else {
      log("Error has occured.");
    }
  };

  eventSource.addEventListener('bye', function(e) {
    log("Event: bye, data: " + e.data);
  });

  eventSource.onmessage = function(e) {
    log("Event: message, data: " + e.data);
  };
}

function stop() { // when "Stop" button pressed
  eventSource.close();
  log("eventSource.close()");
}

function log(msg) {
  logElem.innerHTML += msg + "<br>";
  document.documentElement.scrollTop = 99999999;
}
</script>

<button onclick="start()">Start</button> Press the "Start" to begin.
<div id="logElem" style="margin: 6px 0"></div>

<button onclick="stop()">Stop</button> "Stop" to finish.

概要

EventSourceオブジェクトは、永続的な接続を自動的に確立し、サーバーがその接続を介してメッセージを送信できるようにします。

それは以下のものを提供します。

  • 調整可能なretryタイムアウトによる自動再接続。
  • イベントを再開するためのメッセージID。最後に受信した識別子は、再接続時にLast-Event-IDヘッダーで送信されます。
  • 現在の状態はreadyStateプロパティにあります。

これにより、EventSourceWebSocketの適切な代替手段となります。後者はより低レベルであり、そのような組み込み機能(実装できますが)がありません。

多くの現実世界のアプリケーションでは、EventSourceのパワーで十分です。

すべての最新のブラウザでサポートされています(IEを除く)。

構文は次のとおりです。

let source = new EventSource(url, [credentials]);

第2引数には、{ withCredentials: true }という1つのオプションしかありません。クロスオリジンの資格情報を送信できます。

全体的なクロスオリジンのセキュリティは、fetchやその他のネットワークメソッドと同じです。

EventSourceオブジェクトのプロパティ

readyState
現在の接続状態:EventSource.CONNECTING (=0)EventSource.OPEN (=1)、またはEventSource.CLOSED (=2)のいずれか。
lastEventId
最後に受信したid。再接続時にブラウザはそれをLast-Event-IDヘッダーで送信します。

メソッド

close()
接続を閉じます。

イベント

message
メッセージを受信しました。データはevent.dataにあります。
open
接続が確立されました。
error
接続が切断された場合(自動的に再接続されます)や致命的なエラーが発生した場合など、エラーが発生した場合。readyStateを確認して、再接続が試行されているかどうかを確認できます。

サーバーは、event:でカスタムイベント名を設定できます。このようなイベントは、on<event>ではなくaddEventListenerを使用して処理する必要があります。

サーバーの応答形式

サーバーは、\n\nで区切られたメッセージを送信します。

メッセージには、次のフィールドが含まれる場合があります。

  • data: – メッセージ本文。複数のdataのシーケンスは、部分間に\nを持つ単一のメッセージとして解釈されます。
  • id:lastEventIdを更新し、再接続時にLast-Event-IDで送信されます。
  • retry: – ミリ秒単位で再接続の再試行遅延を推奨します。JavaScriptから設定する方法はありません。
  • event: – イベント名。data:の前に付ける必要があります。

メッセージには、任意の順序で1つ以上のフィールドを含めることができますが、id:は通常最後に来ます。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点の提案がある場合は、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数語のコードを挿入する場合は<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。