今の今まで知らなかった Server-Sent Events を使ってみた

先日、積読になったまま放置していた技術書に目を通していたら **Server-Sent Events（以下、SSE）** に関する記事を見かけたので、実際にコードを書いて挙動を確認してみました。

**SSE** を使うと、サーバからクライアントへのメッセージプッシュが可能になります。

恥ずかしながら、筆者は SSE について今の今まで知らなかったのですが、「**W3C Working Draft 23 April 2009**」というページを見つけましたので、15年前には登場していたようです。

https://www.w3.org/TR/2009/WD-eventsource-20090423/

SSE の特徴（や欠点）については、[こちらの記事](https://qiita.com/h_tyokinuhata/items/809bdb66f8da6dd53bb6#sse)の SSE の項に、とても分かりやすくまとめてありましたので、一読されることをお勧めします。

https://qiita.com/h_tyokinuhata/items/809bdb66f8da6dd53bb6#sse

## 環境

- Ubuntu 22.04 ※WSL2

- Nodejs 18.18.0
- npm 9.8.1
- express 4.19.2

## ディレクトリ構成

**sse-sample** という作業用ディレクトリを作成して、その中に必要ファイルを置きました。
`npm i express` で Express をインストールしておきます。

```bash
sse-sample
├── index.js
├── node_modules
├── package-lock.json
├── package.json
└── public
    └── index.html
```

## プログラム

### ・サーバ側（index.js）

ポイントとなりそうな箇所にコメントを入れています。

```javascript
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>` タグ内に直接書いています。

```html
<!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度、受信したデータが表示されます。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/759747/d1637e14-67b1-b800-7c72-21e0864eaba8.gif)

ちなみに、サーバ側で  `event:` フィールドを持たせると、クライアント側で `addEventListener()` を使ってイベントを待ち受けることができるようです。

### サーバ側（index.js）

```javascript
const device = { name: 'device-A-1234', value: 50 };
res.write(`event: hoge\n`);
res.write(`data: ${JSON.stringify(device)}\n\n`);
```

### クライアント側（public/index.html）

```javascript
eventSource.addEventListener('hoge', (event) => {
  // ...
});
```

まだ試せていませんが、他には  `id:` でイベントの固有ID（識別子）を指定したり、`retry:` で再接続時間（サーバへの接続が切れると、ブラウザは指定した時間だけ待ってから再接続を試みる）の指定もできるようです。

https://developer.mozilla.org/ja/docs/Web/API/Server-sent_events/Using_server-sent_events#フィールド

手っ取り早く動作確認する場合は、サーバ側プログラムだけ準備して、ターミナルを開いて `curl` コマンドを実行すると良いです。ターミナル上で、受信したメッセージが表示されます。

```bash
curl -H Accept:text/event-stream http://localhost:3000/subscribe
```

## 参考URL

- https://developer.mozilla.org/ja/docs/Web/API/EventSource
- [サーバー送信イベントの使用 - Web API | MDN (mozilla.org)](https://developer.mozilla.org/ja/docs/Web/API/Server-sent_events/Using_server-sent_events)
- [How to use Nodejs for Server-Sent Events (SSE) (q-bit.me)](https://blog.q-bit.me/how-to-use-nodejs-for-server-sent-events-sse/)
- [Server Sent Events (javascript.info)](https://ja.javascript.info/server-sent-events)