البحث في الموقع
المحتوى عن 'websocket'.
-
سنشرح في هذا المقال آليات الاتصال الدائم المستمر مع الخادم وهو الاتصال الذي يُنشَأ فيه اتصال مستمر بين الخادم والعميل (غالبًا متصفح الويب) يجري فيه تبادل الطلبيات والردود آنيًا، وتنحصر آلية تنفيذ الاتصال الدائم تلك في ثلاثة أنواع: آلية الاستطلاع المفتوح Long polling وآلية مقابس الويب عبر البروتوكول WebSocket وآلية الأحداث المرسلة EventSource، سنناقش كل واحدة بالتفصيل على حدة. آلية الاستطلاع آلية الجس والاستطلاع polling هي الطريقة الأبسط للمحافظة على اتصال ثابت مع الخادم، ولا تتطلب أي بروتوكولات خاصة مثل WebSocket، أو أي أحداث من طرف الخادم، كما أنها سهلة الإنجاز وكافية في الكثير من الحالات. الاستطلاع الدوري Regular polling الطريقة الأبسط للحصول على معلومات من الخادم هي الاستطلاع الدوري، بإرسال طلبات إلى الخادم لسؤاله عن أي معلومات جديدة على فترات زمنية منتظمة (مثلًا: كل 10 ثوان)، وعند الاستجابة ينتبه الخادم إلى اتصال العميل به، ثم يرسل حزمةً من الرسائل التي تلقاها حتى اللحظة، سيعمل الأمر كما هو متوقع، لكن مع بعض الجوانب السلبية، منها: تمرَّر الرسائل بتأخير زمني قد يصل إلى 10 ثوان (الزمن الفاصل بين الطلبات). سيُغرَق الخادم بالطلبات كل 10 ثوان حتى لو غادر المستخدم الموقع أو استغرق في النوم، وهذا حمل كبير عليه من منظور الأداء. لهذا قد تكون هذه المقاربة جيدةً عندما تكون الخدمة بسيطةً، لكنها وبشكل عام تحتاج إلى التحسين. الاستطلاع المفتوح Long polling وهي طريقة أفضل للاستطلاع أو الجس من حالة الخادم، وتتميز بسهولة تنفيذها، وقدرتها على تسليم الرسائل دون تأخير. حيث: يُرسَل الطلب إلى الخادم. لا يغلق الخادم الاتصال حتى تكون لديه رسالة لتسليمها. عندما تظهر الرسالة، يستجيب الخادم للطلب بهذه الرسالة. يرسل المتصفح طلبًا جديدًا مباشرة. إن أساس هذه الطريقة هو إرسال المتصفح للطلب وإبقاء الاتصال مع الخادم قيد الانتظار، ولا يهيّئ المتصفح الاتصال من جديد حتى يتسلم الاستجابة، فإن فُقد الاتصال نتيجة خطأ ما، فسيرسل المتصفح طلبًا آخر مباشرةً. تمثل الشيفرة التالية الدالة subscribe التي ترسل الطلبات المفتوحة من جهة العميل: async function subscribe() { let response = await fetch("/subscribe"); if (response.status == 502) { // 502 خطأ تجاوز الوقت المسموح للاتصال // يحدث عند بقاء الاتصال مفتوحًا لفترة طويلة جدًا // ويغلقه الخادم لذا يجب إعادة الاتصال await subscribe(); } else if (response.status != 200) { // إظهار الخطأ showMessage(response.statusText); // إعادة الاتصال خلال ثانية await new Promise(resolve => setTimeout(resolve, 1000)); await subscribe(); } else { // الحصول على الرسالة وإظهارها let message = await response.text(); showMessage(message); // استدعاء التابع من جديد await subscribe(); } } subscribe(); يرسل التابع subscribe طلب إحضار، وينتظر الاستجابة ليعالجها عندما تصل، ثم يستدعي نفسه مجددًا. مثال اختباري لنظام محادثة بسيط إليك تطبيق محادثة اختباريًا يمكنك تنزيله وتشغيله محليًا إذا كنت على دراية باستخدام Node.JS وتثبيت الوحدات البرمجية اللازمة: ملف شيفرة العميل "browser.js": // POSTإرسال رسالة، طلب function PublishForm(form, url) { function sendMessage(message) { fetch(url, { method: 'POST', body: message }); } form.onsubmit = function() { let message = form.message.value; if (message) { form.message.value = ''; sendMessage(message); } return false; }; } // تلقي الرسائل باستخدام الاستطلاع المفتوح function SubscribePane(elem, url) { function showMessage(message) { let messageElem = document.createElement('div'); messageElem.append(message); elem.append(messageElem); } async function subscribe() { let response = await fetch(url); if (response.status == 502) { // تجاوز الوقت المسموح // يحدث عندما يبقى الاتصال مفتوحًا لفترة طويلة // لا بد من إعادة الاتصال await subscribe(); } else if (response.status != 200) { // Show Error showMessage(response.statusText); // إعادة الاتصال خلال ثانية await new Promise(resolve => setTimeout(resolve, 1000)); await subscribe(); } else { // الحصول على الرسالة let message = await response.text(); showMessage(message); await subscribe(); } } subscribe(); } ملف شيفرة الواجهة الخلفية "server.js": let http = require('http'); let url = require('url'); let querystring = require('querystring'); let static = require('node-static'); let fileServer = new static.Server('.'); let subscribers = Object.create(null); function onSubscribe(req, res) { let id = Math.random(); res.setHeader('Content-Type', 'text/plain;charset=utf-8'); res.setHeader("Cache-Control", "no-cache, must-revalidate"); subscribers[id] = res; req.on('close', function() { delete subscribers[id]; }); } function publish(message) { for (let id in subscribers) { let res = subscribers[id]; res.end(message); } subscribers = Object.create(null); } function accept(req, res) { let urlParsed = url.parse(req.url, true); // مستخدم جديد يريد النتائج if (urlParsed.pathname == '/subscribe') { onSubscribe(req, res); return; } // إرسال رسالة if (urlParsed.pathname == '/publish' && req.method == 'POST') { // POSTقبول req.setEncoding('utf8'); let message = ''; req.on('data', function(chunk) { message += chunk; }).on('end', function() { publish(message); // النشر للجميع res.end("ok"); }); return; } // البقية ثابتة fileServer.serve(req, res); } function close() { for (let id in subscribers) { let res = subscribers[id]; res.end(); } } // ----------------------------------- if (!module.parent) { http.createServer(accept).listen(8080); console.log('Server running on port 8080'); } else { exports.accept = accept; if (process.send) { process.on('message', (msg) => { if (msg === 'shutdown') { close(); } }); } process.on('SIGINT', close); } الملف الأساسي "index.html": <!DOCTYPE html> <script src="browser.js"></script> All visitors of this page will see messages of each other. <form name="publish"> <input type="text" name="message" /> <input type="submit" value="Send" /> </form> <div id="subscribe"> </div> <script> new PublishForm(document.forms.publish, 'publish'); // random url parameter to avoid any caching issues new SubscribePane(document.getElementById('subscribe'), 'subscribe?random=' + Math.random()); </script> وستظهر النتيجة بالشكل التالي: مجال الاستخدام تعمل طريقة الاستطلاع المفتوح بصورة ممتازة عندما تكون الرسائل نادرةً، لكن عندما تتكرر الرسائل بكثرة، فسيكون منحني (طلب-استجابة) المرسوم في الأعلى على هيئة أسنان المنشار، ولأن لكل رسالة طلبًا مستقلًا له ترويسات وآلية استيثاق خاصة، فمن الأفضل استخدام طرق أخرى مثل: Websocket أوServer Sent Events، التي سنشرحها في الفقرات التالية. استخدام البروتوكول WebSocket يزوّدنا هذا البروتوكول الموصوف في المعيار RFC 6455 بطريقة لتبادل البيانات بين الخادم والمتصفح عبر اتصال ثابت، حيث تمرَّر البيانات إلى كلا الطرفين على شكل حزم دون قطع الاتصال أو استخدام طلبات HTTP إضافية، وتظهر الفائدة الكبيرة لاستخدامه عند وجود خدمات تتطلب التبادل المستمر للبيانات، مثل ألعاب الشبكة وأنظمة التجارة العاملة في الزمن الحقيقي وغيرها. مثال بسيط سنحتاج إلى كائن مقبس اتصال جديد new WebSocket لفتح اتصال websocket باستخدام بروتوكول نقل خاص هو ws في العنوان: let socket = new WebSocket("ws://javascript.info"); كما يمكن استخدام البروتوكول المشفّر //:wss، فهو مشابه للبروتوكول HTTPS لكن مع websockets. ينبغي الاستماع إلى الأحداث التي تطرأ على مقبس الاتصال Socket بعد تأسيسه مباشرةً، وهي أربعة أحداث: open: عند تأسيس الاتصال. message: عند استقبال البيانات. error: عند وقوع خطأ يتعلق بالبروتوكول websocket. close: عند إغلاق الاتصال. ولإرسال أي بيانات يكفي استخدام الصيغة (socket.send(data، وإليك مثالًا: let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello"); socket.onopen = function(e) { alert("[open] Connection established"); alert("Sending to server"); socket.send("My name is John"); }; socket.onmessage = function(event) { alert(`[message] Data received from server: ${event.data}`); }; socket.onclose = function(event) { if (event.wasClean) { alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`); } else { // كأن تلغى العملية أو فشل في الشبكة // 1006 قيمته في هذه الحالة عادة event.code alert('[close] Connection died'); } }; socket.onerror = function(error) { alert(`[error] ${error.message}`); }; لقد كتبنا شيفرة خادم تجريبي باستخدام Node.JS في الملف server.js لتشغيل الشيفرة في المثال السابق، وسيرد الخادم بالرسالة "Hello from server, John"، ثم ينتظر 5 ثوان ويقطع الاتصال. سترى إذًا تسلسل الأحداث open → message → close، والأمر بهذه البساطة فعليًا، لأنك تستخدم الآن البروتوكول WebSocket، لنتحدث الآن في أمور أكثر عمقًا. فتح اتصال مقبس ويب WebSocket يُنشئ الأمر (new WebSocket(url كائنًا جديدًا ويبدأ عملية الاتصال مباشرةً، سيسأل المتصفح الخادم خلال تأسيس الاتصال -عبر الترويسات- إذا كان يدعم البروتوكول Websocket، فإن كان الجواب نعم فسيستمر العمل به، وهو يختلف عن HTTP كليًا. وإليك مثالًا عن ترويسات المتصفح للطلب الذي يرسله الأمر (new WebSocket("wss://javascript.info/chat. GET /chat Host: javascript.info Origin: https://javascript.info Connection: Upgrade Upgrade: websocket Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== Sec-WebSocket-Version: 13 Origin: أصل الصفحة العميلة (https://javascript.info مثلًا)، فالكائنات WebSocket هي كائنات من أصول مختلطة بطبيعتها، حيث لا توجد أي قيود لاستخدامها، وطالما أنّ الخوادم القديمة لا تدعم هذا البروتوكول، فلن نواجه مشكلة توافق. تظهر أهمية الترويسة Origin في أنها تسمح للخادم بالاختيار إن كان سيتواصل مع الصفحة العميلة وفق هذا البروتوكول أم لا. Connection: Upgrade: يشير إلى رغبة العميل في تغيير البروتوكول. Upgrade: websocket: تدل أنّ البروتوكول المطلوب هو websocket. Sec-WebSocket-Key: مفتاح أمان عشوائي يولده المتصفح. Sec-WebSocket-Version: نسخة بروتوكول websocket، وهي حاليًا النسخة 13. ينبغي على الخادم أن يعيد الرمز 101 عندما يوافق على التحوّل إلى البروتوكول WebSocket: 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g= ستكون الترويسة Sec-WebSocket-Accept هي نفسها Sec-WebSocket-Key مسجلةً باستخدام خوارزمية خاصة، وسيستخدمها المتصفح للتأكد من توافق الاستجابة مع الطلب. بعد ذلك ستُنقل البيانات باستخدام بروتوكول WebSocket، وسنطلع على بنيتها (إطاراتها) قريبًا. الموسعات والبروتوكولات الفرعية هنالك أيضًا الترويستان Sec-WebSocket-Extensions التي تصف الموسّعات، وSec-WebSocket-Protocol التي تصف البروتوكولات الفرعية. فمثلًا: Sec-WebSocket-Extensions: deflate-frame: يعني أن المتصفح سيدعم ضغط البيانات، فالموسّع هو شيء يتعلق بنقل البيانات ووظيفة توسّع عمل البروتوكول، يرسل المتصفح الترويسة Sec-WebSocket-Extensions تلقائيًا مزودةً بقائمة الموسّعات المدعومة كلها. Sec-WebSocket-Protocol: soap, wamp: يعني أننا سننقل البيانات باستخدام أحد البروتوكولين الفرعيين SOAP أو WAMP (وهو اختصار للعبارة " WebSocket Application Messaging Protocol" أي "بروتوكول نقل الرسائل لتطبيق websocket"). تُسجل البروتوكولات الفرعية في IANA catalogue، أي أن هذه الترويسة الاختيارية تحدد صيغ البيانات التي سنستخدمها، وتُضبَط باستخدام المعامل الثاني للدالة البانية new WebSocket، فلو أردنا استخدام SOAP أو WAMP مثلًا، فسنضعهما في مصفوفة بالشكل التالي: let socket = new WebSocket("wss://javascript.info/chat", ["soap", "wamp"]); سيستجيب الخادم بقائمة من البروتوكولات والموسّعات التي يوافق على استخدامها، وإليك مثالًا: GET /chat Host: javascript.info Upgrade: websocket Connection: Upgrade Origin: https://javascript.info Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== Sec-WebSocket-Version: 13 Sec-WebSocket-Extensions: deflate-frame Sec-WebSocket-Protocol: soap, wamp وستكون الاستجابة: 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g= Sec-WebSocket-Extensions: deflate-frame Sec-WebSocket-Protocol: soap أي أن الخادم سيدعم الموسّع "deflate-frame" والبروتوكول الفرعي SOAP فقط. نقل البيانات يتكون الاتصال من إطارات "frames"، وهي أجزاء من البيانات يمكن إرسالها من كلا الطرفين وقد تكون من الأنواع التالية: "text": يحتوي بيانات نصيةً ترسلها الأطراف إلى بعضها. "binary data": يحتوي بيانات ثنائيةً تُرسلها الأطراف إلى بعضها. "ping/pong": تستخدَم للتحقق من الاتصال، ويرسلها الخادم إلى المتصفح الذي يرد عليها تلقائيًا. ستجد أيضًا "connection close" وبعض الإطارات الأخرى. ونعمل في بيئة المتصفح مع الإطارات النصية والثنائية. يمكن للتابع ()send. إرسال بيانات نصية أو ثنائية.، ويسمح استدعاء التابع (socket.send(body بأن تكون قيمة جسم الطلبbody بالتنسيق النصي أو الثنائي، بما في ذلك الكائن Blob وArrayBuffer وغيرهما، ولا حاجة لإجراء ضبط خاص بل تُرسَل البيانات كما هي. تأتي البيانات النصية على شكل "string" عندما نستقبل البيانات، ومع ذلك يمكننا الاختيار بين BlobوArrayBuffer للبيانات الثنائية.** وكذا ضبط ذلك باستخدام الخاصية socket.binaryType، التي تأخذ القيمة "blob" افتراضيًا، لذلك تأتي البيانات الثنائية ضمن الكائن Blob، الذي يمثل كائن بيانات ثنائية عالي المستوى ويتكامل مباشرةً مع المعرّفين <a>و<img> وغيرهما، ويمكن الانتقال إلى استخدام الكائن ArrayBuffer عند معالجة البيانات الثنائية والعمل مع البايتات بشكل منفصل. socket.binaryType = "arraybuffer"; socket.onmessage = (event) => { //أما أن يكون نصًا أو بيانات ثنائيةً event.data }; محدودية معدل النقل Rate limiting لنتخيل أن تطبيقنا قادر على توليد كمية كبيرة من البيانات، لكن المستخدم يعاني من اتصال بطيء بالانترنت. يمكن استدعاء التابع (socket.send(data مرات عدةً، لكن البيانات ستُخزّن مؤقتًا في الذاكرة وستُرسَل بالسرعة التي تسمح بها الشبكة فقط، عندها يمكن استخدام الخاصية socket.bufferedAmount التي تُخزّن حجم البيانات الموجودة في المخزن المؤقت في هذه اللحظة وتنتظر الظروف المناسبة لنقلها، ويمكن فحص الحجم المتبقي من البيانات للتأكد من أن المقبس Socket لا يزال متاحًا لمتابعة نقل البيانات. // افحص المقبس كل 100 ثانية وحاول إرسال البيانات // عندما يكون حجم البيانات المخزّنة التي لم تصل صفرًا setInterval(() => { if (socket.bufferedAmount == 0) { socket.send(moreData()); } }, 100); إغلاق الاتصال عندما يحاول أحد الأطراف إغلاق الاتصال (للمخدم والمتصفح الحق نفسه في ذلك)، يرسَل عادةً إطار إغلاق الاتصال "connection close" الذي يحتوي قيمةً عدديةً وسببًا نصيًا للإغلاق. إليك الصيغة اللازمة: socket.close([code], [reason]); code: رمز خاص لإغلاق اتصال WebSocket، وهو اختياري. reason: نص يصف سبب الإغلاق، وهو اختياري. يستقبل معالج الحدث close في الطرف الآخر الرمز والسبب: // الطرف الذي سيغلق الاتصال: socket.close(1000, "Work complete"); // الطرف الآخر socket.onclose = event => { // event.code === 1000 // event.reason === "Work complete" // event.wasClean === true (clean close) }; إليك القيم الأكثر شيوعًا للرموز: 1000: وهي القيمة الافتراضية، وتستخدم للإغلاق العادي (تستخدم إن لم يكن هناك رمز code). 1006: لا يمكن ضبط هذا الرمز يدويًا ويشير إلى فقد الاتصال. 1001: عند مغادرة أحد الأطراف، مثل: إغلاق الخادم أو مغادرة المتصفح للصفحة. 1009: الرسائل أضخم من أن تُعالج. 1011: خطأ في الخادم. يمكن الاطلاع على القائمة الكاملة ضمن المعيار RFC6455, §7.4.1. إنّ رموز WebSocket مشابهة نوعًا ما لرموز HTTP وتختلف في بعض النواحي، فأي رمز أقل من 1000 سيكون محجوزًا، وسيتولد خطأ عند استخدامه. // في حال فشل الاتصال socket.onclose = event => { // event.code === 1006 // event.reason === "" // event.wasClean === false (no closing frame) }; حالة الاتصال تتيح الخاصية socket.readyState إمكانية معرفة الوضع الحالي للاتصال وفقًا للقيم التي تأخذها: 0 متصل "CONNECTING": أي أن الاتصال يجري، ولم يُؤسَّس بعد. 1 مفتوح "OPEN": التواصل قائم. 2 يُغلق "CLOSING": يجري إغلاق الاتصال. 3 مُغلق "CLOSED": الاتصال مغلق. مثال تطبيق محادثة آنية لنراجع مثال تطبيق المحادثة باستخدام الواجهة البرمجية WebSocket API والوحدة البرمجية Node.js WebSocket module، سنركز على طرف العميل، وسيكون الخادم بسيطًا أيضًا. نحتاج إلى نموذج <form> لإرسال الرسائل ومعرِّف <div> لاستقبالها. <!-- message form --> <form name="publish"> <input type="text" name="message"> <input type="submit" value="Send"> </form> <!-- div with messages --> <div id="messages"></div> كما سنستخدم في جافاسكربت: فتح الاتصال. إرسال رسالة عن طريق النموذج (socket.send(message. وضع الرسالة المستقبلة ضمن div#messages. وإليك الشيفرة اللازمة: let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws"); // إرسال رسالة إلى النموذج document.forms.publish.onsubmit = function() { let outgoingMessage = this.message.value; socket.send(outgoingMessage); return false; }; // div#messagesإظهار الرسالة بعد وصولها في socket.onmessage = function(event) { let message = event.data; let messageElem = document.createElement('div'); messageElem.textContent = message; document.getElementById('messages').prepend(messageElem); } لن نتعمق بشيفرة الواجهة الخلفية، فهي خارج نطاق مقالتنا، لكننا سنستخدم هنا Node.js، ومع ذلك فالأمر ليس إلزاميًا، إذ أن للعديد من المنصات طرقها للتعامل مع WebSocket. تمثل الخطوات التالية خوارزمية عمل الواجهة الخلفية: إنشاء مجموعة من مقابس الاتصال ()clients = new Set. إضافة كل مقبس مقبول إلى المجموعة clients.add(socket)، وتهيئة مستمع message للحدث للحصول على الرسائل المتعلقة بالمقبس. عندما استقبال رسالة كرر عملية إرسالها إلى كل عميل. احذف مقابس العملاء عند قطع الاتصال (clients.delete(socket. const ws = new require('ws'); const wss = new ws.Server({noServer: true}); const clients = new Set(); http.createServer((req, res) => { // websocket سنعالج فقط اتصالات // websocket في مشروعنا الحقيقي ستوجد هنا شيفرة تعالج الطلبات بطرق أخرى غير wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect); }); function onSocketConnect(ws) { clients.add(ws); ws.on('message', function(message) { message = message.slice(0, 50); // 50سيكون الحجم الأكبر للرسالة هو for(let client of clients) { client.send(message); } }); ws.on('close', function() { clients.delete(ws); }); } إليك النتيجة: يمكن تنزيل التطبيق وتجربته محليًا على جهازك، ولا تنس تثبيت Node.js، ثم تثبيت WebSocket باستخدام الأمر npm install ws قبل تشغيل التطبيق. الأحداث المرسلة من قبل الخادم تشرح التوصيفات Server-Sent Events صنفًا مدمجًا هو EventSource، حيث يحافظ على الاتصال مع الخادم ويسمح باستقبال أحداث منه، وهو اتصال ثابت مثل WebSocket تمامًا لكن مع بعض الاختلافات بينهما: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } WebSocket EventSource ثنائي الاتجاه: إذ يمكن أن يتبادل الخادم والعميل الرسائل وحيد الاتجاه: فالخادم هو من يرسل الرسائل إلى العميل نصوص وبيانات ثنائية نصوص فقط بروتوكول websocket بروتوكول HTTP النظامي. يمثل EventSource طريقةً أقل قدرةً للاتصال مع الخادم بالموازنة مع WebSocket، إذًا لماذا نستخدمها؟ يعود السبب الرئيسي لاستخدامه في كونه أبسط، إذ لا نحتاج إلى القدرة الكبيرة لبروتوكول WebSocket في بعض التطبيقات، فتلقي بيانات من الخادم مثل رسائل محادثة أو لوائح أسعار أو أي شيء مشابه هو مجال EventSource، كما أنه يدعم إعادة الاتصال، وهو أمر لا بد من إنجازه يدويًا في WebSocket، ويستخدم كذلك بروتوكول HTTP وليس أي بروتوكول جديد. الحصول على الرسائل لا بد من إنشاء كائن جديد من الصنف EventSource للبدء باستقبال الرسائل، وذلك بتنفيذ الأمر (new EventSource(url، حيث سيتصل المتصفح بالعنوان url ويبقي الاتصال مفتوحًا ومنتظرًا الأحداث، وهنا ينبغي أن يستجيب الخادم برمز الحالة 200 وبالترويسة Content-Type: text/event-stream، وسيبقي الخادم بعدها قناة الاتصال مفتوحةً، وسيرسل الرسائل ضمنها بتنسيق خاص بالشكل التالي: data: Message 1 data: Message 2 data: Message 3 data: of two lines تأتي الرسالة بعد الكلمة :data بينما يعَد الفراغ الموجود بعد النقطتين اختياريًا. يفصَل بين الرسائل برمز السطر الجديد مضاعفًا n\n\ يمكننا عند الحاجة إلى إرسال رمز سطر جديد n\ أن نرسل مباشرةً الكلمة :data (كما في الرسالة الثالثة في المثال السابق)، لكن عمليًاP تًرسل الرسائل المركّبة مرمزةً بتنسيق JSON، وترمّز محارف الانتقال إلى سطر جديد بالشكل n\ ضمنها، فلا حاجة عندها للرسائل :data متعددة السطر. فمثلًا: data: {"user":"John","message":"First line\n Second line"} وهكذا يمكن أن نعد أن العبارة data: ستليها رسالة واحدة تمامًا. يولَّد الحدث message لكل من تلك الرسائل بالشكل التالي: let eventSource = new EventSource("/events/subscribe"); eventSource.onmessage = function(event) { console.log("New message", event.data); // سيطبع ثلاث رسائل في المثال السابق }; // or eventSource.addEventListener('message', ...) الطلبات ذات الأصل المختلط يدعم الصنف EventSource الطلبات ذات الأصل المختلط cross-origin requests كما يفعل fetch، إذ يمكننا استخدام أي عنوان URL: let source = new EventSource("https://another-site.com/events"); سيستقبل الخادم البعيد الترويسة Origin وينبغي عليه الإجابة بالترويسة Access-Control-Allow-Origin للمتابعة، ولتمرير الثبوتيات credentials لا بد من ضبط خيار إضافي هو withCredentials بالشكل التالي: let source = new EventSource("https://another-site.com/events", { withCredentials: true }); راجع فصل استخدام Fetch مع الطلبات ذات الأصول المختلطة للاطلاع على تفاصيل أكثر حول ترويسات الأصول المختلطة. إعادة الاتصال بعد إنشاء الكائن EventSource فسيتصل بالخادم مباشرةً ويعيد الاتصال في حال انقطاعه، وبالتالي لا حاجة لإجراء تدابير خاصة بهذا الأمر، وبطبيعة الحال يوجد تأخير زمني صغير لثوان معدودة افتراضيًا بين كل محاولتين لإعادة الاتصال، وهنا يمكن للخادم أن يضبط التأخير اللازم باستخدام الخاصية :retry في الاستجابة (مقدرًا بالميلي ثانية). retry: 15000 data: Hello, I set the reconnection delay to 15 seconds يمكن أن تأتي :retry مصحوبةً ببيانات أخرى أو برسالة وحيدة، وهكذا سيكون على الخادم انتظار الفترة المحددة قبل إعادة الاتصال، وقد ينتظر فترة أطول، فإذا علم المتصفح -عن طريق نظام التشغيل- أن الشبكة غير متصلة حاليًا مثلًا، فسينتظر حتى يعود الاتصال ثم يحاول ثانيةً. إذا أراد الخادم من المتصفح التوقف عن إعادة الاتصال، فعليه أن يرد برمز الحالة 204 (رمز HTTP). إذا أراد المتصفح إغلاق الاتصال، فعليه استدعاء التابع eventSource.close. let eventSource = new EventSource(...); eventSource.close(); إذا احتوت الاستجابة على ترويسة Content-Type خاطئةً فلن يعاد الاتصال، وكذلك إذا اختلف رمز حالة HTTP عن القيم 301 أو 307 أو 200 أو 204، فعندها سيُولد الحدث "error" ولن يعيد المتصفح الاتصال. معرف الرسالة الفريد Message id عندما ينقطع الاتصال جراء خلل في الشبكة، tلن يتأكد كلا الطرفين من كون الرسائل استلمَت أم لا، وحتى يُستأنَف الاتصال بشكل صحيح، فينبغي أن تحمل كل رسالة مُعرِّفًا مميزًا id مثل التالي: data: Message 1 id: 1 data: Message 2 id: 2 data: Message 3 data: of two lines id: 3 عندما يتلقى المتصفح الرسالة مع معرّفها :id فإنه: يضبط قيمة الخاصية eventSource.lastEventId على قيمة المعرِّف. يعيد الترويسة Last-Event-ID أثناء إعادة الاتصال مع قيمة المعرّف id، ليتمكن الخادم من إرسال الرسائل التالية إن وجدت. حالة الاتصال: الخاصية ReadyState يمتلك الكائن EventSource الخاصية readyState التي تحمل إحدى القيم الثلاث التالية: EventSource.CONNECTING = 0; // يتصل أو يعيد الاتصال EventSource.OPEN = 1; // متصل EventSource.CLOSED = 2; // الاتصال مغلق تكون قيمة هذه الخاصية عند إنشاء كائن جديد أو انقطاع الاتصال هي EventSource.CONNECTING دومًا (وهي تعادل 0)، ويمكن الاستعلام عن هذه الخاصية لمعرفة حالة الكائن EventSource. أنواع الأحداث يوّلِّد الكائن EventSource افتراضيًا ثلاثة أحداث: message: عند استلام رسالة، وتكون متاحةً باستخدام event.data. open: عندما يكون الاتصال مفتوحًا. error: عند عدم إمكانية تأسيس الاتصال، كأن يعيد الخادم رمز الحالة 500. فمثلًا: event: join data: Bob data: Hello event: leave data: Bob لمعالجة الأحداث السابقة لابد من استخدام مستمع الحدث addEventListener وليس onmessage: eventSource.addEventListener('join', event => { alert(`Joined ${event.data}`); }); eventSource.addEventListener('message', event => { alert(`Said: ${event.data}`); }); eventSource.addEventListener('leave', event => { alert(`Left ${event.data}`); }); مثال نموذجي كامل يرسل الخادم في هذا المثال الرسائل 1 و2 و3 ثم bye ويقطع الاتصال، بعدها يعيد المتصفح الاتصال تلقائيًا. شيفرة الخادم "server.js": let http = require('http'); let url = require('url'); let querystring = require('querystring'); let static = require('node-static'); let fileServer = new static.Server('.'); function onDigits(req, res) { res.writeHead(200, { 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-cache' }); let i = 0; let timer = setInterval(write, 1000); write(); function write() { i++; if (i == 4) { res.write('event: bye\ndata: bye-bye\n\n'); clearInterval(timer); res.end(); return; } res.write('data: ' + i + '\n\n'); } } function accept(req, res) { if (req.url == '/digits') { onDigits(req, res); return; } fileServer.serve(req, res); } if (!module.parent) { http.createServer(accept).listen(8080); } else { exports.accept = accept; } شيفرة الملف "index.html": <!DOCTYPE html> <script> let eventSource; function start() { // "Start" عند ضغط الزر if (!window.EventSource) { // متصفح إنترنت إكسبلورر أو أي متصفح قديم alert("The browser doesn't support EventSource."); return; } eventSource = new EventSource('digits'); eventSource.onopen = function(e) { log("Event: open"); }; eventSource.onerror = function(e) { log("Event: error"); if (this.readyState == EventSource.CONNECTING) { log(`Reconnecting (readyState=${this.readyState})...`); } else { log("Error has occurred."); } }; eventSource.addEventListener('bye', function(e) { log("Event: bye, data: " + e.data); }); eventSource.onmessage = function(e) { log("Event: message, data: " + e.data); }; } function stop() { // when "Stop" button pressed eventSource.close(); log("eventSource.close()"); } function log(msg) { logElem.innerHTML += msg + "<br>"; document.documentElement.scrollTop = 99999999; } </script> <button onclick="start()">Start</button> Press the "Start" to begin. <div id="logElem" style="margin: 6px 0"></div> <button onclick="stop()">Stop</button> "Stop" to finish. وستظهر النتيجة بالشكل التالي: خلاصة تعرفنا في هذا الفصل على آليات إنشاء إتصال مستمر مع الخادم، فبدأ أولًا بعرض آلية الاستطلاع والجس وهي الطريقة الأبسط للمحافظة على اتصال ثابت مع الخادم، ولا تتطلب أي بروتوكولات أو أي أحداث خاصة من طرف الخادم، وهي طريقة سهلة الإنجاز وكافية في الكثير من الحالات. انتقلنا بعدها إلى شرح بروتوكول WebSocket والذي يُقدم طريقةً عصريةً لتأسيس اتصال ثابت بين المتصفح والخادم، له ميزات منها: لا توجد تقييدات للأصول المختلطة. مدعوم جيدًا من قبل المتصفحات. يمكنه إرسال نصوص وبيانات ثنائية واستقبالها. واجهته البرمجية بسيطة. التوابع المستعملة في هذا البروتوكول: socket.send(data) socket.close([ code ], [reason]) أما الأحداث: open message error close لا تقدّم WebSocket بذاتها تقنيات لإعادة الاتصال أو الاستيثاق وغيرها من الآليات عالية المستوى، لذلك ستجد مكتبات للعميل والخادم تهتم بهذه النقاط، ويمكنك أيضًا تنفيذ شيفرتها يدويًا بنفسك. ولكي تتكامل WebSocket مع مشروع جاهز، جرت العادة أن يشغل المستخدم خادم WebSocket بالتوازي مع خادم HTTP الرئيسي ليتشاركا بقاعدة بيانات واحدة، حيث تُرسل طلبات WebSocket إلى النطاق الفرعي wss://ws.site.com الذي يقود إلى خادم WebSocket، بينما يقود العنوان https://site.com إلى خادم HTTP الرئيسي، كما توجد طرق أخرى للتكامل. عرضنا أخيرًا الآلية الثالثة للاتصال وهي عبر الكائن EventSource الذي يؤسس اتصالًا ثابتًا مع الخادم ويسمح له بإرسال الرسائل عبر قناة الاتصال، ويقدم: إعادة الاتصال التلقائي مع قيمة زمنية retry لإعادة الاتصال بعدها. معرّفات فريدة للرسائل "ids" لاستئناف الأحداث من آخر معرّف أرسِل مع الترويسة Last-Event-ID أثناء إعادة الاتصال. حالة الاتصال من خلال الخاصية readyState. تجعل هذه المزايا الكائن EventSource بديًلا قيمًا للكائن WebSocket، الذي يعمل في مستوىً أدنى ويفتقر إلى الميزات السابقة، والتي يمكن إنجازها يدويًا بالطبع. يعَد الكائن EventSource في الكثير من التطبيقات الحقيقية قادرًا كفاية على إنجاز المطلوب، وتدعمه جميع المتصفحات الحديثة، بينما لا يدعمه IE، وإليك صيغة استخدامه: let source = new EventSource(url, [credentials]); للوسيط الثاني خيار وحيد ممكن هو: { withCredentials: true }، ويسمح بإرسال ثبوتيات طلبات الأصول المختلطة. إن عوامل الأمان المتعلقة باستخدام طلبات الأصول المختلطة للكائن EventSourceمشابهة للاتصال fetch وغيره من طرق الاتصال عبر الشبكات. خاصيات الكائن EventSource: readyState: حالة الاتصال القائم EventSource.CONNECTING (=0) EventSource.OPEN (=1) EventSource.CLOSED (=2) lastEventId: آخر معرّف استقبله المتصفح، وسيرسله أثناء إعادة الاتصال مع الترويسة Last-Event-ID. وتوابع EventSource فتتمثل بالتابع: ()close: يغلق الاتصال. أما أحداث EventSource فهي: message: عند استلام رسالة، وتكون متاحةً باستخدام event.data. open: عندما يكون الاتصال مفتوحًا. error: في حال وقع خطأ، بما في ذلك فقدان الاتصال أو الأخطاء القاتلة، يمكننا التحقق من الخاصية readyState للتأكد من وجود محاولات لإعادة الاتصال. يمكن للخادم اختيار اسم مخصص لحدث ما من خلال :event، وينبغي معالجة هذه الأحداث من خلال مستمع حدث addEventListener وليس باستخدام <on<event. رأينا من أجل تنسيق استجابة الخادم أن الخادم يرسل الرسائل مفصولةً عن بعضها باستخدامn\n\، ويمكن أن تحتوي الرسالة الحقول التالية: :data ويمثل جسم الرسالة، وتُفسَّر سلسلة من الحقول data كرسالة واحدة يفصل بين أجزائها الكتلة n\. :id تُجدِّد قيمة lastEventId وتُرسَل ضمن الترويسة Last-Event-ID عند إعادة الاتصال. :retry تحدد فترة التأخير الزمني بين كل محاولتين لإعادة الاتصال بالميلي ثانية، ولا يمكن ضبطها باستخدام JavaScript. :event اسم الحدث وينبغي أن تسبق الحقل data. يمكن أن تحتوي الرسالة على أحد الحقول السابقة أو أكثر وبأي ترتيب، لكن الحقل :id يأتي في النهاية عادةً. ترجمة -وبتصرف- للفصول Long polling وWebSocket وServer sent events من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: استئناف رفع الملفات في جافاسكريبت تطويع البيانات في جافاسكربت
-
SSE اختصار لـ Server Sent Events وتسمى أيضًا EventSource تُستخدم لِبنّاء تطبيقات ويب تفاعلية. حيث يُرسل الخادوم أحداثا في الوقت الحقيقي إلى المستخدم، قد يتضمن الحدث تنبيهات للمستخدم أو بيانات يمكن استغلالها في عمل شيء ما وتحديث صفحة الويب. في السّابق، لِعمل تحديث تلقائي للبيانات كل ثانية كانت تُستَخدم هذه الطريقة: setInterval(function(){ // إستدعاء الدالة بشكل تكراري xhr = new XMLHttpRequest(); // إستخدام طلب ajax xhr.onreadystatechange = function(){// عند أي تغير في حالة الطلب if(this.readyState == 4){// تم إستقبال النص بنجاح // إفعل بعض الاوامر } } xhr.open("get","http://localhost"); // تحديد نوع الطلب ورابط الموقع xhr.send("mydata"); // إرسال البيانات إلى الخادم }, 1000); // تحديد مدّة التكرار إلى ثانية أو هذه الطريقة الأكثر تطورًا: function getAjax(){ // تعريف الدالة التي ستدعى بشكل تكراري xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(){ if(this.readyState == 4) setTimeout(function(){ // استدعاء ألدالة مجددًا بعد ثانية من الإنتهاء لِتكرار طلب الajax getAjax(); },1000); } xhr.open("get","http://localhost"); xhr.send("mydata"); } getAjax();// إستدعاء الدالة للمرة الأولىهذه الطرق صحيحة لكنها ليست الأفضل، حيث تقوم بإرسال طلب كل ثانية للخادم مما يُسبٍّب ضغط عليه وغالبًا لا يكون التحديث في الوقت الحقيقي لظهوره. فكرة عمل SSE أن الطلب يظل مَفتوحا في إنتظار محتوى جديد والخادم هو من يُرسل المحتوى الجديد وليس المتصفح من يطلبه. SSE ليست الطريقة الوحيدة لِبناء تطبيقات ويب تفاعلية، يوجد العديد من الطرق الأخرى من أشهرها WebSocket. مُقارنة بين SSE و WebSocketالسهولة والتوافقيةمعظم الإستضافات المشتركة لا تَدعم WebSocket وهو يعمل على بروتوكل ws أو wss (في حالة إستخدام شهادة أمان ssl) وهما مختلفان عن الذي عليه واجهة المستخدم مما يُسبِّب مشاكل في مصادفة الاثنين معًا (يوجد مكتبات لفعل ذلك) على خلاف SSE التي تُعتبر طلب عادي على نفس بروتوكول واجهة المستخدم النهائية. تزامن المتسخدمينWebSocket يُمكنها عمل تزامن بين المستخدمين بطريقة مباشرة حيث أن الإتصلات تُعالج كلها معًا. في SSE لايوجد طريقة مباشرة لِتزامن المسخدمين معًا لأن كل طلب مُنفصل عن الآخر لذا تُستخدم قاعدة بيانات وتحفظ أخر التغيرات للمستخدم الأول وعند المستخدم الآخر يَتم البحث بشكل دوري عن التغيرات في قاعدة البيانات ثم إرسالها إلى المستخدم الآخر. إرسال وإستقبال البياناتWebSocket و SSE يدعمان إستقبال البيانات لكن لا يُمكن إرسال البيانات إلى الخادم بإستخدام SSE، أي عند بناء تطبيق يعتمد على إرسال وإستقبال البيانات من الخادوم ستضطر إلى بناء صفحة منفصلة تقوم بإستقبال الطلبات من المستخدم بواسطة ajax مثلًا. تطبيقات قد يكون إستخدام WebSocket فيها أفضلفي تطبيق سيحتاج إلى تفاعل المستخدمين معًا (تطبيق تواصل مثلًا) سيكون إستخدام WebSocket أفضل من SSE لأن الأخيرة لا يُمكنها إستقبال الطلبات من الخادوم.إرسال إشعارات أو رسائل للمستخدم أو أي تطبيق لن يَحتاج المستخدم فيه إلى إرسال رسائل إلى الخادوم. هنا ممكن أن يكون إستخدام SSE أفضل وأسهل.اهلًا بالعالمقبل البدء يجب أن تَعرف المتصفحات الداعمة لـِ SSE وأن متصفح أنترنت أكسبلور لايَدعمه لكن يُمكنك إستخدام مكتبة خارجية لدعمه مثل EventSource.js (يتم تضمين ملف javascript في الصفحة) هذا مثال لنص مصدري لإستجابة طلب SSE: http/1.1 200 OK ## الصفحة موجودة ! Content-Type: text/event-stream ## تعريف المتصفح أن الصفحة هي محتوى لطلب SSE event: delete## تعريف الحدث الذي سيُستقبل من javascript data: "some data" ## البيانات التي سترسل إلى الحدث event: add## تعريف حدث آخر data: "some data" data: "some data"## إرسال بيانات بدون حدث مُعرف id: 3 ## غير مهم وغير مطلوب بشكل ضروري يُستخدم لتحديد مُعرف lastEventId للرسائلالأمثلة مكتوبة بلغة php ويُمكنك فعلها في أي لغة ويب أخرى هذا تطبيق بسيط لجلب الساعة من الخادم لحظة بلحظة. ملف server.php ( الذي سيعالج طلبات SSE احفظه بجوار واجهة المستخدم أو أي مكان آخر مع تغير المسار في شفرة javascript ): <?php set_time_limit(0);// تحديد أقصى مدة للطلب غير نهائية لان هناك خوادم تقوم بتحديدها $time = 1;// متغير بقيمة الوقت الذي سيرسل فيه تحديث جديد header("Content-Type: text/event-stream");// إرسال ترويسة أن الصفحة هي محتوى sse header('Cache-Control: no-cache'); // لمنع حفظ الكاش للصفحة while(true){// تكرار /* date("s-i-h") تُخرج الوقت على التَنسيق ساعة-دقيقة-ثانية PHP_EOL تعني سطر جديد يُمكن إستخدام \n لكن سَتختلف من نظام تَشغيل الى آخر */ echo "data: ".date("s-i-h").PHP_EOL; echo PHP_EOL; // كتابة نص جديد آخر للتفريق بين التنبيهات // إرسال المحتوى الذي كتب دون الإنتظار إلى انتهاء الصفحة ob_flush(); flush(); // إيقاف الاسكربت إلى المدة التي تم تعريفها في المتغير time لمدة ثانية أن لم تَقف الاسكربت سَيعمل بدون توقف sleep($time); } ?>إن فتحت الصفحة ستجد أنها تقوم بإرسال الساعة الحالية كل ثانية. الآن الواجهة التي ستتعامل معها: <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> </head> <body style="direction: rtl"> <label>الساعة الآن : <span>.......</span></label> <script type="text/javascript"> if(!EventSource) alert("مٌتصفحك لايَدعم SSE :("); else{ SSE = new EventSource("server.php"); // انشاء الطلب SSE.onmessage = function(ms){ // عند وجود رسالة جديدة document.querySelector("span").innerHTML = ms.data;// استبدال النص بالجديد } SSE.onerror = function(){//عند وجود خطأ alert("يوجد خطأ في الاتصال ..."); mkcon(); // إعادة الطلب } } </script> </body> </html> الأحداث وطرق انشاءهايُمكن في SSE إنشاء أحداث مخصصة لكي تَكون الشفرة أكثر ذكاءا، مثلًا إرسال حدث لحذف عنصر وحدث لإنشاء عنصر آخر. مثال: تعريف مصفوفة بإسم الألوان أحمر أخضر أزرق. $colors = array("red", "green", "blue");ثم تعريف اسم الحدث بإسم عشوائي من هذه الألوان: $colors[rand(0,2)];ليكون النص الكامل لملف server.php: <?php set_time_limit(0);// تحديد أقصى مدة للطلب غير نهائية لان هناك خوادم تقوم بتحديدها $time = 1;// متغير بقيمة الوقت الذي سيرسل فيه تحديث جديد header("Content-Type: text/event-stream");// إرسال ترويسة أن الصفحة هي محتوى sse $colors = array( // مصفوفة الألوان من النوع الرقمي "red", "green", "blue" ); while(true){// تكرار /* date("s-i-h") تُخرج الوقت على التَنسيق ساعة-دقيقة-ثانية PHP_EOL تعني سطر جديد يُمكن إستخدام \n لكن سَتختلف من نظام تَشغيل إلى آخر */ echo "data: ".date("s-i-h").PHP_EOL; echo "event: ". $colors[rand(0,2)] // توليد لون عشوائي بإسم الحدث .PHP_EOL; echo PHP_EOL; // كتابة نص جديد آخر للتفريق بين التنبيهات // إرسال المحتوى وعدم الإنتظار إلى إكتمال الطلب إلى النهاية ob_flush(); flush(); // إيقاف الاسكربت إلى المدة التي تم تعريفها في المتغير time لمدة ثانية أن لم تَقف الاسكربت سَيعمل بدون توقف sleep($time); } ?>وفي واجهة المستخدم إنشاء بعض العناوين: <style> .red{ color: red } .blue{ color: blue } .green{ color: green } </style> <label class="red"> ....... </label> <label class="green"> ....... </label> <label class="blue"> ....... </label>الآن إستقبال الأحداث: SSE.addEventListener("red",function(ms){ //اللّون الأحمر red اسم الحدث الذي عرف من php document.querySelector(".red").innerHTML = ms.data; });لتصبح شفرة الواجهة الكاملة: <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <style> .red{ color: red } .blue{ color: blue } .green{ color: green } </style> </head> <body> <label class="red"> ....... </label> <label class="green"> ....... </label> <label class="blue"> ....... </label> <script type="text/javascript"> if(!EventSource) alert("مٌتصفحك لايَدعم SSE :("); else{ function mkcon(){ SSE = new EventSource("server.php"); // إنشاء الطلب SSE.addEventListener("red",function(ms){ //اللون الأحمر document.querySelector(".red").innerHTML = ms.data; }); SSE.addEventListener("green",function(ms){ //اللون الأخضر document.querySelector(".green").innerHTML = ms.data; }); SSE.addEventListener("blue",function(ms){ //اللون الأزرق document.querySelector(".blue").innerHTML = ms.data; }); SSE.onerror = function(){//عند وجود خطأ alert("يوجد خطأ في الاتصال ..."); mkcon(); // اعادة الطلب } } mkcon(); } </script> </body> </html>ستجد أن أحدى الساعات من الثلاثة تزيد بشكل عشوائي كل ثانية لكن بالطبع هذا مثال، لن تقوم بإستخدام هذه التقنية في هذا. إستخدام بيانات jsonإن أردت إرسال مقال بإستخدام SSE فستحتاج إلى إرسال العنوان والمحتوى ولن تُرسل حدثين لذلك يُمكنك إستخدام json كما يلي: { "title": "العنوان", "content": "المحتوى" }اذًا سَترسل هذه البيانات: data: {"title": "ألعنوان","content":"ألمحتوى"}ثم سيكون إستقبال البيانات من خلال javascript بهذا الشكل: SSE.onmessage = function(ms){ var JsonData = JSON.parse(ms.data); // قراءة نص json alert(JsonData.title + "\n" + JsonData.content); }مثال عملي عن إرسال بيانات json بتوقيت السيرفر المحلي: ملف server.php <?php set_time_limit(0);//تُحدد أقصى مدة للطلب غير نهائية لان هناك خوادم تقوم بتحديدها $time = 1;// متغير بقيمة الوقت الذي سيرسل فيه تحديث جديد header("Content-Type: text/event-stream");// إرسال ترويسة أن الصفحة هي محتوى sse while(true){// تكرار /* date("s-i-h") تُخرج الوقت على التَنسيق ساعة-دقيقة-ثانية PHP_EOL تعني سطر جديد يُمكن إستخدام \n لكن سَتختلف من نظام تَشغيل الى آخر */ echo 'data: '. /** مصفوفة بالوقت. دالة json_encode تقوم بتحويل البيانات الى ترميز json */ json_encode(array( "hour" => date("h"), // الساعة "minute" => date("i"), // الدقيقة "second" => date("s") // الثانية )) .PHP_EOL; echo PHP_EOL; // كتابة نص جديد آخر للتفريق بين التنبيهات // إرسال المحتوى وعدم الإنتظار إلى إكتمال الطلب إلى النهاية ob_flush(); flush(); // إيقاف الاسكربت إلى المدة التي تم تعريفها في ألمتغير time لمدة ثانية ان لم تَقف الاسكربت سَيعمل بدون توقف sleep($time); } ?>وملف الواجهة: <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> </head> <body style="direction: rtl"> <p>الساعة الآن <b id="hour">0</b> و <b id="minute">0</b> دقيقة و <b id="second">0</b> ثانية.</p> <script type="text/javascript"> if(!EventSource) alert("مٌتصفحك لايَدعم SSE :("); else{ function mkcon(){ SSE = new EventSource("server.php"); // إنشاء الطلب SSE.onmessage = function(ms){ var JsonData = JSON.parse(ms.data); // قراءة نص json document.querySelector("#hour").innerHTML = JsonData.hour; // إستبدال الساعة document.querySelector("#minute").innerHTML = JsonData.minute;// إستبدال الدقائق document.querySelector("#second").innerHTML = JsonData.second;// إستبدال الثواني } SSE.onerror = function(){//عند وجود خطأ alert("يوجد خطأ في الاتصال ..."); mkcon(); // إعادة الطلب } } mkcon(); } </script> </body> </html>في هذا المثال والذي سبقه لن تَحتاج إلى إستحدام SSE ويمكن عَمل ذلك بإستخدام javascript بسهولة. تطبيق على ما شُرح: بناء تطبيق تواصللم تُستخدم قاعدة بيانات لتسهيل تشغيل السكربت (لكن بالطبع إستخدام قاعدة بيانات أفضل) سيولد لون لكل مستخدم وسيحفظ في cookies للتعرف عليه في الرسائل، ستحفظ البيانات في ملف ChatData.json على هذا الشكل (كان يُمكن إستخدام قاعدة بيانات sqlite): [// مصفوفة الرسائل { "author": "rgb(54,48,98)", // لون السمتخدم "message": "رسالة", }, { "author": "rgb(54,48,98)", // لون السمتخدم "message": "رسالة 2", }// ....,...,.. ]تصميم الواجهةهذه هيكلة الصفحة: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>تجربة</title> <meta name="viewport" content="width=device-width"> </head> <body> <div class="messages"> </div> <div class="form"> <div class="author me"></div> <textarea placeholder="كتابة رسالة"></textarea> <button id="send">أرسل</button> </div> </body> </html>إضافة الحيوية للصفحة: body,html{ width: 100%;/** جعل عرض الصفحة بكامل الشاشة **/ direction: rtl; height: 100%; background: #f0f0f0;/** لون الخلفية **/ padding: 0; margin: 0 } /** جعل حجم بعض العناصر يحسب بالحواشي الخارجة **/ .messages, .messages > .message, .message > .content, .form, .form > textarea{ box-sizing: border-box } /** الرسائل **/ .messages{ padding: 40px; /** عمل فراغ بقيمة 40px من كل جهة **/ height: 80%; overflow-y: auto/** جعل الرسائل الزائدة تتحول الى اسكرول **/ } /** عناصر لن تكون بكامل عرض الحافظة **/ .message > .author, .message > .content{ display: inline-block;/** لن يأخذ العنصر كامل العرض **/ vertical-align: top /** العنصر سيكون في محاذة الأعلى **/ } /** هوية المستخدم **/ .message > .author{ width: 64px; height: 64px; background: #ddd; /** اللون الافتراضي سيغير بعد ذلك **/ border-radius: 100% /** تدوير العنصر **/ } /** جسم الرسالة **/ .message > .content{ background: white; box-shadow: 0 0 1px #ccc; /** تأثير الظل **/ padding: 20px; margin-right: 30px; /** لجعل الصفحة أكثر إستجابة على الشاشات المختلفة **/ width: 100%; max-width: 550px } /** عناصرالتحكم لإرسال رسالة **/ .form{ padding: 10px; height: 20% } /** صندوق النصوص **/ .form > textarea{ width: 100%; max-width: 678px; height: 100% } /** زر الإرسال **/ .form > button{ background: rgb(31, 158, 238); padding: 4px 20px; border: 0; cursor: pointer; /** مؤشر الفارة **/ color: white; border-radius: 3px /** عمل بعض الحواف **/ } .form > button:active{ background: rgb(31, 158, 200) } /** تصميم الجوال إصلاح بعض العلل **/ @media screen and (max-width:640px){ .messages{ padding: 0 } .message > .content{ margin: 0; margin-bottom: 20px } }إستقبال الرسائلSEE لاتدعم إرسال البيانات إلى الخادوم لذا سَتستخدم ajax لذلك. هذه شفرة بسيطة لإرسال الرسائل إلى الخادوم (يُضاف بين الوسم script في أخر الصفحة قبل </body> أو إن كنت ستضيفه الى head، يجب أن يكون في دالة: window.onload = function(){ //هنا }; document.querySelector("#send").addEventListener("click",function(){// عند الضغط على زر إرسال var mss = document.querySelector(".form > textarea").value; // محتوى الرسالة if(mss && mss.length){// التأكد من أن الرسالة مكتوبة var ajax = new XMLHttpRequest(); ajax.onreadystatechange = function(){ if(this.readyState == 4) alert("تم إرسال الرسالة"); } ajax.open("get","server.php"); // نوع الطلب ومسار الإرسال ajax.send("?ms="+mss);// إرسال الرسالة } });ستحدث مشكلة إن أرسل المستخدم أكثر من رسالة في وقت واحد، من الممكن أن تصل رسالة قبل الأخرى. لذا سنستعمل قائمة بالرسائل المرسلة ونقوم بمعالجتها واحدة ثم الأخرى مع تحسين تجربة المستخدم قليلًا: messagesList = []; // مصفوفة بالرسائل InProgress = false;// يوجد طلب يُعالج أم لا function sendFirstMessage(){ if(!messagesList.length || InProgress) // لايوجد رسائل أو هناك رسالة تُعالج return false; var ajax = new XMLHttpRequest(); ajax.onreadystatechange = function(){ // الرسالة ارسلت if(this.readyState == 4){ messagesList.splice(0, 1);// حذف الرسالة المرسلة اول رسالة InProgress = false; // تم الإنتهاء من العمل sendFirstMessage(); // إرسال الرسالة الأخرى } } ajax.onerror = function(){ alert("خطأ في الإتصال جاري المحاولة مجددًا"); sendFirstMessage(); } InProgress = true; ajax.open("get","send.php?message="+messagesList[0]); // نوع الطلب ومسار الإرسال ajax.send();// إرسال الرسالة } document.querySelector("#send").addEventListener("click",function(){// عند الضغط على زر إرسال var mss = document.querySelector(".form > textarea").value; // محتوى الرسالة if(mss && mss.length){// التأكد من أن هناك رسالة مكتوبة messagesList.push(mss); // إضافة رسالة إلى المصفوفة sendFirstMessage(); // إرسال الرسالة الأولى document.querySelector(".form > textarea").value = "";// افراغ حقل إرسال الرسائل } });ملف الخادوم الذي سيستقبل الرسائل، لن تكتب [] في الملف لتسهيل الكتابة في نهايته دون قِرائته: ملف send.php (الذي سيستقبل الرسائل) <?php if(empty($_GET["message"])) die(); /* التأكد من المستخدم */ if(!isset($_COOKIE["author"]) || !preg_match("/^rgb\([0-9]{0,3}\,[0-9]{0,3}\,[0-9]{0,3}\)$/i",$_COOKIE["author"])){ // توليد لون عشوائي أن لم يُعرف من قبل $R = rand(0,250); // اللّون الأحر $G = rand(0,250); // اللّون الأخضر $B = rand(0,250); // اللّون الأزرق setcookie("author","rgb($R,$G,$B)"); // نمط الألوان rgb } $message = str_replace(array("<",">"),array("<",">"),$_GET["message"]); // الوقاية من ثغرة xss $data = array( // البيانات التي ستكتب "content" => $message, "author" => $_COOKIE["author"] ); $FILE = fopen("./ChatData.json","a"); //فتح الملف للكتابة فيه $star = (!filesize("./ChatData.json")) ? "" :",";// إضافة فاصلة أن تم الكتابة بالملف من قبل fwrite($FILE,$star.json_encode($data)); // كتابة بيانات json في الملف fclose($FILE); ?>ملاحظة: سبب إستخدام الألوان هو لكي تتمكن من التعامل مع المستخدمين وحفظ البيانات في قاعدة البيانات للمستخدم. ملف server.php (الذي سيراقب تغير الملف ويرسل التحديث) <?php set_time_limit(0); // لايوجد اقصى مدة للطلب header("Content-Type: text/event-stream"); $viewd = 0; // عدد الرسائل التي عرضت لمنع إرسال الرسالة اكثر من مرة $lastFileSize = 0; // حجم الملف عندما يتغير فان هناك رسالة ارسلت while(true){ if(filesize("./ChatData.json") >= $lastFileSize){// التحديث فقط عند تغير حجم الملف $messages = json_decode("[".file_get_contents("./ChatData.json")."]"); // قراءة الرسائل for($i=$viewd;$i<=sizeof($messages)-1;$i++){ // حلقة تكرار من أخر رسالة مُرسلة echo "data: ".json_encode($messages[$i]).PHP_EOL; echo PHP_EOL; } $viewd = sizeof($messages); // تحديث عدد الرسائل المرسلة $lastFileSize = filesize("./ChatData.json"); // تحديث مُتغير أخر حجم للملف } ob_flush(); flush(); sleep(1); } ?>ثم في واجهة المستخدم شفرة javascript لجلب التحديثات: sse = new EventSource("server.php"); // انشاء الاتصال sse.onmessage = function(ms){ var data = JSON.parse(ms.data); // قراءة بيانات json var message = document.createElement("div"); // عنصر الرسائل message.setAttribute("class","message"); // اضافة كلاسس message var author = document.createElement("div"); // عنصر مُرسل الرسائل author.setAttribute("class","author"); // اضافة كلاسس author author.style.backgroundColor = data.author; // تغير لون الخلفية لمعرف المستخدم message.appendChild(author); // اضافة عنصر مُعرف مُعرف المستخدم الى عنصر الرسالة var content = document.createElement("p"); // محتوى الرسالة content.setAttribute("class","content"); content.innerHTML = data.content;// محتوى الرسالة message.appendChild(content); messages = document.querySelector(".messages"); messages.appendChild(message);// اضافة عنصر الرسالة الى حافظة الرسائل messages.scrollTop = messages.scrollHeight; // انزال الاسكرول الى الاعلى } ليعمل على انترنت اكسبلور يجب تضمين ملف javscript هذا في الـ header: <script src="https://rawgithub.com/remy/polyfills/master/EventSource.js"></script> يجب ان يكون العمل مُطابق لهذا: إن لم يعمل معك بشكل صحيح يُمكنك تحميل ملفات الأمثلة وقراءتها حتى تتمكن من معرفة كيف يعمل هذا المثال . الخاتمةيُمكنك رؤية المزيد من المصادر لمعرفة المزيد حول SSE: eventsource من طرف W3C.Server Sent Events من طرف مدونة موزيلا للمبرمجين.