جریان به روز رسانی با رویدادهای ارسال شده توسط سرور

رویدادهای ارسال شده توسط سرور (SSEs) به‌روزرسانی‌های خودکار را از یک سرور به مشتری با اتصال HTTP ارسال می‌کنند. هنگامی که اتصال برقرار شد، سرورها می توانند انتقال داده را آغاز کنند.

ممکن است بخواهید از SSE ها برای ارسال اعلان های فشار از برنامه وب خود استفاده کنید. SSE ها اطلاعات را در یک جهت ارسال می کنند، بنابراین شما به روز رسانی را از مشتری دریافت نخواهید کرد.

مفهوم SSEs ممکن است آشنا باشد. یک برنامه وب در جریانی از به روز رسانی های تولید شده توسط یک سرور "مشترک" می شود و هر زمان که یک رویداد جدید رخ می دهد، یک اعلان برای مشتری ارسال می شود. اما برای درک واقعی رویدادهای ارسال شده توسط سرور، باید محدودیت های پیشینیان AJAX آن را درک کنیم. این شامل:

  • نظرسنجی : برنامه به طور مکرر از یک سرور برای داده نظرسنجی می کند. این تکنیک توسط اکثر برنامه های AJAX استفاده می شود. با پروتکل HTTP، واکشی داده ها حول یک فرمت درخواست و پاسخ می چرخد. مشتری درخواستی می دهد و منتظر می ماند تا سرور با داده ها پاسخ دهد. اگر هیچ یک در دسترس نباشد، یک پاسخ خالی برگردانده می شود. نظرسنجی اضافی سربار HTTP بیشتری ایجاد می کند.

  • نظرسنجی طولانی (Hanging GET / COMET) : اگر سرور داده های موجود را نداشته باشد، سرور درخواست را تا زمانی که داده های جدید در دسترس قرار گیرد باز نگه می دارد. از این رو، این تکنیک اغلب به عنوان "Hanging GET" شناخته می شود. هنگامی که اطلاعات در دسترس قرار می گیرد، سرور پاسخ می دهد، اتصال را می بندد و فرآیند تکرار می شود. بنابراین، سرور به طور مداوم با داده های جدید پاسخ می دهد. برای تنظیم این، توسعه‌دهندگان معمولاً از هک‌هایی مانند افزودن برچسب‌های اسکریپت به یک iframe «بی‌نهایت» استفاده می‌کنند.

رویدادهای ارسال شده توسط سرور، از ابتدا طراحی شده اند تا کارآمد باشند. هنگام برقراری ارتباط با SSE ها، سرور می تواند داده ها را هر زمان که بخواهد به برنامه شما ارسال کند، بدون اینکه نیازی به درخواست اولیه باشد. به عبارت دیگر، به‌روزرسانی‌ها را می‌توان از سروری به کلاینت دیگر در صورت وقوع جریان داد. SSE ها یک کانال تک جهتی را بین سرور و کلاینت باز می کنند.

تفاوت اصلی بین رویدادهای ارسال شده توسط سرور و نظرسنجی طولانی این است که SSE ها مستقیماً توسط مرورگر مدیریت می شوند و کاربر فقط باید به پیام ها گوش دهد.

رویدادهای ارسال شده توسط سرور در مقابل WebSockets

چرا رویدادهای ارسال شده توسط سرور را از WebSockets انتخاب می کنید؟ سوال خوبیه

WebSockets یک پروتکل غنی با ارتباطات دوطرفه و تمام دوبلکس دارد. یک کانال دو طرفه برای بازی‌ها، برنامه‌های پیام‌رسان و هر موردی که در آن به به‌روزرسانی‌های تقریباً هم‌زمان در هر دو جهت نیاز دارید، بهتر است.

با این حال، گاهی اوقات شما فقط به ارتباط یک طرفه از یک سرور نیاز دارید. به عنوان مثال، هنگامی که یک دوست وضعیت خود را به‌روزرسانی می‌کند، نمادهای سهام، فیدهای خبری یا سایر مکانیسم‌های فشار داده خودکار را به‌روزرسانی می‌کند. به عبارت دیگر، به روز رسانی به پایگاه داده وب SQL سمت سرویس گیرنده یا ذخیره شیء IndexedDB. اگر نیاز به ارسال داده به سرور دارید، XMLHttpRequest همیشه یک دوست است.

SSE ها از طریق HTTP ارسال می شوند. هیچ پروتکل یا اجرای سرور خاصی برای شروع کار وجود ندارد. WebSocket ها برای مدیریت پروتکل به اتصالات تمام دوبلکس و سرورهای WebSocket جدید نیاز دارند.

علاوه بر این، رویدادهای ارسال‌شده توسط سرور دارای ویژگی‌های مختلفی هستند که WebSocket‌ها از نظر طراحی فاقد آن‌ها هستند، از جمله اتصال مجدد خودکار، شناسه‌های رویداد و توانایی ارسال رویدادهای دلخواه.

یک EventSource با جاوا اسکریپت ایجاد کنید

برای اشتراک در جریان رویداد، یک شیء EventSource ایجاد کنید و URL جریان خود را ارسال کنید:

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

