Sunucu tarafından gönderilen etkinliklerle güncelleme akışı

Sunucu tarafından gönderilen etkinlikler (SSE'ler), HTTP bağlantısıyla istemciye bir sunucudan otomatik güncellemeler gönderir. Bağlantı kurulduktan sonra, sunucular veri iletimini başlatabilir.

Web uygulamanızdan push bildirimleri göndermek için SSE'leri kullanmak isteyebilirsiniz. SSE'ler bilgileri tek bir yönde gönderir, bu nedenle istemciden güncelleme almazsınız.

SSE kavramı size tanıdık gelebilir. Bir web uygulaması, bir sunucu tarafından oluşturulan güncelleme akışına "abone olur" ve her yeni etkinlik gerçekleştiğinde istemciye bildirim gönderilir. Ancak sunucu tarafından gönderilen etkinlikleri gerçekten anlamak için AJAX'tan öncekilerin sınırlamalarını anlamamız gerekir. Bu hizmet ve özellikler arasında aşağıdakiler bulunmaktadır:

  • Yoklama: Uygulama, veriler için sunucuyu sürekli olarak yoklar. Bu teknik, çoğu AJAX uygulaması tarafından kullanılır. HTTP protokolünde, veri getirme işlemi bir istek ve yanıt biçimi etrafında yapılır. İstemci bir istekte bulunur ve sunucunun verilerle yanıt vermesini bekler. Hiçbiri yoksa boş bir yanıt döndürülür. Fazladan yoklama, daha fazla HTTP ek yüküne neden olur.

  • Uzun yoklama (Asılı GET / COMET): Sunucuda kullanılabilir veri yoksa sunucu, yeni veriler kullanılabilir hale gelene kadar isteği açık tutar. Dolayısıyla, bu teknik genellikle "Asılı GET" olarak adlandırılır. Bilgiler kullanılabilir hale geldiğinde sunucu yanıt verir, bağlantıyı kapatır ve işlem tekrar eder. Bu nedenle, sunucu sürekli olarak yeni verilerle yanıt verir. Bunu ayarlamak için, geliştiriciler genellikle "sonsuz" iframe'e komut dosyası etiketleri eklemek gibi saldırıları kullanırlar.

Sunucu tarafından gönderilen etkinlikler tepeden tırnağa verimli olacak şekilde tasarlanmıştır. Sunucu, SSE'lerle iletişim kurarken ilk istek yapmaya gerek kalmadan istediği zaman uygulamanıza veri aktarabilir. Başka bir deyişle, güncellemeler yapıldıkları anda sunucudan istemciye akışla alınabilir. SSE'ler, sunucu ile istemci arasında tek bir tek yönlü kanal açar.

Sunucu tarafından gönderilen etkinlikler ile uzun yoklama arasındaki temel fark, SSE'lerin doğrudan tarayıcı tarafından işlenmesi ve kullanıcının yalnızca mesajları dinlemesidir.

Sunucu tarafından gönderilen etkinlikler ile WebSocket'lerin karşılaştırması

Neden WebSockets yerine sunucu tarafından gönderilen etkinlikleri seçersiniz? Güzel soru.

WebSockets, çift yönlü, tam çift yönlü iletişimle zengin bir protokole sahiptir. İki yönlü bir kanal; oyunlar, mesajlaşma uygulamaları ve her iki yönde de neredeyse gerçek zamanlı güncellemelere ihtiyaç duyduğunuz tüm kullanım alanları için daha uygundur.

Ancak, bazen bir sunucudan yalnızca tek yönlü iletişime ihtiyacınız olur. Örneğin, bir arkadaşınız durumunu, borsa takip ekranını, haber feed'lerini veya diğer otomatik veri aktarma mekanizmalarını güncellediğinde. Başka bir deyişle, istemci tarafı Web SQL Veritabanı veya IndexedDB nesne deposunda yapılan bir güncelleme. Bir sunucuya veri göndermeniz gerektiğinde, XMLHttpRequest her zaman arkadaşınızdır.

SSE'ler HTTP üzerinden gönderilir. Çalışmaya başlamak için özel bir protokol veya sunucu uygulaması yoktur. WebSocket'lerin protokolü işlemesi için tam çift yönlü bağlantılar ve yeni WebSocket sunucuları gerekir.

Buna ek olarak, sunucu tarafından gönderilen etkinliklerde WebSockets'in tasarımı gereği bulunmayan çeşitli özellikler (ör. otomatik yeniden bağlanma, etkinlik kimlikleri ve rastgele etkinlikler gönderme olanağı) bulunur.

JavaScript ile EventSource oluşturma

Bir etkinlik akışına abone olmak için EventSource nesnesi oluşturun ve bu nesneye akışınızın URL'sini iletin:

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

Ardından, message etkinliği için bir işleyici ayarlayın. İsteğe bağlı olarak open ve error öğesini dinleyebilirsiniz:

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.
  }
});

