Server-Sent Events(SSE)をPHPで使うときの自分用まとめ
Server-Sent Events(SSE)をPHPで使うときの自分用まとめ:
SSEを自分なりに調べました、間違いなどありましたらご指摘お願いいたします
Server-Sent Events とは
Google翻訳↓この仕様は、DOMイベントの形式でサーバーからプッシュ通知を受信するためのHTTP接続を開くためのAPIを定義します。 このAPIは、Push SMSなどの他のプッシュ通知スキームと連携するように拡張できるように設計されています。よくwebsocketと比較される。websocketはブラウザとサーバーで双方向通信できるが、SSEはサーバーからブラウザへの一方向の通信。
SSEのすぐ動かせるサンプル
↑のページのコードを参考にさせていただきましたevents.php
<?php header('Content-Type: text/event-stream'); header('Cache-Control: no-store'); while(true) { echo sprintf("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); }
index.html
<!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 => { ({ time, word } = JSON.parse(e.data)); sample.appendChild(document.createElement('li')).textContent = `${time} ${word}`; }); }); </script> </head> <body> <ul id="sample"></ul> </body> </html>
このとき、stream.phpは以下のように文字列を返しています
フィールドごとに調べたこと
data
、event
、id
、retry
のそれぞれのフィールドについて調べました
data
基本的に送信するデータは、'data: ホゲホゲ'
という文字列をecho
、print_r
などで出力すると良い、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); // ホゲホゲ });
stream.php
の出力内容が、空白行ごとに区切られてjsのmessageイベントが実行されていきます。送信したい単位で1行空白行を入れることを忘れないようにしましょう。例1
stream.phpのレスポンス内容
data: ホゲホゲ data: フガフガ data: ピヨピヨ
message
イベントは3回実行されることになります例2
空白行を入れずに複数行データを送信することもできます
stream.phpのレスポンス内容
data: ホゲホゲ data: フガフガ data: ピヨピヨ
ホゲホゲ
とフガフガ\nピヨピヨ
で message
イベントは2回実行されます
NG例
- 大文字小文字は区別される
php
<?php header('Content-Type: text/event-stream;'); echo 'Data: 送信データ'; // Dが大文字なので無視される echo "\n\n";
- 送信したい単位で空白行を1行入れないと送信されない
php
<?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
<?php header('Content-Type: text/event-stream'); echo sprintf("data: %s\n\n", json_encode([ 'hoge' => 1, 'fuga' => 'A', 'piyo' => false, ]));
javascript
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
自分でイベント名を設定することができるphp
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イベントで受け取れる
phpのレスポンス内容
event: add data: 追加 data: ホゲ event: update data: 更新
js
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
で取得できるようでした。js
const es = new EventSource('./events.php'); es.addEventListener('message', e => { console.log(e.lastEventId, e.data); });
php
<?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
イベントが実行されるようでした。php
echo "data:".$_SERVER['HTTP_LAST_EVENT_ID']."\n\n"; // 3
retry
再接続しに行くまでの秒数を設定できます。デフォルトが何秒なのかを調べましたが、見つけることができませんでした。感覚的には3秒ぐらいなようです。
php
<?php header('Content-Type: text/event-stream'); echo "retry:10000\n"; // 10秒に設定 echo "data: hoge\n\n";
接続時のイベント
js
const es = new EventSource('./events.php'); es.addEventListener('open', e => { console.log('open'); });
エラー処理
error
イベントで、接続できなかった場合、接続が途切れた場合などの処理を書けるjs
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() !== 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()メソッドが呼び出されました。
js
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
js
// 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); });
http://localhost:8888/events.php
<?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を使う
js
// 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); });
http://localhost:8888/events.php
<?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";
文字化けした
文字化けした時にheaderでutf-8
と明示すると文字化けしなくなりましたphp
<?php header('Content-Type: text/event-stream; charset=utf-8');
nginxで実行
apacheだと特に問題なく動いたのですが、nginxだとうまく行きませんでした、下のコードだと1秒おきに出力して欲しいのですが、バッファされているようだけのようでタイムアウトしてしまいました。
↓これだとnginxではうまく動きませんでした。
php
<?php header('Content-Type: text/event-stream; charset=utf-8'); header('Cache-Control: no-store'); while(true) { echo sprintf("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) { echo sprintf("data: %s\n\n", json_encode([ 'time' => (new DateTime())->format(DateTime::RFC3339), ])); ob_end_flush(); flush(); sleep(1); }
nginxの設定ファイルをいじるという方法でも大丈夫なようです
ブラウザ対応状況
https://caniuse.com/#search=server-sent%20events2019/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
コメント
コメントを投稿