Streaming update dengan peristiwa yang dikirim server

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

Anda dapat menggunakan SSE untuk mengirim notifikasi push dari aplikasi web Anda. SSE mengirimkan informasi dalam satu arah, sehingga Anda tidak akan menerima pembaruan dari dengan klien besar.

Konsep SSE mungkin sudah tidak asing lagi. Aplikasi web "berlangganan" ke aliran pembaruan yang dihasilkan oleh server dan, setiap kali peristiwa baru terjadi, notifikasi dikirim ke klien. Tapi untuk benar-benar memahami peristiwa yang dikirim server, kita perlu memahami keterbatasan pendahulu AJAX. Hal ini mencakup:

  • Polling: Aplikasi berulang kali polling data di server. 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 untuk merespons dengan data. Jika tidak ada yang tersedia, kolom kosong ditampilkan. Polling tambahan menghasilkan overhead HTTP yang lebih besar.

  • Polling panjang (Hanging GET / COMET): Jika server tidak memiliki data tersedia, server menahan permintaan hingga data baru tersedia. Oleh karena itu, teknik ini sering disebut sebagai "Hanging GET". Kapan informasi menjadi tersedia, server merespons, menutup koneksi, dan prosesnya diulang. Jadi, server terus-menerus merespons dengan data baru. Untuk menyiapkannya, developer biasanya menggunakan peretasan seperti menambahkan tag skrip menjadi 'tak terbatas' iframe.

Peristiwa yang dikirim server, telah dirancang dari awal agar menjadi efisien. Saat berkomunikasi dengan SSE, server dapat mengirimkan data ke kapan saja, tanpa perlu membuat permintaan awal. Dengan kata lain, {i>SUMIF<i} memiliki daftar sel pembaruan dapat di-{i>streaming <i}dari server ke klien saat terjadi. SSE membuka saluran searah tunggal antara server dan klien.

Perbedaan utama antara peristiwa yang dikirim server dan polling panjang adalah bahwa SSE ditangani langsung oleh {i>browser<i} dan pengguna hanya perlu mendengarkan pesan.

Peristiwa yang dikirim server versus WebSockets

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

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

Namun, terkadang Anda hanya memerlukan komunikasi satu arah dari server. Misalnya, saat teman memperbarui status, saham, umpan berita, atau mekanisme push data otomatis lainnya. Dengan kata lain, {i>SUMIF<i} memiliki daftar sel pembaruan pada Database SQL Web sisi klien atau penyimpanan objek IndexedDB. Jika Anda perlu mengirim data ke server, XMLHttpRequest akan selalu menjadi teman.

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

Selain itu, peristiwa yang dikirim server memiliki berbagai fitur yang tidak dimiliki WebSockets sesuai dengan desainnya, termasuk sambungan kembali otomatis, ID peristiwa, dan kemampuan untuk 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. Anda dapat memilih dengarkan 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 didorong dari server, pengendali onmessage diaktifkan dan data baru akan tersedia di properti e.data. Bagian ajaibnya adalah setiap kali koneksi ditutup, {i>browser<i} secara otomatis terhubung kembali ke sumber setelah ~3 detik. Implementasi server Anda bahkan memiliki kontrol atas waktu tunggu koneksi kembali ini.

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

Format streaming peristiwa

Mengirim aliran peristiwa dari sumber adalah masalah menyusun respons teks biasa, yang disajikan dengan Content-Type text/event-stream, yang mengikuti format SSE. Dalam bentuk dasarnya, respons harus berisi baris data:, diikuti oleh elemen pesan, diikuti dengan dua "\n" untuk mengakhiri streaming:

data: My message\n\n

Data multibaris

Jika pesan lebih panjang, Anda dapat memisahkannya menggunakan beberapa baris data:. Dua atau lebih 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 yang terakhir, yang harus berakhir dengan dua). Hasil yang diteruskan ke pengendali message Anda 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 seseorang dapat menggunakan e.data.split('\n').join('') untuk merekonstruksi pesan sans "\n" karakter.

Mengirim data JSON

Menggunakan beberapa baris membantu Anda mengirim JSON tanpa melanggar 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 bersama acara streaming dengan menyertakan baris yang dimulai dengan id::

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

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

Mengontrol waktu tunggu koneksi ulang

Browser 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 menunggu sebelum mencoba menghubungkan kembali.

Contoh berikut mencoba menghubungkan kembali setelah 10 detik:

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

Menentukan 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 tersebut akan dikaitkan dengan nama tersebut. Di klien, pemroses peristiwa dapat disiapkan untuk memproses peristiwa tertentu tersebut.

Misalnya, {i>output<i} server berikut ini mengirimkan tiga jenis kejadian, 'pesan' umum {i>event<i}, '{i>userlogon<i}', dan '{i>update<i}' acara:

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 adalah penerapan 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 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 streaming acara

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

Untuk membatalkan streaming dari klien, panggil:

source.close();

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

Kedua metode tersebut mencegah browser membuat koneksi kembali.

Sekilas tentang keamanan

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