Server-Sent EventsとWebSocketの簡単な実装例

2023/01/15更新

目次

概要

Server-Sent EventsWebSocket は、ともにWeb標準となっているそれぞれ独立したPush技術である。

従来よりウェブでは、まずブラウザなどのクライアント側からサーバーにリクエストが行き、サーバーがそれに応答するという形で通信を行なっていたため、サーバーからクライアントへ任意のタイミングで通信するということができなかった。Push技術とはそれを可能にする新しい技術の総称で、Server-Sent Eventsはサーバーから任意のタイミングでのメッセージ送信を、WebSocketはさらにサーバーとクライアントの間でのリアルタイム性の高い双方向の通信を可能にしてくれる。

いずれも最初に規格が登場したのは2010年前後とかなり昔であるが、しばらくはブラウザの対応状況などからなかなか気軽に利用できるものではなかった。しかし、近年では古いブラウザの廃止によりそのような懸念も払拭され、ごく簡単にサーバーPushや双方向通信が誰でも実装できるようになっている。

Server-Sent Events

Server-Sent Events(サーバー送信イベント)は、EventSource APIとしてHTML5で標準化されたサーバーPush技術の一つである。従来のHTTPの接続を用いて、サーバーPushを可能にする。一方で、EventSourceによる接続ではクライアント側からサーバーへの送信はできないので、双方向通信ではない。

クライアント側の実装例

クライアント側では、EventSourceオブジェクトを通して接続を開始し、messageイベントでサーバーからのメッセージを受信できる。また、独自のイベント名で受信することもできる。

EventSourceは、接続が切断されるとerrorイベントが発生するが、自動的に再接続を試みて、再び接続に成功すればopenイベントが発生する。明示的に切断するには、close()を呼ぶ必要がある。

ここでは単なる文字列を受信することを想定しているが、JSONなどにシリアライズすれば複数の情報を一度に受信することもできるので便利である。

// Server-Sent Eventsを実装したサーバーに接続する。
const sse = new EventSource('http://localhost:3000/');

// 接続成功時に発生するイベント
sse.addEventListener('open', (e) => {
    console.log('接続しました。');
);

// メッセージ受信時に発生するイベント
sse.addEventListener('message', (e) => {
    console.log('メッセージを受信しました。');
    // e.dataで受信した文字列を取得できる
    console.log(e.data);
});

// 独自の名前付きメッセージ受信時に発生するイベント
sse.addEventListener('myOriginalMessage', (e) => {
    console.log('myOriginalMessageを受信しました。');
    // e.dataで受信した文字列を取得できる
    console.log(e.data);
});

// 接続失敗時に発生するイベント
sse.addEventListener('error', (e) => {
    console.log('接続できません。');
});

// 切断するにはclose()を呼び出す
document.getElementById('close').addEventListener('click', () => {
    sse.close();
    console.log('切断しました。');
});

Node.jsによるサーバー側の実装例

Server-Sent Eventのサーバー側の実装例として、Node.jsでの例を以下に示す。

サーバー側は、Content-Type: text/event-streamでレスポンスを返す必要があり、「data: ~」と空行を出力することで、任意のタイミングでクライアントにメッセージを送信できる。また、「data: ~」の直前に「event: ~」を出力することで、クライアント側の任意の名前のイベントを発火させることもできる。

const http = require('http');
const server = http.createServer(function(req, res) {

    // 接続を受けたら「Content-Type: text/event-stream」で応答する
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Connection': 'keep-alive',
        'Cache-Control': 'no-cache'
    });

    // 3秒ごとにメッセージを送信する
    const timer1 = setInterval(function() {
        // 「data: 」に続けて文字列と空行を出力すると、クライアント側でmessageイベントが発生する
        res.write('data: サーバーからのメッセージです。\n\n');
    }, 3000);

    // 5秒ごとに名前付きメッセージを送信する
    const timer2 = setInterval(function() {
        // 「data: 」の前に「event: 」でイベント名を出力すると、クライアント側でその名前のイベントが発生する
        res.write('event: myOriginalMessage\n');
        res.write('data: サーバーからの名前付きメッセージです。\n\n');
    }, 5000);

    // 接続が切断されたら終了する
    req.connection.addListener("close", function () {
        clearInterval(timer1);
        clearInterval(timer2);
    }, false);

});
server.listen(8081);

CGIによるサーバー側の実装例

Server-Sent Eventsは、従来のHTTPの仕組みの上に成り立っているため、特殊な環境が必要無く、例えば昔ながらのApacheサーバー上のCGIでも実装しようと思えばできる。以下は、PerlによるCGIで実装した例である。

