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

Sunucu tarafından gönderilen etkinlikler (SSE'ler), HTTP bağlantısı olan 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 öncüllerinin sınırlamalarını anlamamız gerekiyor. Bunlardan bazıları:

  • Yoklama: Uygulama, veri almak 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. Ekstra yoklama, daha fazla HTTP ek yükü oluşturur.

  • Uzun yoklama (Asılı GET / COMET): Sunucuda kullanılabilir veri yoksa yeni veriler kullanılabilir hale gelene kadar sunucu 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 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 oluşturuldukları anda sunucudan istemciye yayınlanabilir. 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 sunucu tarafından gönderilen etkinlikleri neden 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, hisse senedi simgelerini, haber feed'lerini veya diğer otomatik veri aktarma mekanizmalarını güncellediğinde. 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 arkadaşınızdır.

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. İşin sihirli tarafı, bağlantı her kapatıldığında tarayıcının kaynağa yaklaşık 3 saniye sonra 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 İçerik Türü 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 ardışık satır, tek bir veri parçası olarak kabul edilir. 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

Kimlik ayarlamak, tarayıcının tetiklenen son etkinliği izlemesini sağlar. Böylece sunucuyla olan bağlantı kesilirse yeni istekle birlikte özel bir HTTP üst bilgisi (Last-Event-ID) ayarlanır. Bu sayede tarayıcı, hangi etkinliğin tetiklenmesinin uygun olduğunu belirleyebilir. message etkinliği, bir e.lastEventId özelliği içerir.

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ğlanma teşebbüsünde bulunulmuştur:

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, 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 "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

İstemcide etkinlik işleyiciler 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()));
?>

Node JS'de Express işleyicisi kullanılarak 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 bir akışı iptal etmek için text/event-stream Content-Type dışında bir yanıt verin 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ü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.