سپس، یک کنترل کننده برای رویداد message تنظیم کنید. شما می توانید به صورت اختیاری به موارد open و 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.
  }
});

وقتی به‌روزرسانی‌ها از سرور ارسال می‌شوند، onmessage handler فعال می‌شود و داده‌های جدید در ویژگی e.data آن در دسترس است. بخش جادویی این است که هر زمان که اتصال بسته شود، مرورگر به طور خودکار پس از 3 ثانیه به منبع متصل می شود. اجرای سرور شما حتی می‌تواند روی این زمان اتصال مجدد کنترل داشته باشد.

همین است. مشتری شما اکنون می تواند رویدادها را از stream.php پردازش کند.

قالب جریان رویداد

ارسال یک جریان رویداد از منبع، ایجاد یک پاسخ متنی ساده است که با یک text/event-stream نوع محتوا ارائه می‌شود، که از قالب SSE پیروی می‌کند. در شکل اصلی خود، پاسخ باید حاوی یک data: خط، به دنبال آن پیام شما، و به دنبال آن دو کاراکتر "\n" برای پایان دادن به جریان:

data: My message\n\n

داده های چند خطی

اگر پیام شما طولانی‌تر است، می‌توانید با استفاده از چندین data: خطوط. دو یا چند خط متوالی که با data: به عنوان یک تکه داده در نظر گرفته می شوند، به این معنی که فقط یک رویداد message اجرا می شود.

هر خط باید به یک "\n" ختم شود (به جز خط آخر که باید به دو ختم شود). نتیجه ای که به کنترل کننده message شما ارسال می شود یک رشته منفرد است که توسط کاراکترهای خط جدید به هم متصل شده است. به عنوان مثال:

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

این "خط اول\nخط دوم" را در e.data تولید می کند. سپس می توان از e.data.split('\n').join('') برای بازسازی پیام بدون کاراکترهای "\n" استفاده کرد.

ارسال داده های JSON

استفاده از چندین خط به شما کمک می کند تا JSON را بدون شکستن نحو ارسال کنید:

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

و کد ممکن سمت مشتری برای مدیریت آن جریان:

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

یک شناسه را با یک رویداد مرتبط کنید

می‌توانید با اضافه کردن خطی که با id: شروع می‌شود، یک شناسه منحصربه‌فرد با یک رویداد جریانی ارسال کنید:

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

تنظیم یک شناسه به مرورگر اجازه می‌دهد آخرین رویداد اجرا شده را ردیابی کند تا اگر اتصال به سرور قطع شد، یک هدر HTTP ویژه ( Last-Event-ID ) با درخواست جدید تنظیم شود. این به مرورگر اجازه می‌دهد تا مشخص کند کدام رویداد مناسب است. رویداد message حاوی ویژگی e.lastEventId است.

زمان اتصال مجدد را کنترل کنید

مرورگر تقریباً 3 ثانیه پس از بسته شدن هر اتصال سعی می کند دوباره به منبع متصل شود. می‌توانید با اضافه کردن خطی که با retry: و سپس تعداد میلی‌ثانیه‌های منتظر قبل از تلاش برای اتصال مجدد.

مثال زیر بعد از 10 ثانیه دوباره وصل می شود:

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

نام رویداد را مشخص کنید

یک منبع رویداد واحد می تواند انواع مختلفی از رویدادها را با گنجاندن نام رویداد ایجاد کند. اگر خطی که با event: شروع می شود وجود داشته باشد و به دنبال آن یک نام منحصر به فرد برای رویداد وجود داشته باشد، رویداد با آن نام مرتبط می شود. در مشتری، شنونده رویداد را می توان برای گوش دادن به آن رویداد خاص تنظیم کرد.

به عنوان مثال، خروجی سرور زیر سه نوع رویداد ارسال می‌کند، یک رویداد «پیام» عمومی، «userlogon» و «به‌روزرسانی»:

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

با راه اندازی شنوندگان رویداد بر روی مشتری:

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

نمونه های سرور

در اینجا یک اجرای اساسی سرور در 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()));
?>

در اینجا پیاده سازی مشابهی در Node JS با استفاده از یک هندلر 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>

یک جریان رویداد را لغو کنید

معمولاً هنگامی که اتصال بسته می شود، مرورگر به طور خودکار به منبع رویداد متصل می شود، اما این رفتار می تواند از مشتری یا سرور لغو شود.

برای لغو یک جریان از مشتری، تماس بگیرید:

source.close();

برای لغو یک جریان از سرور، با یک Content-Type غیر text/event-stream پاسخ دهید یا وضعیت HTTP غیر از 200 OK را برگردانید (مانند 404 Not Found ).

هر دو روش از برقراری مجدد اتصال توسط مرورگر جلوگیری می کنند.

یک کلمه در مورد امنیت

درخواست‌هایی که توسط EventSource ایجاد می‌شوند، مشمول خط‌مشی‌های یکسانی هستند که سایر API‌های شبکه مانند واکشی. اگر به نقطه پایانی SSE در سرور خود نیاز دارید که از مبداهای مختلف قابل دسترسی باشد، نحوه فعال کردن با Cross Origin Resource Sharing (CORS) را بخوانید.