SSEを自分なりに調べました、間違いなどありましたらご指摘お願いいたします
Server-Sent Events とは
Google翻訳↓
この仕様は、DOMイベントの形式でサーバーからプッシュ通知を受信するためのHTTP接続を開くためのAPIを定義します。 このAPIは、Push SMSなどの他のプッシュ通知スキームと連携するように拡張できるように設計されています。
よくwebsocketと比較される。websocketはブラウザとサーバーで双方向通信できるが、SSEはサーバーからブラウザへの一方向の通信。
SSEのすぐ動かせるサンプル
↑のページのコードを参考にさせていただきました
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-store');
while(true) {
printf("data: %s\n\n", json_encode([
'time' => (new DateTime('now', new DateTimeZone('Asia/Tokyo')))->format('H:i:s'),
'word' => 'abcあいう😀😁😂',
]));
ob_end_flush();
flush();
sleep(1);
}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
document.addEventListener('DOMContentLoaded', e => {
const es = new EventSource('./events.php');
es.addEventListener('message', e => {
const { time, word } = JSON.parse(e.data);
sample.appendChild(document.createElement('li')).textContent = `${time} ${word}`;
});
});
</script>
</head>
<body>
<ul id="sample"></ul>
</body>
</html>
このとき、events.phpは以下のように文字列を返しています
フィールドごとに調べたこと
data
、event
、id
、retry
のそれぞれのフィールドについて調べました
data
基本的に送信するデータは、'data: ホゲホゲ'
という文字列をecho
、print
などで出力すると良い、javascriptでmessage
イベントでe.data
からホゲホゲ
という文字列を取得できる
<?php
header('Content-Type: text/event-stream');
echo 'data: ホゲホゲ';
echo "\n\n";
const es = new EventSource('./events.php');
es.addEventListener('message', e => {
console.log(e.data); // ホゲホゲ
});
events.php
の出力内容が、空白行ごとに区切られてjsのmessageイベントが実行されていきます。送信したい単位で1行空白行を入れることを忘れないようにしましょう。
例1
data: ホゲホゲ
data: フガフガ
data: ピヨピヨ
↑だと message
イベントは3回実行されることになります
例2
空白行を入れずに複数行データを送信することもできます
data: ホゲホゲ
data: フガフガ
data: ピヨピヨ
↑だとホゲホゲ
とフガフガ\nピヨピヨ
で message
イベントは2回実行されます
NG例
- 大文字小文字は区別される
<?php
header('Content-Type: text/event-stream;');
echo 'Data: 送信データ'; // Dが大文字なので無視される
echo "\n\n";
- 送信したい単位で空白行を1行入れないと送信されない
<?php
header('Content-Type: text/event-stream;');
echo 'data: A'; // A が送信される
echo "\n\n";
echo 'data: B';
echo "\n";
echo 'data: C'; // B\nC が送信される
echo "\n\n";
echo 'data: D'; // この行の後に空白行が続いていないため送信されない
書き方が間違っていてもエラーになるわけではなく、その行が無視されるだけなのでそこが個人的にやりにくかったです
複数データの送信
phpでJSON形式の文字列を送信し、jsでJSON.parse(e.data)
とする
<?php
header('Content-Type: text/event-stream');
printf("data: %s\n\n", json_encode([
'hoge' => 1,
'fuga' => 'A',
'piyo' => false,
]));
const es = new EventSource('./events.php');
es.addEventListener('message', e => {
({hoge, fuga, piyo} = JSON.parse(e.data));
console.log(hoge, fuga, piyo); // 1 "A" false
});
JSONをプリティプリントする
送信するJSONの文字列が長くなってしまうとデバッグがしずらくなってしまいます
<?php
header('Content-Type: text/event-stream');
echo 'data:'.json_encode($_SERVER)."\n\n";
私にはこのようなやり方しか思いつきませんでしたが、とりあえずこれで人間の目にも優しく、EventSourceとしてもJSONとして扱えるように出力することができました
<?php
header('Content-Type: text/event-stream');
// echo 'data:'.json_encode($_SERVER)."\n\n";
echo implode("\n", preg_replace('/^/', 'data:', explode("\n", json_encode($_SERVER, JSON_PRETTY_PRINT))))."\n\n";
event
自分でイベント名を設定することができる
header('Content-Type: text/event-stream');
echo "event: add\n";
echo "data: 追加\n\n"; // addイベントで受け取れる
echo "data: ホゲ\n\n"; // event名を設定していないので、messageイベントで受け取れる
echo "event: update\n";
echo "data: 更新\n\n"; // updateイベントで受け取れる
event: add
data: 追加
data: ホゲ
event: update
data: 更新
const es = new EventSource('./events.php');
es.addEventListener('message', e => {
console.log('message -> '+e.data); // ホゲ
});
es.addEventListener('add', e => {
console.log('add -> '+e.data); // 追加
});
es.addEventListener('update', e => {
console.log('update -> '+e.data); // 更新
});
id
data
と一緒に一意のIDの文字列を送ることができます。
e.lastEventId
で取得できるようでした。
const es = new EventSource('./events.php');
es.addEventListener('message', e => {
console.log(e.lastEventId, e.data);
});
<?php
header('Content-Type: text/event-stream');
echo "id: 1\n";
echo "data:hoge\n\n";
echo "id: 2\n";
echo "data:fuga\n\n";
echo "id: 3\n";
echo "data:piyo\n\n";
EventSource
ですが、接続先とエラーで接続が途切れた場合、自動で再接続しに行きます。
再接続の際にリクエストヘッダーでLast-Event-ID
を一緒に送信するようです
※events.phpの処理が正常終了した場合もエラー扱いなようで、error
イベントが実行されるようでした。
echo "data:".$_SERVER['HTTP_LAST_EVENT_ID']."\n\n"; // 3
retry
再接続しに行くまでの秒数を設定できます。
デフォルトが何秒なのかを調べましたが、見つけることができませんでした。感覚的には3秒ぐらいなようです。
<?php
header('Content-Type: text/event-stream');
echo "retry:10000\n"; // 10秒に設定
echo "data: hoge\n\n";
接続時のイベント
const es = new EventSource('./events.php');
es.addEventListener('open', e => {
console.log('open');
});
エラー処理
error
イベントで、接続できなかった場合、接続が途切れた場合などの処理を書ける
const es = new EventSource('./events.php');
es.addEventListener('error', e => {
console.log('error');
es.close(); // エラーが起きても再接続する必要がない場合はclose()を実行する
})
phpの方でも接続が切れた場合の処理を作成してみる。
<?php
ignore_user_abort(true);
header('Content-Type: text/event-stream');
// 接続中の間はループ
while(!connection_aborted()) {
echo "data: ホゲ\n\n";
ob_end_flush();
flush();
sleep(1);
}
// 接続が切れた場合の処理
// ...
connection_aborted()
でクライアントとの接続が切れているかわかるようです。
ただphpのデフォルトではクライアントが接続を破棄した後はスクリプトは終了してしまうため、whileを抜けた後の処理が実行されません。
なのでignore_user_abort(true);
、またはini_set('ignore_user_abort', 1);
などとすることで接続が切れた後も途中終了せず処理を実行してくれるようです。
参考
接続状態の確認
- 引用:https://www.w3.org/TR/eventsource/ ※google翻訳↓
- 0:接続はまだ確立されていないか、または切断されてユーザーエージェントが再接続しています。
- 1:ユーザーエージェントはオープン接続を持っており、それを受け取るとイベントを送出しています。
- 2:接続は開かれておらず、ユーザーエージェントは再接続しようとしていません。致命的なエラーが発生したか、close()メソッドが呼び出されました。
const es = new EventSource('./events.php');
console.log('init -> '+es.readyState);
es.addEventListener('open', e => {
console.log('open -> '+es.readyState);
});
es.addEventListener('message', e => {
console.log('message -> '+es.readyState);
});
es.addEventListener('error', e => {
console.log('error -> '+es.readyState);
es.close();
console.log('error -> '+es.readyState);
})
以下は一度接続し、接続中にサーバをシャットダウンした時の動作です。es.readyState
で正しく状態が取得できるのが確認できました。
クロスオリジン
ajaxと同じです。
urlを別のドメインに設定すると以下のようにエラーになります
あまりやったことがないので、以下を参考にしつつ行いました。
間違っていたらご指摘お願いいたしますm(_ _)m
- CORSリクエストでクレデンシャル(≒クッキー)を必要とする場合の注意点 - Qiita
- jquery - CORS not working php - Stack Overflow
- php - While loops for server-sent events are causing page to freeze - Stack Overflow
別ドメインのphpを読み込む
// http://192.168.33.10 から http://localhost:8888 を読み込みに行く
const es = new EventSource('http://localhost:8888/events.php');
es.addEventListener('message', e => {
console.log(e.data);
});
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header("Access-Control-Allow-Origin: *");
echo "data:{$_SERVER["HTTP_HOST"]}{$_SERVER["REQUEST_URI"]}\n\n";
別ドメインからデータを取得できました
cookieを使う
// http://192.168.33.10 から http://localhost:8888 を読み込みに行く
const es = new EventSource('http://localhost:8888/events.php', { withCredentials: true });
es.addEventListener('message', e => {
console.log(e.data);
});
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
header('Access-Control-Allow-Credentials: true');
setcookie('user', 'Jhon');
echo "data: {$_SERVER["HTTP_HOST"]}{$_SERVER["REQUEST_URI"]}\n";
echo "data: ".json_encode($_COOKIE)."\n\n";
クロスオリジンでcookieを設定できました
文字化けした
文字化けした時にheaderでutf-8
と明示すると文字化けしなくなりました
<?php
header('Content-Type: text/event-stream; charset=utf-8');
nginxで実行
apacheだと特に問題なく動いたのですが、nginxだとうまく行きませんでした、
下のコードだと1秒おきに出力して欲しいのですが、バッファされているようだけのようでタイムアウトしてしまいました。
↓これだとnginxではうまく動きませんでした。
<?php
header('Content-Type: text/event-stream; charset=utf-8');
header('Cache-Control: no-store');
while(true) {
printf("data: %s\n\n", json_encode([
'time' => (new DateTime())->format(DateTime::RFC3339),
]));
ob_end_flush();
flush();
sleep(1);
}
X-Accel-Buffering: no
を追加するとうまく行きました
<?php
header('Content-Type: text/event-stream; charset=utf-8');
header('Cache-Control: no-store');
+ header('X-Accel-Buffering: no');
while(true) {
printf("data: %s\n\n", json_encode([
'time' => (new DateTime())->format(DateTime::RFC3339),
]));
ob_end_flush();
flush();
sleep(1);
}
nginxの設定ファイルをいじるという方法でも大丈夫なようです
ブラウザ対応状況
2019/2/3現在
IE、Edgeでは使えないようなのでpolifillを使うと良いようです
その他自分が疑問に思った部分
ob_end_flush()
とflush()
の両方を実行しないといけないのはなぜ?
@7of9 さんから情報いただきました、ありがとうございますm(_ _)m
ob_end_flush()
とob_flush()
はどちらが良い?
sseで使うのにどちらを使ったほうがいいのかはわかりませんでした。
解説ページによって、ob_flush()
だったりob_end_flush()
だったりしていて、どちらでも動きとしては問題なさそうでした。
情報お持ちの方おりましたらよろしくお願いいたします
参考
- Server-sent events - Wikipedia
- Server-Sent Events
- Stream Updates with Server-Sent Events - HTML5 Rocks
- キャッシュについて整理 - Qiita
- ExpressでServer Sent Event (SSE) を簡単に扱ってみる - Qiita
- WebSocketとServer-Sent Eventの違いとリアルタイムWebアプリの作り方 - WPJ
- 十三章第二回 Server-Sent Events — JavaScript初級者から中級者になろう — uhyohyo.net
- サーバPUSHざっくりまとめ
- Server Sent Events(SSE)の使いどころと使い方 | GREE Engineers' Blog
最後まで読んでいただいてありがとうございましたm(_ _)m