طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 242 · أطر الويب
الدرس 19 · الوحدة 8: تنفيذ قنوات اتصال فورية

WebSocket و Socket.io

خادم يرسل إليك دون أن تسأله ⏱ نحو 45 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تشرح الفرق بين HTTP و WebSocket ومتى تحتاج الاتصال الفوري.
  • تنشئ خادم Socket.io فوق Express وتستقبل الاتصالات.
  • ترسل الأحداث وتستقبلها في الاتجاهين، مع الإقرار.
  • تختار نطاق البث المناسب، وتدمج Socket.io في React بأمان.

١لماذا الاتصال الفوري؟

في HTTP العادي العميل يسأل والخادم يجيب، ثم ينتهي الاتصال. الخادم لا يستطيع أن يبادر فيخبر المتصفح «وصلتك رسالة جديدة». فكيف تعمل الدردشة والإشعارات ولوحات النتائج الحية؟

الطريقةالفكرةالعيب
الاستطلاع Pollingالعميل يسأل كل ثانيتين: هل من جديد؟طلبات كثيرة بلا فائدة، وتأخير حتى السؤال التالي
WebSocketاتصال دائم في الاتجاهين: أي طرف يرسل متى شاءيحتاج خادمًا يحتفظ بالاتصالات المفتوحة

المتصفحات تدعم new WebSocket(url) مباشرة، لكنها واجهة بسيطة: ترسل نصوصًا فقط. مكتبة Socket.io تبني فوقها ما تحتاجه التطبيقات: أحداث بأسماء، وإعادة الاتصال تلقائيًّا، والغرف، والإقرار بالاستلام، والبث لمجموعة. ولها جزءان: socket.io للخادم، و socket.io-client للواجهة.

🖥 الطرفية
$ npm install socket.io            # في مجلد الخادم$ npm install socket.io-client     # في مجلد الواجهة

٢أول اتصال: من المتصل الآن؟

Socket.io يعمل فوق خادم HTTP نفسه الذي يشغّل Express، لذلك ننشئه بـ http.createServer(app) ونستدعي server.listen بدل app.listen. ومع كل عميل جديد يُطلق الحدث connection ومعه socket يمثّل اتصال ذلك العميل وحده.

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