ただ、Perlはイベント駆動ではないので、「他の場所で何かが発生したらこの人にメッセージを送信」という実装は難しそうである。

#!/usr/bin/perl

# Apacheのバッファリングをすり抜けるため、NPHスクリプト(ファイル名をnph-xxx.cgi)にする

# Perlのバッファリングを無効にする
$| = 1;

# NPHスクリプトなので、ステータスコードから書き出す
print "HTTP/1.1 200 OK\n";
print "Content-type: text/event-stream\n";
print "Cache-Control: no-cache\n\n";

# 10秒ごとにメッセージを送信
while (1) {
    print "data: CGIからのメッセージです。\n\n"; 
    sleep 10;
}

WebSocket

WebSocketは、Web標準となっている新しいプロトコルで、単一のTCPコネクションで継続した双方向通信を可能にする。HTTPとは異なるプロトコルなので、URLではws://(セキュアな場合はwss://)というスキームで接続する。

Server-Sent Eventsと異なり、双方向通信なので、クライアント側からサーバーへの送信もWebSocket上で行なうことができる。Server-Sent Eventsよりも後発であるため、全般的にServer-Sent Eventsよりも使い勝手が良い。最近であればよほど古いブラウザの考慮が必要などでなければ、WebSocketの方がモダンかつ便利と思われる。

クライアント側の実装例

クライアント側では、WebSocketオブジェクトで通して接続を開始し、messageイベントでサーバーからのメッセージを受信できる。この辺りの流れはServer-Sent Eventsとほぼ同じである。WebSocketは、ウィンドウやタブが非アクティブになった場合などに切断されやすいようで、再接続の処理は自前で用意しておいた方がよい。サーバーへメッセージを送信する場合は、単にsend()を呼ぶだけで送信できる。

Server-Sent Eventsの場合と同様に、実際には単なる文字列を送受信するのではなく、JSONにシリアライズしたオブジェクトを送受信する形が一般的である。また、WebSocketの場合は文字列以外にもArrayBufferBlobなどのバイナリデータも送信できる。

// 現在の接続
let connection = null;

// WebSocketで接続
const connect = function() {
    const ws = new WebSocket('ws://localhost:8081/');

    // 接続成功時に発生するイベント
    ws.addEventListener('open', (e) => {
        connection = ws;
        console.log('接続しました。');
    });

    // メッセージ受信時に発生するイベント
    ws.addEventListener('message', (e) => {
        console.log('メッセージを受信しました。');
        // e.dataで受信した文字列を取得できる
        console.log(e.data);
    });

    // 接続切断時に発生するイベント
    ws.addEventListener('close', (e) => {
        connection = null;
        console.log('切断されました。');
    });
};

// 接続する
connect();

// 接続が維持されているか定期的に確認する
setInterval(() => {
    if (connection) return;

    // 接続が切断されていれば再接続を試みる
    console.log('再接続しています…');
    connect();

}, 5000);

// サーバー側にメッセージを送信する
document.getElementById('send').addEventListener('click', () => {
    if (!connection) {
        console.log('接続されていないため、送信できません。');
        return;
    }

    // send()でメッセージを送信できる。
    connection.send('メッセージです。');
});

Node.jsによるサーバー側の実装例

WebSocketのサーバー側の実装例として、Node.jsでの例を以下に示す。

Node.jsではwsというモジュールを使ってWebSocketサーバーが簡単に実装できる。以下では、複数のクライアントからの接続を同時に受け付けて、メッセージを受け取ったら他の接続中のクライアントに送信している。

const WebSocket = require('ws');

// WebSocketサーバーの開始
const ws = new WebSocket.Server({ port: 8081 });

// 接続中のクライアント
const clients = {};
let clientId = 0;

// 接続を受けたとき
ws.on('connection', function(client) {

    // クライアントにIDを付与して、接続中クライアントとして保持する
    const myId = clientId++;
    clients[myId] = client;

    // クライアントからメッセージが送られてきた場合
    client.on('message', function(msg) {
        // 接続中のクライアントに送信
        for (const id in clients) {
            // 自分自身は除く
            if (id == myId) continue;
            clients[id].send(String(msg));
        }
    });

    // 接続が切断した場合
    client.on('close', function() {
        // 接続中クライアントから削除する
        delete clients[myId];
    });

});

外部リンク

ここまで紹介したのはごく初歩的な実装例であった。ローカルや閉じた環境など、セキュリティや接続数をそれほど気にしなくてもよい環境で使用するにはこの程度でまずは十分と思われるが、現実の本番環境で利用する場合には、より詳細な仕様や注意すべき点など、その他の記事を参照されたい。