Güncellemeler sunucudan aktarıldığında onmessage işleyici etkinleşir ve yeni veriler e.data özelliğinde kullanılabilir olur. İşin sihirli kısmı, bağlantı her kapatıldığında tarayıcının yaklaşık 3 saniye sonra kaynağa otomatik olarak yeniden bağlanmasıdır. Sunucu uygulamanız, bu yeniden bağlantı zaman aşımını bile kontrol edebilir.

Bu kadar basit. Müşteriniz artık stream.php etkinliklerini işleyebilir.

Etkinlik akışı biçimi

Kaynaktan bir etkinlik akışı göndermek, SSE biçimine uyan text/event-stream İçerik Türü ile sunulan bir düz metin yanıtı oluşturmaktır. Temel biçiminde yanıt, bir data: satırı, mesajınızı ve ardından da akışı sonlandırmak için iki "\n" karakteri içermelidir:

data: My message\n\n

Çok satırlı veriler

İletiniz daha uzunsa birden fazla data: satırı kullanarak bölebilirsiniz. data: ile başlayan iki veya daha fazla ardışık satır tek bir veri parçası olarak değerlendirilir, yani yalnızca bir message etkinliği tetiklenir.

Her satır tek bir "\n" ile bitmelidir (sonuncusu hariç, iki ile bitmelidir). message işleyicinize iletilen sonuç, yeni satır karakterleri ile birleştirilen tek bir dizedir. Örneğin:

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

Bu, e.data içinde "ilk satır\nikinci satır" oluşturur. Bu durumda "\n" karakterlerini içeren mesajı yeniden oluşturmak için e.data.split('\n').join('') kullanılabilir.

JSON verilerini gönder

Birden çok satır kullanmak, söz dizimini bozmadan JSON göndermenize yardımcı olur:

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

Ve bu akışı işlemek için kullanabileceği olası istemci tarafı kodu:

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

Kimliği bir etkinlikle ilişkilendirme

id: ile başlayan bir satır ekleyerek akış etkinliğiyle benzersiz bir kimlik gönderebilirsiniz:

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

Kimlik ayarlamak, tarayıcının tetiklenen son etkinliği takip etmesine olanak tanır. Böylece, sunucu bağlantısı kesilirse yeni istekle birlikte özel bir HTTP üstbilgisi (Last-Event-ID) ayarlanır. Bu, tarayıcının hangi etkinliğin tetiklenmeye uygun olduğunu belirlemesine olanak tanır. message etkinliği e.lastEventId özelliği içeriyor.

Yeniden bağlantı zaman aşımını kontrol etme

Tarayıcı, her bağlantı kapatıldıktan yaklaşık 3 saniye sonra kaynağa yeniden bağlanmayı dener. retry: ile başlayan bir satır ve ardından yeniden bağlanmayı denemeden önce beklenecek milisaniye sayısını ekleyerek bu zaman aşımını değiştirebilirsiniz.

Aşağıdaki örnekte, 10 saniye sonra yeniden bağlanma deneniyor:

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

Bir etkinlik adı belirtin

Tek bir etkinlik kaynağı, etkinlik adı ekleyerek farklı türde etkinlikler oluşturabilir. event: ile başlayan bir satır varsa ve ardından benzersiz bir etkinlik adı geliyorsa etkinlik bu adla ilişkilendirilir. İstemcide, söz konusu etkinliği dinleyecek bir etkinlik işleyici ayarlanabilir.

Örneğin, aşağıdaki sunucu çıkışı üç tür etkinlik gönderir: genel bir "message" etkinliği, "userlogon" ve "update" etkinliği:

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

İstemcide etkinlik işleyicileri ayarlandığında:

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}`);
};

Sunucu örnekleri

Aşağıda, PHP'deki temel sunucu uygulaması gösterilmektedir:

<?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()));
?>

Express işleyicisi kullanılarak yapılan benzer bir Node JS uygulaması aşağıda verilmiştir:

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>

Etkinlik akışını iptal etme

Normalde, bağlantı kapatıldığında tarayıcı etkinlik kaynağına otomatik olarak yeniden bağlanır, ancak bu davranış istemci veya sunucu tarafından iptal edilebilir.

İstemciden gelen canlı yayını iptal etmek için şu numarayı arayın:

source.close();

Sunucudan gelen bir akışı iptal etmek için text/event-stream Content-Type dışında bir HTTP durumu döndürün veya 200 OK dışında bir HTTP durumu (404 Not Found gibi) döndürün.

Her iki yöntem de tarayıcının bağlantıyı yeniden kurmasını engeller.

Güvenlikle ilgili söz

EventSource tarafından oluşturulan istekler, getirme gibi diğer ağ API'leriyle aynı kaynak politikalarına tabidir. Sunucunuzdaki SSE uç noktasına farklı kaynaklardan erişilmesi gerekiyorsa Kaynaklar Arası Kaynak Paylaşımı (CORS) ile nasıl etkinleştirileceğini öğrenin.