WebSockets: اتصال حي في الاتجاهين
درس 5 من 5
5WebSockets: اتصال حي في الاتجاهين
الصورة الكبيرةHTTP سؤال وجواب: العميل يسأل والخادم يرد ثم ينتهي. WebSocket مكالمة مفتوحة: بعد المصافحة يتكلم الخادم متى أراد، فتصل الرسالة الجديدة فورًا دون أن يسأل أحد.
ماذا تشاهد في الرسم: عمر اتصال WebSocket: يبدأ كطلب HTTP عادي يطلب الترقية، ثم يصبح قناة مفتوحة تمرّ فيها الرسائل في الاتجاهين حتى يغلقها أحد الطرفين.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
HTTP مثل الرسائل البريدية: ترسل سؤالًا وتنتظر الرد، ولو أردت خبرًا جديدًا ترسل سؤالًا آخر. WebSocket مثل مكالمة هاتفية: الخط مفتوح، وأي طرف يتكلم متى شاء. لهذا تُبنى عليه المحادثات والإشعارات الحية والألعاب الجماعية.
يبدأ العميل بطلب GET فيه Upgrade: websocket وSec-WebSocket-Key، والخادم يرد 101 Switching Protocols. بعدها تمرّ إطارات (frames) نصية أو ثنائية على نفس اتصال TCP، مع إطارات ping/pong لإبقائه حيًا. الخادم يحمل حالة لكل اتصال، فالتوسّع الأفقي يحتاج وسيطًا مشتركًا (Redis pub/sub) ليصل البث لعملاء خوادم أخرى. البدائل: Server-Sent Events للبث من الخادم فقط، والاستطلاع الطويل (long polling) حين يمنع شيء WebSocket.
قبل أن تبدأ
📨HTTP
تعرف الطلب والرد والترويسات.
⏳asyncio
درس البرمجة غير المتزامنة: async وawait.
الفكرة الجوهرية
1لماذا لا يكفي HTTP؟
لتعرف إن وصلت رسالة جديدة بـ HTTP عليك أن تسأل كل ثانيتين (polling). أغلب الأسئلة جوابها «لا جديد»، فتهدر الشبكة والخادم، والرسالة تتأخر حتى السؤال التالي. WebSocket يقلب الاتجاه: الخادم يدفع الرسالة لحظة وصولها.
2المصافحة والرسائل
في المتصفح الأمر أربعة أحداث: open وmessage وclose وerror، ودالة send.
const ws = new WebSocket("wss://example.com/chat");
ws.addEventListener("open", () => ws.send(JSON.stringify({ type: "join", name: "Sara" })));
ws.addEventListener("message", (e) => {
const msg = JSON.parse(e.data);
console.log(`${msg.from}: ${msg.text}`);
});
ws.addEventListener("close", () => console.log("disconnected, retry in 2s"));wss:// (المشفّر) في الإنتاج، وأرسل JSON بحقل type لتعرف نوع كل رسالة.3البث: خادم دردشة مصغّر
الخادم يحفظ مجموعة المتصلين. كل رسالة تصل يرسلها للجميع، وعند الانقطاع يحذف صاحبها. هنا نحاكي العملاء بطوابير asyncio لترى المنطق يعمل دون شبكة.
import asyncio
class Room:
def __init__(self):
self.clients = {}
def join(self, name):
self.clients[name] = asyncio.Queue()
return self.clients[name]
def leave(self, name):
self.clients.pop(name, None)
async def broadcast(self, sender, text):
for name, inbox in self.clients.items():
if name != sender:
await inbox.put(f"{sender}: {text}")
async def main():
room = Room()
sara, omar, lina = room.join("sara"), room.join("omar"), room.join("lina")
await room.broadcast("sara", "hello")
room.leave("lina")
await room.broadcast("omar", "hi sara")
for who, inbox in [("sara", sara), ("omar", omar), ("lina", lina)]:
got = []
while not inbox.empty():
got.append(inbox.get_nowait())
print(who, "->", got)
asyncio.run(main())نفس المنطق مع مكتبة websockets الحقيقية:
import asyncio
import websockets
clients = set()
async def handler(ws):
clients.add(ws)
try:
async for message in ws:
websockets.broadcast(clients - {ws}, message)
finally:
clients.discard(ws)
async def main():
async with websockets.serve(handler, "localhost", 8765):
await asyncio.Future()
asyncio.run(main())finally ليحدث دائمًا.4التوسّع وما بعده
خادم واحد يعرف عملاءه فقط. لو شغّلت ثلاث نسخ خلف موازن أحمال، رسالة على النسخة 1 لن تصل لعملاء النسخة 2. الحل وسيط مثل Redis pub/sub: كل نسخة تنشر الرسالة وتستقبل ما ينشره غيرها ثم تبثّه لعملائها.
مثال محلول
المسألة: لوحة طلبات المقهى تحدّث نفسها بطلب كل 3 ثوانٍ، و50 شاشة تعني 1000 طلب في الدقيقة أغلبها فارغ. التحليل: البيانات تتغيّر نادرًا لكن السؤال دائم. الحل: اتصال WebSocket واحد لكل شاشة، والخادم يدفع {"type": "order", ...} فقط حين يصل طلب جديد. صفر طلبات فارغة، والطلب يظهر خلال أجزاء من الثانية.
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| WebSocket بديل كامل لـ HTTP. | لا. استخدم HTTP للطلبات العادية (تسجيل، قراءة صفحة) وWebSocket للحي فقط. |
| لو أُغلق الاتصال سيعود وحده. | المتصفح لا يعيد الاتصال تلقائيًا. اكتب إعادة اتصال بتأخير متزايد. |
أين تقابله في العمل
💬المحادثات
رسائل فورية ومؤشر «يكتب الآن».
📈اللوحات الحية
أسعار وطلبات ومراقبة تتحدث لحظيًا.
🎮الألعاب الجماعية
حركات اللاعبين تصل للجميع فورًا.
اختبر فهمك
بماذا يرد الخادم على طلب ترقية WebSocket ناجح؟
101 تعني أن الاتصال تحوّل من HTTP إلى بروتوكول WebSocket.
أين تضع حذف العميل من القائمة؟
finally تعمل سواء انتهى الاتصال بسلام أو بخطأ.
ثلاث نسخ من الخادم، والبث لا يصل لكل العملاء. الحل؟
كل نسخة تعرف عملاءها فقط، والوسيط يوصل الرسالة لكل النسخ.
بطاقات مراجعة سريعة
تحقق من فهمك
متى تختار SSE بدل WebSocket؟
حين يرسل الخادم فقط والعميل لا يرسل شيئًا حيًا، مثل الإشعارات أو تدفق نص من نموذج لغوي.
لماذا JSON بحقل type؟
لأن القناة واحدة وأنواع الرسائل كثيرة. الحقل يخبر المستقبِل كيف يتصرّف.
ماذا يحدث لو لم تحذف العميل المنقطع؟
تتراكم الأشباح وتفشل كتابات البث وتتسرّب الذاكرة.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.