Streaming update dengan peristiwa yang dikirim server

Peristiwa yang dikirim server (SSE) mengirimkan pembaruan otomatis ke klien dari server, dengan koneksi HTTP. Setelah koneksi dibuat, server dapat memulai transmisi data.

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

Konsep SSE mungkin sudah tidak asing lagi. Aplikasi web "berlangganan" ke aliran update yang dihasilkan oleh server dan, setiap kali peristiwa baru terjadi, notifikasi akan dikirim ke klien. Namun, untuk benar-benar memahami peristiwa yang dikirim server, kita perlu memahami keterbatasan 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 berkisar pada format permintaan dan respons. Klien membuat permintaan dan menunggu server merespons dengan data. Jika tidak ada yang tersedia, respons kosong akan ditampilkan. Polling tambahan akan menghasilkan overhead HTTP yang lebih besar.

  • Long polling (Hanging GET / COMET): Jika server tidak memiliki data yang tersedia, server akan menahan permintaan terbuka hingga data baru tersedia. Oleh karena itu, teknik ini sering disebut sebagai "Hanging GET". Saat informasi tersedia, server akan merespons, menutup koneksi, dan prosesnya diulang. Dengan demikian, server terus merespons dengan data baru. Untuk menyiapkannya, developer biasanya menggunakan hack seperti menambahkan tag skrip ke iframe 'tak terbatas'.

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

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

Peristiwa yang dikirim server versus WebSocket

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

WebSockets memiliki protokol yang kaya dengan komunikasi full-duplex dua arah. Saluran dua arah lebih baik untuk game, aplikasi pesan, dan kasus penggunaan apa pun yang memerlukan pembaruan mendekati real-time di kedua arah.

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

SSE dikirim melalui HTTP. Tidak ada protokol atau penerapan server khusus yang perlu dilakukan. WebSocket memerlukan koneksi full-duplex dan server WebSocket baru untuk menangani protokol.

Selain itu, peristiwa yang dikirim server memiliki berbagai fitur yang tidak dimiliki WebSockets karena desainnya, termasuk penyambungan ulang otomatis, ID peristiwa, dan kemampuan untuk mengirim peristiwa arbitrer.

Membuat EventSource dengan JavaScript

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

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

Selanjutnya, siapkan pengendali untuk peristiwa message. Secara opsional, Anda dapat 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 akan diaktifkan dan data baru akan tersedia di properti e.data-nya. Bagian yang ajaib 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 penyambungan ulang ini.

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

Format aliran peristiwa

Mengirim aliran peristiwa dari sumber adalah masalah dalam membuat respons teks biasa, yang ditayangkan dengan Content-Type 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 Anda lebih panjang, Anda dapat membaginya menggunakan beberapa baris data:. Dua baris atau lebih 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 Anda adalah satu string yang digabungkan dengan karakter baris baru. Contoh:

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

Tindakan ini akan menghasilkan "baris pertama\nbaris kedua" di e.data. Selanjutnya, seseorang dapat menggunakan e.data.split('\n').join('') untuk merekonstruksi pesan tanpa karakter "\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 aliran data 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 dengan peristiwa aliran data dengan menyertakan baris yang dimulai dengan id::

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

Menetapkan 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 penyambungan ulang

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

Contoh berikut mencoba menghubungkan kembali setelah 10 detik:

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

Tentukan nama peristiwa

Satu sumber peristiwa dapat menghasilkan berbagai jenis peristiwa dengan menyertakan nama peristiwa. Jika ada baris yang diawali dengan event:, diikuti dengan nama unik untuk peristiwa, peristiwa 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' generik, '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 di 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 adalah 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 adalah implementasi serupa di 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 streaming peristiwa

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 bukan text/event-stream atau tampilkan status HTTP selain 200 OK (seperti 404 Not Found).

Kedua metode ini mencegah browser membuat ulang koneksi.

Sekilas tentang keamanan

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