يا أهلاً وسهلاً فيكم يا جماعة، معكم أبو عمر.
خلوني أحكيلكم قصة صارت معي قبل كم سنة، قصة علّمتني درس قاسي ومهم في عالم البرمجة. كنت وقتها شغال على مشروع داشبورد (لوحة تحكم) لمتجر إلكتروني، والمطلوب كان إنه لما يجي طلب جديد، يظهر إشعار فوري على شاشة مدير المتجر. أنا، بحماسي وقلة خبرتي وقتها، فكرت بأبسط حل ممكن: “ليش ما أخلي التطبيق اللي عند المدير يسأل الخادم كل ثانية: ‘يا خادم، في إشي جديد؟’“.
وبالفعل، كتبت الكود. في البداية، ومع عدد قليل من المستخدمين، كان كل شيء تمام والوضع “عال العال”. لكن الكارثة بلشت تبين لما كبر المتجر وزاد عدد فروعه ومدراء الأقسام اللي بستخدموا الداشبورد. فجأة، صار عندي 50 مستخدم، كل واحد منهم تطبيقه بسأل الخادم كل ثانية! يعني 50 طلب في الثانية، 3000 طلب في الدقيقة! الخادم المسكين بلش يصرخ ويستنجد، استهلاك المعالج (CPU) وصل السما، وقواعد البيانات على وشك الإغلاق، وفاتورة الاستضافة… آه يا قلبي على الفاتورة.
كنت عايش في كابوس اسمه “جحيم الاستطلاع المستمر” أو الـ Polling. كنت أصحى من النوم مفزوع على إشعارات من خدمة المراقبة بتحكيلي إنه الخادم “مات”. في تلك اللحظة، أدركت أن طريقتي كانت غبية ومدمرة. وهنا بدأت رحلة البحث عن حل أنقذني وأنقذ مشروعي، حل اسمه “خطافات الويب” أو Webhooks.
ما هو الاستطلاع (Polling) ولماذا هو جحيم حقيقي؟
قبل ما نحكي عن الحل، خلينا نفهم أصل المشكلة. الأسلوب اللي اتبعته يسمى تقنياً “الاستطلاع” أو Polling. ببساطة، هو أن يقوم العميل (Client)، مثل تطبيق الويب أو الموبايل، بسؤال الخادم (Server) بشكل متكرر على فترات زمنية ثابتة لمعرفة ما إذا كان هناك أي بيانات جديدة.
// مثال بسيط جداً على الـ Polling باستخدام JavaScript
// هذا الكود كان كارثتي!
setInterval(async () => {
try {
const response = await fetch('/api/orders/latest');
const data = await response.json();
if (data.newOrder) {
// إذا كان هناك طلب جديد، قم بتحديث الواجهة
updateDashboard(data.newOrder);
} else {
// لا يوجد شيء جديد... مجرد طلب ضائع آخر
console.log('لا يوجد طلبات جديدة. سأسأل مرة أخرى بعد ثانية.');
}
} catch (error) {
console.error('فشل الاتصال بالخادم:', error);
}
}, 1000); // اسأل الخادم كل 1000 ميلي ثانية (كل ثانية)
للوهلة الأولى، يبدو هذا الحل منطقياً، لكنه يخفي وراءه عيوباً قاتلة:
- استنزاف الموارد: تخيل أن 99% من هذه الطلبات تعود بإجابة “لا، لا يوجد شيء جديد”. أنت تستهلك موارد الشبكة، المعالج، وقواعد البيانات على الخادم والعميل من أجل لا شيء. إنه مثل أن تتصل بصديقك كل دقيقة لتسأله “هل وصلت؟” بدلاً من أن تنتظره ليتصل بك عند وصوله.
- التأخير الحتمي (Latency): حتى مع السؤال كل ثانية، هناك دائماً تأخير. لو وصل الطلب الجديد بعد جزء من الثانية من سؤالك الأخير، سيتعين عليك الانتظار 0.9 ثانية أخرى لمعرفته. تقليل الفترة الزمنية (مثلاً إلى 100 ميلي ثانية) يجعل مشكلة استنزاف الموارد أسوأ بعشرة أضعاف.
- صعوبة التوسع (Scalability): هذا النموذج لا يتوسع أبداً. كل مستخدم جديد هو عبء إضافي ثابت على خادمك. 1000 مستخدم يسألون كل ثانية يعني 1000 طلب في الثانية! هذا طريق سريع نحو انهيار الخادم.
الحل السحري: خطافات الويب (Webhooks)
بعد ليالٍ من البحث والتجربة، وجدت المفهوم الذي غير طريقة تفكيري: Webhooks. الفكرة عبقرية في بساطتها.
بدلاً من أن يسأل العميل الخادم “هل هناك جديد؟”، يقوم الخادم هو بإخبار العميل “يا عميل، لقد حدث شيء جديد!” عند وقوع الحدث فعلاً.
هذا يسمى نموذج “الدفع” (Push model) أو البرمجة القائمة على الأحداث (Event-driven). الـ Webhook هو ببساطة عنوان URL (نقطة نهاية أو Endpoint) يوفره تطبيقك (العميل) لتطبيق آخر (الخادم)، ويقول له: “عندما يقع الحدث ‘X’ الذي يهمني، أرسل لي إشعاراً على هذا العنوان”.
لنعد لمثال البيتزا الذي أحب استخدامه:
- الـ Polling: أن تتصل بمطعم البيتزا كل دقيقتين وتسأل “هل جهزت البيتزا؟”.
- الـ Webhook: أن تعطي المطعم رقم هاتفك وتقول له “عندما تجهز البيتزا، اتصل بي”.
النتيجة واحدة (ستحصل على البيتزا)، لكن الطريقة الثانية أكثر كفاءة وراحة للطرفين.
كيف تعمل خطافات الويب بالضبط؟
العملية تسير في خطوات منظمة:
- التسجيل (Registration): تطبيقك (المستقبِل) يرسل طلباً إلى الخادم (المُرسِل) يحتوي على عنوان URL الخاص بالـ Webhook ونوع الحدث الذي يهتم به (مثلاً
new_order). - الحدث (Event): يقع حدث ما على الخادم (مثلاً، عميل جديد يทำการ بطلب).
- الإشعار (Notification): الخادم، الذي لديه الآن قائمة بعناوين الـ Webhooks المسجلة لحدث
new_order، يقوم فوراً بإرسال طلبHTTP POSTإلى كل عنوان مسجل. هذا الطلب يحتوي على “حمولة” (Payload) وهي بيانات بصيغة JSON تصف الحدث الذي وقع. - المعالجة (Processing): تطبيقك يستقبل هذا الطلب على عنوان الـ Webhook، يقرأ البيانات، ويتخذ الإجراء المناسب (مثل تحديث الداشبورد).
الجميل في الأمر أن الاتصال يحدث مرة واحدة فقط عند الحاجة. لا يوجد طلبات ضائعة، لا يوجد استنزاف للموارد، والتحديث شبه فوري.
يلا نشتغل عملي: بناء نظام Webhook بسيط
الكلام النظري جميل، لكن “الشغل العملي بحكي قصة ثانية”. دعونا نبني مثالاً بسيطاً باستخدام Node.js و Express. سيكون لدينا خدمتان:
- خدمة الطلبات (Order Service): هي الخادم الذي تحدث فيه الأحداث (إنشاء طلب جديد)، وهو من سيرسل إشعار الـ Webhook.
- خدمة الإشعارات (Notification Service): هي العميل الذي يستقبل إشعارات الـ Webhook ويعرضها.
الطرف الأول: خدمة الطلبات (مُرسِل الـ Webhook)
هذا الكود يمثل الخادم الذي يدير الطلبات. سنحتاج لمكتبة مثل axios لإرسال طلبات الـ HTTP.
// Order Service - server.js
const express = require('express');
const bodyParser = require('body-parser');
const axios = require('axios');
const app = express();
app.use(bodyParser.json());
// سنخزن عناوين الـ Webhooks المسجلة في مصفوفة بسيطة (في تطبيق حقيقي، استخدم قاعدة بيانات)
const webhooks = [];
// 1. نقطة نهاية لتسجيل Webhook جديد
app.post('/register-webhook', (req, res) => {
const { url } = req.body;
if (!url) {
return res.status(400).send('عنوان URL مطلوب');
}
webhooks.push(url);
console.log(`تم تسجيل Webhook جديد: ${url}`);
res.status(200).send(`تم تسجيل الـ Webhook بنجاح.`);
});
// 2. نقطة نهاية لإنشاء طلب جديد (هنا يقع الحدث)
app.post('/orders', async (req, res) => {
const newOrder = { id: Date.now(), ...req.body };
console.log('تم استلام طلب جديد:', newOrder);
// *** هنا يحدث السحر ***
// قم بإعلام كل الـ Webhooks المسجلة
console.log(`إرسال إشعارات إلى ${webhooks.length} مشترك...`);
const notificationPayload = {
event: 'new_order',
data: newOrder
};
// نستخدم Promise.all لإرسال الإشعارات بالتوازي
const promises = webhooks.map(url =>
axios.post(url, notificationPayload).catch(err => {
// مهم: سجل الأخطاء ولكن لا توقف العملية
console.error(`فشل إرسال Webhook إلى ${url}:`, err.message);
})
);
await Promise.all(promises);
res.status(201).json(newOrder);
});
app.listen(3000, () => {
console.log('خدمة الطلبات تعمل على المنفذ 3000');
});
الطرف الثاني: خدمة الإشعارات (مُستقبِل الـ Webhook)
هذا هو التطبيق الذي كان يسبب لي المشاكل. الآن، بدلاً من السؤال كل ثانية، سيقوم فقط بالاستماع.
// Notification Service - server.js
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
// 3. نقطة النهاية التي ستستقبل إشعارات الـ Webhook
app.post('/webhook-listener', (req, res) => {
const payload = req.body;
console.log('🎉 تم استلام إشعار Webhook!');
if (payload.event === 'new_order') {
const order = payload.data;
console.log(`طلب جديد وصل! رقم الطلب: ${order.id}, المنتج: ${order.product}`);
// هنا يمكنك إضافة منطق لتحديث الواجهة باستخدام WebSockets أو إرسال إشعار للمستخدم
}
// مهم جداً: أرسل استجابة 200 OK فوراً لتأكيد الاستلام
res.status(200).send('تم استلام الإشعار بنجاح.');
});
app.listen(4000, () => {
console.log('خدمة الإشعارات تستمع على المنفذ 4000');
// عند بدء التشغيل، يمكننا تسجيل الـ Webhook الخاص بنا في خدمة الطلبات
// في تطبيق حقيقي، هذا يتم مرة واحدة فقط
const axios = require('axios');
axios.post('http://localhost:3000/register-webhook', {
url: 'http://localhost:4000/webhook-listener'
}).catch(err => console.error('فشل تسجيل الـ Webhook'));
});
بهذه الطريقة، تحولنا من نظام صاخب ومزعج إلى حوار هادئ وفعال بين الخدمتين. لا يتم إرسال أي طلبات إلا عند وجود سبب حقيقي لذلك.
نصائح من دفتر أبو عمر
استخدام الـ Webhooks ليس مجرد كتابة كود، بل هو تغيير في طريقة التفكير. وهذه بعض النصائح من تجربتي الشخصية لتستخدمها بشكل احترافي:
1. الأمان أولاً وقبل كل شيء
نقطة نهاية الـ Webhook الخاصة بك هي عنوان URL عام على الإنترنت. أي شخص يعرفه يمكنه إرسال طلبات إليه. لا تثق بأي طلب يصلك!
الحل: استخدم “التوقيعات” (Signatures). عند إرسال الـ Webhook، يقوم المرسل بإنشاء توقيع (عادةً HMAC) باستخدام “سر مشترك” (Shared Secret) وحمولة الطلب. عند استقبال الطلب، تقوم أنت بإعادة حساب التوقيع بنفس الطريقة ومقارنته بالتوقيع المرسل. إذا تطابقا، فالطلب موثوق. إذا لم يتطابقا، فتجاهله فوراً.
2. كن مستعداً للفشل (Idempotency)
قد يفشل إرسال الـ Webhook (مثلاً، خدمتك كانت معطلة مؤقتاً). معظم الخدمات المحترمة ستحاول إعادة الإرسال. هذا يعني أنك قد تستقبل نفس الإشعار مرتين. يجب أن يكون نظامك قادراً على التعامل مع هذا الموقف دون أن يسبب مشاكل (مثل إنشاء إشعارين لنفس الطلب).
الحل: اجعل نقطة النهاية الخاصة بك “عطول” أو Idempotent. هذا مصطلح فخم يعني أن تنفيذ نفس العملية عدة مرات يعطي نفس نتيجة تنفيذها مرة واحدة. يمكنك تحقيق ذلك عن طريق التحقق من معرف فريد للحدث (event ID) قبل معالجته. إذا كنت قد عالجت هذا المعرف من قبل، تجاهل الطلب الجديد.
3. لا تقم بالعمل الشاق مباشرة
مهمة نقطة نهاية الـ Webhook هي استقبال الطلب، التحقق منه، والرد بسرعة باستجابة 200 OK لإعلام المرسل بأنك استلمت الإشعار. لا تقم بعمليات معقدة أو طويلة (مثل معالجة صور، إرسال آلاف الإيميلات) مباشرة في هذه النقطة.
الحل: استخدم “طابور مهام” (Job/Message Queue) مثل RabbitMQ, Redis (مع BullMQ), أو AWS SQS. استقبل الطلب، ضعه في الطابور، ثم دع “عامل” (Worker) في الخلفية يسحب المهمة من الطابور وينفذها براحته. هذا يبقي نقطة النهاية سريعة ومستجيبة.
الخلاصة: ارتاح وخلّي الخادم يشتغل عنك 🧘♂️
الانتقال من الـ Polling إلى الـ Webhooks كان نقلة نوعية في طريقة بنائي للتطبيقات. لقد تعلمت أن أكون “كسولاً بذكاء”. بدلاً من العمل الجاد في السؤال المستمر، جعلت الخوادم تعمل بذكاء وتتحدث مع بعضها فقط عند الضرورة.
الـ Webhooks هي العمود الفقري للكثير من الخدمات التي نستخدمها يومياً: إشعارات الدفع من Stripe، التنبيهات من GitHub، تحديثات الطلبات من خدمات الشحن، وغيرها الكثير. إنها تقنية بسيطة لكنها قوية بشكل لا يصدق.
نصيحتي الأخيرة لك: في المرة القادمة التي تجد فيها نفسك تكتب setInterval أو setTimeout لسؤال خادم عن تحديثات، توقف للحظة واسأل نفسك: “هل يمكنني استخدام Webhook هنا؟”. في معظم الحالات، سيكون الجواب نعم، وسيشكرك خادمك (ومحفظتك) على هذا القرار. 😉