Streaming update dengan peristiwa yang dikirim server

Peristiwa yang dikirim server (SSE) mengirim update otomatis ke klien dari server, dengan koneksi HTTP. Setelah koneksi tersambung, server dapat memulai transmisi data.

Sebaiknya gunakan SSE untuk mengirim notifikasi push dari aplikasi web. SSE mengirim informasi dalam satu arah sehingga Anda tidak akan menerima info terbaru dari klien.

Konsep SSE mungkin sudah tidak asing bagi Anda. Aplikasi web "berlangganan" aliran update yang dihasilkan oleh server dan, setiap kali peristiwa baru terjadi, notifikasi akan dikirimkan ke klien. Namun, untuk benar-benar memahami peristiwa yang dikirim server, kita perlu memahami batasan pendahulunya AJAX. Hal ini mencakup:

  • Polling: Aplikasi berulang kali melakukan polling pada server untuk mendapatkan data. Teknik ini digunakan oleh sebagian besar aplikasi AJAX. Dengan protokol HTTP, pengambilan data berkaitan dengan format permintaan dan respons. Klien membuat permintaan dan menunggu server merespons dengan data. Jika tidak ada yang tersedia, respons kosong akan ditampilkan. Polling tambahan menciptakan overhead HTTP yang lebih besar.

  • Polling panjang (Hanging GET / COMET): Jika server tidak memiliki data yang tersedia, server akan menahan permintaan hingga data baru tersedia. Oleh karena itu, teknik ini sering disebut sebagai "GETing Hanging". Saat informasi tersedia, server akan merespons, menutup koneksi, dan proses tersebut diulang. Dengan demikian, server terus merespons dengan data baru. Untuk menyiapkannya, developer biasanya menggunakan peretasan seperti menambahkan tag skrip ke iframe 'limited'.

Acara yang dikirim server telah dirancang dari awal agar efisien. Saat berkomunikasi dengan SSE, server dapat mengirim data ke aplikasi Anda kapan pun diinginkan, tanpa perlu membuat permintaan awal. Dengan kata lain, update dapat di-streaming dari server ke klien saat terjadi. SSE membuka satu saluran searah antara server dan klien.

Perbedaan utama antara peristiwa yang dikirim server dan polling panjang adalah SSE ditangani langsung oleh browser dan pengguna hanya perlu memproses pesan.

Peristiwa yang dikirim server versus WebSockets

Mengapa Anda memilih peristiwa yang dikirim server daripada WebSockets? Pertanyaan bagus.

WebSockets memiliki protokol kaya dengan komunikasi dupleks penuh dua arah. Saluran dua arah lebih cocok untuk game, aplikasi pesan, dan kasus penggunaan apa pun yang memerlukan update hampir real-time di kedua arah.

Namun, terkadang Anda hanya memerlukan komunikasi satu arah dari server. Misalnya, saat teman memperbarui statusnya, kode saham, feed berita, atau mekanisme pengiriman data otomatis lainnya. Dengan kata lain, update pada Database SQL Web sisi klien atau penyimpanan objek IndexedDB. Jika Anda perlu mengirimkan data ke server, XMLHttpRequest selalu menjadi teman Anda.

SSE dikirim melalui HTTP. Tidak ada protokol khusus atau implementasi server untuk mulai bekerja. WebSocket memerlukan koneksi full-duplex dan server WebSocket baru untuk menangani protokol tersebut.

Selain itu, peristiwa yang dikirim server memiliki berbagai fitur yang kurang dari desain WebSockets, termasuk koneksi ulang otomatis, ID peristiwa, dan kemampuan untuk mengirim peristiwa arbitrer.

Membuat EventSource dengan JavaScript

Untuk berlangganan aliran peristiwa, buat objek EventSource dan teruskan URL aliran data Anda:

const source = new EventSource('stream.php');

Selanjutnya, siapkan pengendali untuk peristiwa message. Anda dapat memilih untuk memproses open dan error:

source.addEventListener('message', (e) => {
  console.log(e.data);
});

source.addEventListener('open', (e) => {
  // Connection was opened.
});

source.addEventListener('error', (e) => {
  if (e.readyState == EventSource.CLOSED) {
    // Connection was closed.
  }
});

Saat update dikirim dari server, pengendali onmessage diaktifkan dan data baru tersedia di properti e.data. Bagian ajaibnya adalah setiap kali koneksi ditutup, browser akan otomatis terhubung kembali ke sumber setelah ~3 detik. Implementasi server Anda bahkan dapat memiliki kontrol atas waktu tunggu koneksi ulang ini.

Selesai. Klien Anda sekarang dapat memproses peristiwa dari stream.php.

Format aliran peristiwa

Mengirim aliran peristiwa dari sumber adalah masalah pembuatan respons teks biasa, yang ditayangkan dengan Jenis Konten text/event-stream, yang mengikuti format SSE. Dalam bentuk dasarnya, respons harus berisi baris data:, diikuti dengan pesan Anda, diikuti dengan dua karakter "\n" untuk mengakhiri streaming:

data: My message\n\n

Data multibaris

Jika pesan lebih panjang, Anda dapat memisahkannya menggunakan beberapa baris data:. Dua atau beberapa baris berturut-turut yang dimulai dengan data: diperlakukan sebagai satu bagian data, yang berarti hanya satu peristiwa message yang diaktifkan.

Setiap baris harus diakhiri dengan satu "\n" (kecuali baris terakhir, yang harus diakhiri dengan dua). Hasil yang diteruskan ke pengendali message adalah string tunggal yang digabungkan oleh karakter baris baru. Contoh:

data: first line\n
data: second line\n\n</pre>

Ini menghasilkan "baris pertama\nbaris kedua" di e.data. Kemudian, pengguna dapat menggunakan e.data.split('\n').join('') untuk merekonstruksi karakter pesan sans "\n".

Mengirim data JSON

Menggunakan beberapa baris membantu Anda mengirim JSON tanpa merusak sintaksis:

data: {\n
data: "msg": "hello world",\n
data: "id": 12345\n
data: }\n\n

Dan kemungkinan kode sisi klien untuk menangani streaming tersebut:

source.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  console.log(data.id, data.msg);
});

Mengaitkan ID dengan peristiwa

Anda dapat mengirim ID unik beserta peristiwa streaming dengan menyertakan baris yang dimulai dengan id::

id: 12345\n
data: GOOG\n
data: 556\n\n

Menyetel ID memungkinkan browser melacak peristiwa terakhir yang diaktifkan sehingga jika koneksi ke server terputus, header HTTP khusus (Last-Event-ID) akan ditetapkan dengan permintaan baru. Hal ini memungkinkan browser menentukan peristiwa mana yang sesuai untuk diaktifkan. Peristiwa message berisi properti e.lastEventId.

Mengontrol waktu tunggu koneksi ulang

Browser akan mencoba menghubungkan kembali ke sumber sekitar 3 detik setelah setiap koneksi ditutup. Anda dapat mengubah waktu tunggu tersebut dengan menyertakan baris yang diawali dengan retry:, diikuti dengan jumlah milidetik untuk menunggu sebelum mencoba menghubungkan kembali.

Contoh berikut mencoba menghubungkan kembali setelah 10 detik:

retry: 10000\n
data: hello world\n\n

Menentukan nama peristiwa

Sumber peristiwa tunggal dapat menghasilkan berbagai jenis peristiwa dengan menyertakan nama peristiwa. Jika terdapat baris yang dimulai dengan event:, diikuti dengan nama unik untuk peristiwa tersebut, peristiwa tersebut akan dikaitkan dengan nama tersebut. Di klien, pemroses peristiwa dapat disiapkan untuk memproses peristiwa tertentu tersebut.

Misalnya, output server berikut mengirimkan tiga jenis peristiwa, yaitu peristiwa 'message' umum, 'userlogon', dan 'update':

data: {"msg": "First message"}\n\n
event: userlogon\n
data: {"username": "John123"}\n\n
event: update\n
data: {"username": "John123", "emotion": "happy"}\n\n

Dengan penyiapan pemroses peristiwa pada klien:

source.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  console.log(data.msg);
});

source.addEventListener('userlogon', (e) => {
  const data = JSON.parse(e.data);
  console.log(`User login: ${data.username}`);
});

source.addEventListener('update', (e) => {
  const data = JSON.parse(e.data);
  console.log(`${data.username} is now ${data.emotion}`);
};

Contoh server

Berikut ini implementasi server dasar di PHP:

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache'); // recommended to prevent caching of event data.

/**
* Constructs the SSE data format and flushes that data to the client.
*
* @param string $id Timestamp/id of this connection.
* @param string $msg Line of text that should be transmitted.
**/

function sendMsg($id, $msg) {
  echo "id: $id" . PHP_EOL;
  echo "data: $msg" . PHP_EOL;
  echo PHP_EOL;
  ob_flush();
  flush();
}

$serverTime = time();

sendMsg($serverTime, 'server time: ' . date("h:i:s", time()));
?>

Berikut implementasi yang serupa pada Node JS menggunakan pengendali Express:

app.get('/events', (req, res) => {
    // Send the SSE header.
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    });

    // Sends an event to the client where the data is the current date,
    // then schedules the event to happen again after 5 seconds.
    const sendEvent = () => {
        const data = (new Date()).toLocaleTimeString();
        res.write("data: " + data + '\n\n');
        setTimeout(sendEvent, 5000);
    };

    // Send the initial event immediately.
    sendEvent();
});

sse-node.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <script>
    const source = new EventSource('/events');
    source.onmessage = (e) => {
        const content = document.createElement('div');
        content.textContent = e.data;
        document.body.append(content);
    };
    </script>
  </body>
</html>

Membatalkan aliran data acara

Biasanya, browser akan otomatis terhubung kembali ke sumber peristiwa saat koneksi ditutup, tetapi perilaku tersebut dapat dibatalkan dari klien atau server.

Untuk membatalkan streaming dari klien, panggil:

source.close();

Untuk membatalkan streaming dari server, respons dengan Content-Type non-text/event-stream atau tampilkan status HTTP selain 200 OK (misalnya 404 Not Found).

Kedua metode tersebut mencegah browser membuat kembali koneksi.

Sedikit info tentang keamanan

Permintaan yang dibuat oleh EventSource tunduk pada kebijakan origin yang sama dengan API jaringan lainnya seperti pengambilan. Jika endpoint SSE di server Anda harus dapat diakses dari berbagai origin, baca cara mengaktifkannya dengan Cross Origin Resource Sharing (CORS).