import { useState, useEffect } from 'react';import { io } from 'socket.io-client'; export default function App({ user }) {  const [online, setOnline] = useState(0);  const [connected, setConnected] = useState(false);   useEffect(() => {    const socket = io('http://localhost:3000');    socket.on('connect', () => setConnected(true));    socket.on('disconnect', () => setConnected(false));    socket.on('online', (n) => setOnline(n));    return () => socket.disconnect();                 // التنظيف عند إزالة المكوّن  }, []);   return (    <div>      <p>{connected ? '🟢 متصل' : '⚪ غير متصل'} — أنا {user}</p>      <p className="count">المتصلون الآن: <b>{online}</b></p>    </div>  );}

٣الأحداث في الاتجاهين

كل شيء في Socket.io حدث له اسم تختاره أنت، ومعه بيانات (أي قيمة JSON). الطرف المرسِل يستدعي emit، والمستقبِل يسجّل on بالاسم نفسه:

الإرسالالاستقبال
من العميل إلى الخادمsocket.emit('msg', data) في Reactsocket.on('msg', (data) => …) في الخادم
من الخادم إلى عميلsocket.emit('reply', data)socket.on('reply', …) في React

والإقرار Acknowledgement: إن جعل المرسِل آخر وسيط دالةً، استدعاها المستقبِل ليرد على هذا الحدث بعينه، مثل طلب ورد HTTP مصغّر:

import { useState, useEffect, useRef } from 'react';import { io } from 'socket.io-client'; export default function App() {  const socket = useRef(null);  const [result, setResult] = useState('');  const [a, setA] = useState(7);   useEffect(() => {    socket.current = io('http://localhost:3000');    return () => socket.current.disconnect();  }, []);   function ask() {    // الوسيط الأخير دالة: يستدعيها الخادم بالجواب    socket.current.emit('square', Number(a), (answer) => setResult(answer));  }   return (    <div>      <input type="number" value={a} onChange={e => setA(e.target.value)} />      <button onClick={ask}>احسب المربع في الخادم</button>      <p className="res">{result}</p>    </div>  );}

٤لمن يُرسل الحدث؟

الكود (في الخادم)يصل إلى
socket.emit('e', d)هذا العميل فقط
io.emit('e', d)كل المتصلين
socket.broadcast.emit('e', d)الجميع ما عدا هذا العميل
socket.join('room1')يُدخل هذا العميل غرفة
io.to('room1').emit('e', d)كل من في الغرفة
socket.to('room1').emit('e', d)من في الغرفة ما عدا المرسِل

هنا ثلاثة أزرار تجرّب الأنواع الثلاثة. اضغط من نافذة سارة وراقب ما يصل إلى كل نافذة:

import { useState, useEffect, useRef } from 'react';import { io } from 'socket.io-client'; export default function App({ user }) {  const socket = useRef(null);  const [inbox, setInbox] = useState([]);   useEffect(() => {    const s = io('http://localhost:3000', { auth: { name: user } });    s.on('note', (text) => setInbox(prev => [...prev, text]));    socket.current = s;    return () => s.disconnect();  }, [user]);   const send = (kind) => socket.current.emit('send', kind);   return (    <div>      <button onClick={() => send('me')}>لي فقط</button>      <button onClick={() => send('all')}>للجميع</button>      <button onClick={() => send('others')}>للآخرين</button>      <ul>{inbox.map((t, i) => <li key={i}>{t}</li>)}</ul>    </div>  );}

٥Socket.io داخل React

الاتصال عمل جانبي، فمكانه useEffect: ننشئ الاتصال، ونسجّل المستمعين، ونعيد دالة تنظيف تقطعه، وإلا تراكمت الاتصالات مع كل إعادة تركيب للمكوّن. ونحفظ المقبس في useRef لنستخدمه في معالجات الأحداث. ولأن المستمع يُسجَّل مرة واحدة، نحدّث القوائم بالصيغة الدالية setItems(prev => [...prev, x]) حتى لا نقرأ قيمة قديمة.

useEffect(() => {  const s = io(URL);  s.on('msg', (m) => setMessages(prev => [...prev, m]));   // ليس [...messages, m]  socketRef.current = s;  return () => s.disconnect();}, []);

توقّع وتحقّق

ما الميزة الأساسية لـ WebSocket على HTTP العادي؟

في الاتجاهين.

أي سطر يرسل الحدث لكل المتصلين؟

io = الجميع.

أين ننشئ اتصال Socket.io في مكوّن React؟

عمل جانبي.

ما الإقرار ack؟

رد مصغّر.

الخلاصة

تحدٍّ برمجي: تصويت مباشر

متوسط

الخادم جاهز: يرسل الحدث results بكائن الأصوات {"React": 0, "Vue": 0, "Angular": 0} عند الاتصال وبعد كل تصويت، ويستقبل الحدث vote باسم الخيار. اكتب الواجهة.

المطلوب:
  • اتصل بالخادم داخل useEffect مع التنظيف.
  • لكل خيار زر نصّه اسم الخيار؛ الضغط يرسل emit('vote', الاسم).
  • اعرض لكل خيار <li> بالشكل React: 2 داخل <ul className="results">، ويتحدث فور وصول results.
👀 اعرض الناتج المطلوب (وجرّبه)
✓ التحقق الآلي من حلّك:
  • الكود يعمل بلا أخطاء في Console
  • تعرض النتائج الأولية الثلاث
  • زر React يرسل صوتًا وتتحدث النتيجة إلى 1
  • ضغطتان على Vue ← Vue: 2
  • الاتصال داخل useEffect مع disconnect في التنظيف

💡 تلميح 1
المستمع: s.on('results', setVotes) يكفي لأن الخادم يرسل الكائن كاملًا.
💡 تلميح 2
الأزرار من مفاتيح الكائن: Object.keys(votes).map(…).

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

import { useState, useEffect, useRef } from 'react';import { io } from 'socket.io-client'; export default function App() {  const [votes, setVotes] = useState({});  const socket = useRef(null);   useEffect(() => {    const s = io('http://localhost:3000');    s.on('results', setVotes);    socket.current = s;    return () => s.disconnect();  }, []);   return (    <div>      {Object.keys(votes).map(k => <button key={k} onClick={() => socket.current.emit('vote', k)}>{k}</button>)}      <ul className="results">        {Object.entries(votes).map(([k, n]) => <li key={k}>{k}: {n}</li>)}      </ul>    </div>  );}
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس