نشر ومشاركة قالب في Node.js

0 الأسهم
0
0
0
0

مقدمة

نمط النشر/الاشتراك هو نمط مراسلة متعدد الاستخدامات أحادي الاتجاه، حيث يُنتج الناشر بيانات/رسائل، ويشترك المشتركون لتلقي أنواع محددة من الرسائل. يمكن تنفيذه باستخدام بنية نظير إلى نظير أو وسيط رسائل لتسهيل التواصل.


تُظهر الصورة أعلاه نموذج النشر/الاشتراك من نظير إلى نظير، حيث يُرسل الناشر رسائله مباشرةً إلى المشتركين دون وسطاء. يحتاج المشتركون إلى معرفة عنوان الناشر أو نقطة النهاية لاستلام الرسالة.


في الصورة أعلاه، يستخدم نموذج النشر/الاشتراك وسيط رسائل كمركز رئيسي لإرسال الرسائل بين الناشرين والمشتركين. يتوسط الوسيط تبادل الرسائل ويوزعها من الناشرين إلى المشتركين. تشترك عُقد المشتركين مباشرةً مع الوسيط بدلاً من الناشر. يُحسّن وجود وسيط عزل عُقد النظام، حيث يتفاعل الناشر والمشتركون فقط مع الوسيط. في هذا البرنامج التعليمي، ستُنشئ تطبيق دردشة آنية لتوضيح هذا النمط بشكل أكبر.

المتطلبات الأساسية
  • تم تثبيت Node.js (الإصدار >= 12) على نظام التشغيل الخاص بك.
  • محرر أكواد مثل VSCode
  • تم تثبيت Redis على جهازك.
  • فهم أساسي لـ HTML و DOM و VanillaJS و WebSocket.

الخطوة 1 – التنفيذ من جانب الخادم

لبدء تشغيل جانب الخادم، نقوم بتهيئة تطبيق Nodejs أساسي باستخدام الأمر الأولي:

npm init -y

يؤدي الأمر أعلاه إلى إنشاء ملف package.json افتراضيًا.

بعد ذلك، نقوم بتثبيت حزمة التبعية WebSocket (ws)، والتي تعد مطلوبة طوال عملية البناء هذه:

npm install ws

سيكون التنفيذ من جانب الخادم عبارة عن تطبيق دردشة بسيط. سنتبع سير العمل التالي:

  1. إعداد الخادم
  2. قراءة ملف HTML لعرضه في المتصفح
  3. إعداد اتصال WebSocket.
إعداد الخادم

قم بإنشاء ملف يسمى app.js في الدليل الخاص بك ثم ضع الكود التالي بداخله:

const http = require("http");
const server = http.createServer((req, res) => {
res.end("Hello Chat App");
});
const PORT = 3459;
server.listen(PORT, () => {
console.log(`Server up and running on port ${PORT}`);
});

طريقة إنشاء خادم في الوحدة http محلي نود.جي اس سيتم استخدامه لبدء تشغيل الخادم. يتم تحديد المنفذ الذي سيستمع إليه الخادم للطلبات، ويتم استدعاء طريقة الاستماع على مثيل الخادم المُنشأ للاستماع للطلبات الواردة على المنفذ المحدد.

طلب تطبيق node.js قم بتشغيله في محطتك ويجب أن تحصل على استجابة مثل هذه:

OutputServer is up and running on port 3459

إذا طلبت هذا المنفذ في متصفحك، فيجب أن تحصل على شيء مثل هذا كرد فعل لك:


قراءة ملف HTML لعرضه في المتصفح

قم بإنشاء ملف يسمى index.html في المجلد الرئيسي وانسخ الكود التالي:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<p>Serving HTML file</p>
</body>
</html>

هذا ملف HTML أساسي يُظهر رسالة Hello. الآن، علينا قراءة هذا الملف وإظهاره كاستجابة عند إرسال طلب HTTP إلى خادمنا.

// app.js
const server = http.createServer((req, res) => {
const htmlFilePath = path.join(__dirname, "index.html");
fs.readFile(htmlFilePath, (err, data) => {
if (err) {
res.writeHead(500);
res.end("Error occured while reading file");
}
res.writeHead(200, { "Content-Type": "text/html" });
res.end(data);
});
});

هنا، نستخدم وحدة المسار الداخلي والدالة ينضم نستخدمها لربط أجزاء المسار معًا. ثم نستخدم الدالة قراءة الملف لقراءة الملف index.html يُستخدم بشكل غير متزامن. يتطلب وسيطتين: مسار الملف المراد قراءته، وقراءة عكسية. رمز الحالة 500 يتم إرسال رأس استجابة ورسالة خطأ إلى العميل. في حال قراءة البيانات بنجاح، يُعاد رمز حالة النجاح. 200 نرسل رأس الاستجابة ونرسل بيانات الاستجابة إلى العميل، وهي في هذه الحالة محتوى الملف. إذا لم يُحدد أي ترميز، مثل ترميز UTF-8، فسيتم إرجاع المخزن المؤقت الخام. وإلا، فسيتم حفظ الملف. HTML لقد تم إرجاعه.

قم بإرسال طلب إلى الخادم في متصفحك، ويجب أن تحصل على هذا:


إعداد اتصال WebSocket
// app.js
const webSocketServer = new WebSocket.Server({ server });
webSocketServer.on("connection", (client) => {
console.log("successfully connected to the client");
client.on("message", (streamMessage) => {
console.log("message", streamMessage);
distributeClientMessages(streamMessage);
});
});
const distributeClientMessages = (message) => {
for (const client of webSocketServer.clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
}
};

في سطر التعليمات البرمجية السابق، قمنا بإنشاء خادم WebSocket جديد يسمى خادم ويب سوكت نقوم بإنشائه وإرساله إلى الخادم. بروتوكول نقل النص التشعبي (HTTP) نتصل بمنفذنا الحالي. هذا يسمح لنا بمعالجة طلبات HTTP القياسية واتصالات WebSocket على منفذ واحد 3459.

يتم تشغيل حدث الاتصال on() عند إنشاء اتصال ناجح بـ WebSocket. العميل في الدالة أتصل مرة أخرى يُمثِّل كائن اتصال WebSocket اتصالاً بالعميل. ويُستخدم لإرسال واستقبال الرسائل والاستماع إلى أحداث مثل رسائل العميل.

تُستخدم دالة distributeClientMessages هنا لإرسال الرسائل الواردة إلى جميع العملاء المتصلين. تأخذ هذه الدالة وسيطة رسالة وتعيدها إلى العملاء المتصلين بخادمنا. ثم تتحقق من حالة اتصال كل عميل (readyState === WebSocket.OPEN). هذا لضمان أن الخادم يرسل الرسائل فقط إلى العملاء الذين لديهم اتصالات مفتوحة. إذا كان اتصال العميل مفتوحًا، يرسل الخادم الرسالة إلى ذلك العميل باستخدام دالة client.send(message).

الخطوة 2 – التنفيذ من جانب العميل

لتشغيل جانب العميل، الملف index.html نحن نغير أنفسنا قليلا.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<p>Pub/Sub Pattern with Chat Messaging</p>
<div id="messageContainer"></div>
<form id="messageForm">
<form id="messageForm">
<input
type="text"
id="messageText"
placeholder="Send a message"
style="
padding: 10px;
margin: 5px;
border-radius: 5px;
border: 1px solid #ccc;
outline: none;
"
onfocus="this.style.borderColor='#007bff';"
onblur="this.style.borderColor='#ccc';"
/>
<input
type="button"
value="Send Message"
style="
padding: 10px;
margin: 5px;
border-radius: 5px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
"
onmouseover="this.style.backgroundColor='#0056b3';"
onmouseout="this.style.backgroundColor='#007bff';"
/>
</form>
</form>
<script>
const url = window.location.host;
const socket = new WebSocket(`ws://${url}`);
</script>
</body>
</html>

في هذا المقطع البرمجي، أضفنا عنصر نموذج يحتوي على مدخل وزر لإرسال رسالة. تبدأ اتصالات WebSocket بواسطة العملاء، وللتواصل مع خادم WebSocket الذي قمنا بإعداده مبدئيًا، نحتاج إلى إنشاء مثيل لكائن WebSocket يحدد ws://url، والذي يحدد الخادم الذي نريد استخدامه. عند تسجيل الدخول، سيحتوي متغير URL على عنوان URL المتصل بالمنفذ 3459، وهو المنفذ الذي يستمع إليه خادمنا.

// app.js
console.log("url", url); // localhost:3459
console.log("socket", socket); // { url: "ws://localhost:3459/", readyState: 0, bufferedAmount: 0, onopen: null, onerror: null, onclose: null, extensions: "", protocol: "", onmessage: null, binaryType: "blob" }

لذا، عندما تقوم بإرسال طلب إلى الخادم في متصفحك، يجب أن ترى ما يلي:


لنقم بتحديث البرنامج النصي الخاص بنا حتى نتمكن من إرسال الرسائل من العميل إلى الخادم واستقبال الرسائل من الخادم.

// index.html
<script>
const url = window.location.host;
const socket = new WebSocket(`ws://${url}`);
const messageContainer = document.getElementById("messageContainer");
socket.onmessage = function (eventMessage) {
eventMessage.data.text().then((text) => {
const messageContent = document.createElement("p");
messageContent.innerHTML = text;
document
.getElementById("messageContainer")
.appendChild(messageContent);
});
};
const form = document.getElementById("messageForm");
form.addEventListener("submit", (event) => {
event.preventDefault();
const message = document.getElementById("messageText").value;
socket.send(message);
document.getElementById("messageText").value = "";
});
</script>

كما ذكرنا سابقًا، نسترجع عنوان URL الذي يرسل طلبًا إلى خادمنا من العميل (المتصفح) وننشئ مثيلًا جديدًا لكائن WebSocket باستخدام عنوان URL. ثم، بالنقر على الزر إرسال رسالة نُنشئ حدثًا على عنصر النموذج. يُستخرج النص الذي يُدخله المستخدم في واجهة المستخدم من عنصر الإدخال، وتُستدعى دالة الإرسال على مثيل المقبس لإرسال الرسالة إلى الخادم.

حدث رسالة على يُستدعى على كائن المقبس، ويُفعّل عند استلام رسالة من الخادم. يُستخدم لتحديث واجهة المستخدم للرسالة المُستقبَلة. المعلمة رسالة الحدث في الوظيفة أتصل مرة أخرى تحتوي على البيانات (الرسالة) المُرسلة من الخادم، والتي تُعاد ككائن. بعد ذلك، تُستخدم دالة text() على بيانات الكائن، والتي تُرجع وعدًا، ويتم حلها باستخدام دالة then() للحصول على النص الفعلي من الخادم.

لنختبر ما لدينا. ابدأ تشغيل الخادم بتشغيل

node app.js

ثم افتح علامتي تبويب مختلفتين في المتصفح، http://localhost:3459/ افتح وحاول إرسال الرسائل بين علامات التبويب للاختبار:


الخطوة 3 - توسيع نطاق التطبيق

لنفترض أن تطبيقنا بدأ بالنمو، ونحاول توسيع نطاقه من خلال استخدام عدة نسخ من خادم الدردشة. ما نريد تحقيقه هو أن يتمكن مستخدمان مختلفان متصلان بخادمين مختلفين من إرسال الرسائل النصية لبعضهما البعض بنجاح. حاليًا، لدينا خادم واحد فقط، وإذا طلبنا خادمًا آخر، على سبيل المثال http://localhost:3460/رسائل الخادم على المنفذ 3459 لن يكون لدينا. أي أن المستخدمين المتصلين فقط هم من سيتمكنون من الوصول إلى 3460 يمكنهم الدردشة مع أنفسهم. يعمل التطبيق الحالي بطريقة أنه عند إرسال رسالة دردشة على خادمنا قيد التشغيل، تُوزّع الرسالة محليًا فقط على العملاء المتصلين بذلك الخادم، كما هو موضح عند http://localhost:3459/ نفتحه على متصفحين مختلفين. الآن، لنرَ كيف يمكننا دمج خادمين مختلفين للتواصل.

الخطوة 4 – Redis كوسيط رسائل

Redis هو مخزن بيانات سريع ومرن في الذاكرة. يُستخدم غالبًا كقاعدة بيانات أو خادم تخزين مؤقت لتخزين البيانات. بالإضافة إلى ذلك، يُمكن استخدامه لتنفيذ نمط مركزي لتبادل الرسائل Pub/Sub. بفضل سرعته ومرونته، أصبح Redis خيارًا شائعًا جدًا لمشاركة البيانات في الأنظمة الموزعة.

الهدف هنا هو دمج خوادم الدردشة لدينا باستخدام Redis كوسيط رسائل. ينشر كل خادم كل رسالة مستلمة من العميل (المتصفح) إلى وسيط الرسائل في آنٍ واحد. ويشترك وسيط الرسائل في كل رسالة مرسلة من خوادمه.

دعونا نرفع الملف تطبيق.js دعونا نغير أنفسنا:

//app.js
const http = require("http");
const fs = require("fs");
const path = require("path");
const WebSocket = require("ws");
const Redis = require("ioredis");
const redisPublisher = new Redis();
const redisSubscriber = new Redis();
const server = http.createServer((req, res) => {
const htmlFilePath = path.join(__dirname, "index.html");
fs.readFile(htmlFilePath, (err, data) => {
if (err) {
res.writeHead(500);
res.end("Error occured while reading file");
}
res.writeHead(200, { "Content-Type": "text/html" });
res.end(data);
});
});
const webSocketServer = new WebSocket.Server({ server });
webSocketServer.on("connection", (client) => {
console.log("succesfully connected to the client");
client.on("message", (streamMessage) => {
redisPublisher.publish("chat_messages", streamMessage);
});
});
redisSubscriber.subscribe("chat_messages");
console.log("sub", redisSubscriber.subscribe("messages"));
redisSubscriber.on("message", (channel, message) => {
console.log("redis", channel, message);
for (const client of webSocketServer.clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
}
});
const PORT = process.argv[2] || 3459;
server.listen(PORT, () => {
console.log(`Server up and running on port ${PORT}`);
});

هنا، نستخدم إمكانيات النشر/المشاركة. ريديس نستخدم حالتي اتصال مختلفتين، واحدة لنشر الرسائل والأخرى للاشتراك في قناة. عند إرسال رسالة من العميل، تُنشر على قناة Redis تُسمى redisPublisher باستخدام دالة الناشر في حالة redisPublisher. ""رسائل الدردشة"" ننشر. تُستدعى دالة الاشتراك في مثيل redisSubscribe للاشتراك في نفس قناة chat_message. عند نشر رسالة على هذه القناة، يُفعّل مُستمع الحدث redisSubscriber.on. يُكرّر مُستمع الحدث هذا على جميع عملاء WebSocket المتصلين حاليًا، ويُرسل الرسالة المُستلمة إلى كل عميل. هذا لضمان استلام جميع المستخدمين الآخرين المتصلين بكل مثيل خادم لهذه الرسالة فورًا عند إرسال أي مستخدم لرسالة.

إذا كنت تقوم بإعداد خادمين مختلفين، قل:

node app.js 3459
node app.js 3460

عند إرسال نص المحادثة على مثيل، يمكننا الآن بث الرسائل عبر خوادمنا المتصلة بدلاً من خادم واحد محدد. يمكنك القيام بذلك بتشغيل http://localhost:3459/ و http://localhost:3460/ قم باختبارها ثم قم بإرسال الدردشات بينهما وشاهد الرسائل التي يتم بثها في الوقت الفعلي عبر الخادمين.

يمكنك عرض الرسائل المنشورة في قناة من ريديس-كلي قم بمراقبة القناة والاشتراك فيها أيضًا لتلقي الرسائل بانتظام:

طلب ريديس-كلي اركض. ثم أدخل شاشة ارجع إلى متصفحك وابدأ الدردشة. في جهازك، سترى ما يلي، بافتراض أنك أرسلت رسالة دردشة Wow:


لعرض الرسائل المنشورة المشتركة، استخدم نفس الأمر ريديس-كلي اركض و اشترك في اسم القناة أدخل اسم القناة الخاصة بنا. رسائل الدردشة إذا قمت بإرسال رسالة نصية، فيجب أن يكون لديك شيء مثل هذا في محطتك: رائع من المتصفح:


الآن، يمكننا تشغيل مثيلات متعددة من خادمنا على منافذ مختلفة أو حتى أجهزة مختلفة، وطالما أنها مشتركة في نفس قناة Redis، فيمكنها تلقي الرسائل وبثها إلى جميع العملاء المتصلين، مما يضمن أن يتمكن المستخدمون من الدردشة بسلاسة عبر المثيلات.

هل تذكرون في المقدمة أننا تحدثنا عن تطبيق نمط النشر/الاشتراك باستخدام وسيط رسائل؟ هذا المثال يُلخص الأمر تمامًا.


في الشكل أعلاه، يتصل عميلان مختلفان (متصفحان) بخوادم الدردشة. خادما الدردشة ليسا متصلين مباشرةً، بل من خلال نسخة Redis. هذا يعني أنهما يتعاملان مع اتصالات العميل بشكل مستقل، ويتشاركان المعلومات (رسائل الدردشة) عبر وسيط مشترك (Redis). يتصل كل خادم دردشة أعلاه بـ Redis. يُستخدم هذا الاتصال لنشر الرسائل على Redis والاشتراك في قنوات Redis لتلقي الرسائل. عندما يرسل مستخدم رسالة، ينشرها خادم الدردشة على القناة المحددة في Redis.

عندما يستقبل Redis رسالة منشورة، يُبثّها إلى جميع خوادم الدردشة المشاركة. يُرسل كل خادم رسالة إلى جميع العملاء المتصلين، مما يضمن استلام كل مستخدم للرسائل التي أرسلها، بغض النظر عن الخادم المتصل به.

تتيح لنا هذه البنية توسيع نطاق تطبيق الدردشة أفقيًا بإضافة المزيد من مثيلات الخادم حسب الحاجة. بفضل إمكانيات نظام النشر/الاشتراك Redis، الذي يضمن توزيعًا موحدًا للرسائل عبر جميع المثيلات، يمكن لكل مثيل إدارة مجموعته الخاصة من العملاء المتصلين. هذا الإعداد فعال للتعامل مع عدد كبير من المستخدمين المتزامنين ويضمن توافر تطبيقك.

نتيجة

في هذا البرنامج التعليمي، تعلمنا نمط النشر/الاشتراك أثناء إنشاء تطبيق دردشة بسيط لتوضيح هذا النمط، باستخدام Redis كوسيط رسائل. الخطوة التالية هي تعلم كيفية تنفيذ نظام مراسلة نظير إلى نظير في الحالات التي قد لا يكون فيها وسيط الرسائل هو الحل الأمثل، على سبيل المثال، في الأنظمة الموزعة المعقدة حيث لا يكون وجود نقطة فشل واحدة (الوسيط) خيارًا متاحًا.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

قد يعجبك أيضاً