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

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

Web uygulamanızdan push bildirimleri göndermek için SSE'leri kullanabilirsiniz. SSE'ler bilgileri tek yönlü gönderir. Bu nedenle istemciden güncelleme almazsınız.

SSE kavramı size tanıdık gelebilir. Bir web uygulaması, sunucu tarafından oluşturulan bir güncelleme akışına "abone olur" ve yeni bir etkinlik gerçekleştiğinde istemciye bildirim gönderilir. Ancak sunucu tarafından gönderilen etkinlikleri gerçekten anlamak için AJAX'ın önceki sürümlerinin sınırlamalarını anlamamız gerekir. Bunlardan bazıları:

  • Yoklama: Uygulama, veri için bir sunucuyu tekrar tekrar yoklar. Bu teknik, AJAX uygulamalarının çoğu tarafından kullanılır. HTTP protokolünde, veri getirme işlemi istek ve yanıt biçiminde gerçekleşir. İstemci bir istek gönderir ve sunucunun veri ile yanıt vermesini bekler. Hiçbiri mevcut değilse boş bir yanıt döndürülür. Ek anket, daha fazla HTTP yükü oluşturur.

  • Uzun süreli sorgu (askıda GET / COMET): Sunucunun veri yoksa sunucu, yeni veriler sağlanana kadar isteği açık tutar. Bu nedenle, bu tekniğe genellikle "askıda GET" denir. Bilgiler kullanılabilir hale geldiğinde sunucu yanıt verir, bağlantıyı kapatır ve süreç tekrarlanır. Bu nedenle sunucu sürekli olarak yeni verilerle yanıt verir. Geliştiriciler bunu ayarlamak için genellikle "sonsuz" bir iFrame'e komut dosyası etiketleri ekleme gibi hilelere başvurur.

Sunucu tarafından gönderilen etkinlikler, sıfırdan verimli olacak şekilde tasarlanmıştır. Bir sunucu, SSE'lerle iletişim kurarken ilk istek gönderme gerekmeden istediği zaman uygulamanıza veri gönderebilir. Başka bir deyişle, güncellemeler gerçekleştikçe sunucudan istemciye aktarılabilir. SSE'ler, sunucu ile istemci arasında tek yönlü bir kanal açar.

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

Sunucu tarafından gönderilen etkinlikler ve WebSocket'ler

WebSocket'ler yerine neden sunucu tarafından gönderilen etkinlikleri tercih edersiniz? Güzel soru.

WebSockets, iki yönlü, tam çift yönlü iletişime sahip zengin bir protokole sahiptir. İki yönlü kanallar 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 iyidir.

Ancak bazen bir sunucudan yalnızca tek yönlü iletişime ihtiyacınız vardır. Örneğin, bir arkadaşınız durumunu güncellediğinde, hisse senedi akışları, haber feed'leri veya diğer otomatik veri yayınlama mekanizmaları. Diğer bir deyişle, istemci tarafında Web SQL veritabanı veya IndexedDB nesne deposunda yapılan bir güncelleme. Bir sunucuya veri göndermeniz gerekiyorsa XMLHttpRequest her zaman yardımcı olur.

SSE'ler HTTP üzerinden gönderilir. Özel bir protokol veya sunucu uygulaması gerekmez. WebSocket'ler, protokolü işleyebilmek için tam çift yönlü bağlantılar ve yeni WebSocket sunucuları gerektirir.

Ayrıca, sunucu tarafından gönderilen etkinlikler; otomatik yeniden bağlantı, etkinlik kimlikleri ve rastgele etkinlikler gönderme gibi WebSocket'lerin tasarım gereği sahip olmadığı çeşitli özelliklere sahiptir.

JavaScript ile EventSource oluşturma

Bir etkinlik akışına abone olmak için bir 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. İsterseniz open ve error için 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 itildiğinde onmessage işleyicisi tetiklenir ve e.data mülkünde yeni veriler kullanılabilir hale gelir. En iyi kısmı ise 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ı üzerinde kontrol sahibi olabilir.

Bu kadar basit. Müşteriniz artık stream.php'ten gelen etkinlikleri işleyebilir.

Etkinlik akışı biçimi

Kaynaktan etkinlik akışı göndermek için SSE biçimini izleyen bir text/event-stream Content-Type ile sunulan düz metin yanıtı oluşturmanız gerekir. Temel biçiminde yanıt, bir data: satırı, ardından mesajınız ve ardından akışı sonlandırmak için iki "\n" karakteri içermelidir:

data: My message\n\n

Çok satırlı veriler

Mesajınız daha uzunsa birden fazla data: satırı kullanarak mesajı bölebilirsiniz. data: ile başlayan iki veya daha fazla art arda 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 (son satır iki tane ile bitmelidir). message işleyicinize iletilen sonuç, yeni satır karakterleriyle birleştirilmiş tek bir dizedir. Örneğin:

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

Bu işlem, e.data içinde "first line\nsecond line" ifadesini oluşturur. Ardından, "\n" karakterleri olmadan mesajı yeniden oluşturmak için e.data.split('\n').join('') kullanılabilir.

JSON verileri gönderme

Birden fazla 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

Aşağıda, bu akışı işleyebilecek istemci tarafı kod da verilmiştir:

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

Bir kimliği etkinlikle ilişkilendirme

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

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

Bir kimlik ayarlamak, tarayıcının son etkinliği takip etmesini sağlar. Böylece, sunucuyla bağlantı kesilirse yeni istekle özel bir HTTP başlığı (Last-Event-ID) ayarlanır. Bu sayede tarayıcı, hangi etkinliğin tetiklenmesinin uygun olduğunu belirleyebilir. message etkinliği bir e.lastEventId mülkü 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ğlanmaya çalışır. Bu zaman aşım süresini, retry: ile başlayan bir satır ve ardından yeniden bağlanmayı denemeden önce beklemeniz gereken milisaniye sayısını ekleyerek değiştirebilirsiniz.

Aşağıdaki örnekte, 10 saniye sonra yeniden bağlantı kurulmaya çalışılmaktadır:

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

Etkinlik adı belirtin

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

Örneğin, aşağıdaki sunucu çıkışı üç tür etkinlik gönderir: genel bir "mesaj" etkinliği, "kullanıcıgirişi" ve "güncelleme" 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

Müşteride 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

PHP'de temel bir sunucu uygulaması aşağıda verilmiştir:

<?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 kullanan Node JS'de benzer bir uygulamayı aşağıda bulabilirsiniz:

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 yayınını iptal etme

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

İstemciden bir aktarımı iptal etmek için şu işlevi çağırın:

source.close();

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

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

Güvenlik hakkında

EventSource tarafından oluşturulan istekler, fetch gibi diğer ağ API'leriyle aynı kaynak politikalarına tabidir. Sunucunuzdaki SSE uç noktasının farklı kaynaklardan erişilebilir olması gerekiyorsa merkezler arası kaynak paylaşımı (CORS) ile nasıl etkinleştirileceğini okuyun.