先日、積読になったまま放置していた技術書に目を通していたら Server-Sent Events(以下、SSE) に関する記事を見かけたので、実際にコードを書いて挙動を確認してみました。
SSE を使うと、サーバからクライアントへのメッセージプッシュが可能になります。
恥ずかしながら、筆者は SSE について今の今まで知らなかったのですが、「W3C Working Draft 23 April 2009」というページを見つけましたので、15年前には登場していたようです。
SSE の特徴(や欠点)については、こちらの記事の SSE の項に、とても分かりやすくまとめてありましたので、一読されることをお勧めします。
環境
-
Ubuntu 22.04 ※WSL2
-
Nodejs 18.18.0
-
npm 9.8.1
-
express 4.19.2
ディレクトリ構成
sse-sample という作業用ディレクトリを作成して、その中に必要ファイルを置きました。
npm i express
で Express をインストールしておきます。
sse-sample
├── index.js
├── node_modules
├── package-lock.json
├── package.json
└── public
└── index.html
プログラム
・サーバ側(index.js)
ポイントとなりそうな箇所にコメントを入れています。
const express = require('express');
const app = express();
app.use(express.static('public'));
app.get('/subscribe', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream', // SSE を使うために `text/event-stream` を指定する
'Connection': 'keep-alive', // 接続を開いたままにする
'Cache-Control': 'no-cache' // キャッシュ処理をしない
});
const intervalId = setInterval(() => {
// 送信したいメッセージ(データ)は「data:」の後に書く
// 「\n\n」がメッセージの区切りになる
// 今回はデバイスから何らかの値(計測値)をプッシュしているイメージ
const device = { name: 'device-A-1234', value: Math.floor(Math.random() * 100) };
res.write(`data: ${JSON.stringify(device)}\n\n`);
}, 3000);
req.on('close', () => {
clearInterval(intervalId);
res.end();
});
});
app.listen(3000, () => console.log('Server listening: http://localhost:3000'));
・クライアント側(public/index.html)
ポイントとなりそうな箇所にはコメントを入れています。
※今回は、HTML ファイルの <script>
タグ内に直接書いています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Server-Sent Events Sample</title>
</head>
<body>
<h1>SSE Sample</h1>
<ul id="list"></ul>
<script>
// SSEを実装したサーバに接続する
const eventSource = new EventSource('/subscribe');
// open/error/message が既定イベント
eventSource.onopen = () => {
// 接続の確立
};
eventSource.onerror = () => {
// 接続の切断、など
};
eventSource.onmessage = (event) => {
// サーバからのメッセージを受信(データは event.data にある)
const eventList = document.getElementById('list');
const newElement = document.createElement('li');
const parsedData = JSON.parse(event.data);
newElement.textContent = `${parsedData.name} / ${parsedData.value}`;
eventList.appendChild(newElement);
};
</script>
</body>
</html>
動作確認
node index.js
を実行した後、ブラウザで http://localhost:3000
にアクセスしてみます。
サーバ側では3秒に1度 data: ... \n\n
でメッセージを送っているため、クライアント側では3秒に1度、受信したデータが表示されます。
ちなみに、サーバ側で event:
フィールドを持たせると、クライアント側で addEventListener()
を使ってイベントを待ち受けることができるようです。
サーバ側(index.js)
const device = { name: 'device-A-1234', value: 50 };
res.write(`event: hoge\n`);
res.write(`data: ${JSON.stringify(device)}\n\n`);
クライアント側(public/index.html)
eventSource.addEventListener('hoge', (event) => {
// ...
});
まだ試せていませんが、他には id:
でイベントの固有ID(識別子)を指定したり、retry:
で再接続時間(サーバへの接続が切れると、ブラウザは指定した時間だけ待ってから再接続を試みる)の指定もできるようです。
手っ取り早く動作確認する場合は、サーバ側プログラムだけ準備して、ターミナルを開いて curl
コマンドを実行すると良いです。ターミナル上で、受信したメッセージが表示されます。
curl -H Accept:text/event-stream http://localhost:3000/subscribe