Server-Sent Events(SSE)をPHPで使うときの自分用まとめ



Server-Sent Events(SSE)をPHPで使うときの自分用まとめ:

SSEを自分なりに調べました、間違いなどありましたらご指摘お願いいたします:bow:


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> 


jP6D7HvW9e.gif


このとき、stream.phpは以下のように文字列を返しています



Screen Shot 2019-02-03 at 6.05.35.png



フィールドごとに調べたこと

dataeventidretry のそれぞれのフィールドについて調べました


data

基本的に送信するデータは、'data: ホゲホゲ'という文字列をechoprint_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"; 


Screen Shot 2019-02-03 at 6.58.56.png


私にはこのようなやり方しか思いつきませんでしたが、とりあえずこれで人間の目にも優しく、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"; 


Screen Shot 2019-02-03 at 7.02.54.png



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); // 更新 
}); 


Screen Shot 2019-02-03 at 7.22.37.png



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"; 


Screen Shot 2019-02-03 at 7.53.30.png



EventSourceですが、接続先とエラーで接続が途切れた場合、自動で再接続しに行きます。

再接続の際にリクエストヘッダーでLast-Event-IDを一緒に送信するようです

※events.phpの処理が正常終了した場合もエラー扱いなようで、errorイベントが実行されるようでした。



Screen Shot 2019-02-03 at 75926 copy (1) (1).png


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);などとすることで接続が切れた後も途中終了せず処理を実行してくれるようです。

参考


接続状態の確認

Screen Shot 2019-02-03 at 9.54.58.png

  • 引用: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で正しく状態が取得できるのが確認できました。



Screen Shot 2019-02-03 at 9.59.58.png



クロスオリジン

ajaxと同じです。

urlを別のドメインに設定すると以下のようにエラーになります



Screen Shot 2019-02-03 at 10.47.40.png


あまりやったことがないので、以下を参考にしつつ行いました。

間違っていたらご指摘お願いいたしますm(_ _)m

別ドメインのphpを読み込む

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"; 
別ドメインからデータを取得できました



Screen Shot 2019-02-03 at 11.39.59.png




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"; 
クロスオリジンでcookieを設定できました



Cv0k3HxAPg.gif



文字化けした

文字化けした時に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); 
} 


c4zW9j3qGT.gif



nginxの設定ファイルをいじるという方法でも大丈夫なようです


ブラウザ対応状況

https://caniuse.com/#search=server-sent%20events

2019/2/3現在



Screen Shot 2019-02-03 at 18.16.07.png


IE、Edgeでは使えないようなのでpolifillを使うと良いようです


その他自分が疑問に思った部分


ob_end_flush()flush()の両方を実行しないといけないのはなぜ?

@7of9 さんから情報いただきました、ありがとうございますm(_ _)m


ob_end_flush()ob_flush()はどちらが良い?

sseで使うのにどちらを使ったほうがいいのかはわかりませんでした。

解説ページによって、ob_flush()だったりob_end_flush()だったりしていて、どちらでも動きとしては問題なさそうでした。

情報お持ちの方おりましたらよろしくお願いいたします


参考

最後まで読んでいただいてありがとうございましたm(_ _)m

コメント

このブログの人気の投稿

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2020-12-01 09:41:49 RSSフィード2020-12-01 09:00 分まとめ(69件)