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> );}
const express = require('express');const http = require('http');const { Server } = require('socket.io'); const app = express();const server = http.createServer(app); // خادم HTTP واحد يخدم Express و Socket.ioconst io = new Server(server, { cors: { origin: 'http://localhost:5173' } }); io.on('connection', (socket) => { console.log('اتصل عميل:', socket.id); io.emit('online', io.engine.clientsCount); // للجميع socket.on('disconnect', () => { console.log('انقطع:', socket.id); io.emit('online', io.engine.clientsCount); });}); server.listen(3000, () => console.log('الخادم الفوري يعمل'));
٣الأحداث في الاتجاهين
كل شيء في Socket.io حدث له اسم تختاره أنت، ومعه بيانات (أي قيمة JSON). الطرف المرسِل يستدعي emit، والمستقبِل يسجّل on بالاسم نفسه:
| الإرسال | الاستقبال | |
|---|---|---|
| من العميل إلى الخادم | socket.emit('msg', data) في React | socket.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> );}
const http = require('http');const { Server } = require('socket.io');const server = http.createServer();const io = new Server(server, { cors: { origin: '*' } }); io.on('connection', (socket) => { socket.on('square', (n, ack) => { console.log('طُلب مربع', n); if (typeof n !== 'number' || Number.isNaN(n)) return ack('أرسل رقمًا'); ack(`${n}² = ${n * n}`); });}); server.listen(3000);
٤لمن يُرسل الحدث؟
| الكود (في الخادم) | يصل إلى |
|---|---|
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> );}
const http = require('http');const { Server } = require('socket.io');const server = http.createServer();const io = new Server(server, { cors: { origin: '*' } }); io.on('connection', (socket) => { const name = socket.handshake.auth.name; // بيانات أرسلها العميل عند الاتصال socket.on('send', (kind) => { console.log(name, 'أرسل', kind); if (kind === 'me') socket.emit('note', 'رسالة لك وحدك يا ' + name); if (kind === 'all') io.emit('note', name + ' يحيّي الجميع'); if (kind === 'others') socket.broadcast.emit('note', name + ' يحيّي الآخرين'); });}); server.listen(3000);
٥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؟
الخلاصة
WebSocket: اتصال دائم في الاتجاهين؛Socket.ioيضيف الأحداث والغرف والإقرار وإعادة الاتصال.http.createServer(app)+new Server(server)+server.listen.emitيرسل وonيستقبل بالاسم نفسه؛ والدالة الأخيرة إقرار.socket.emitلواحد، وio.emitللجميع، وbroadcastلغير المرسِل، وto(room)لغرفة.- في
React: الاتصال فيuseEffect، والتنظيفdisconnect، والتحديث الدالي للحالة.
تحدٍّ برمجي: تصويت مباشر
متوسطالخادم جاهز: يرسل الحدث 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> );}
const http = require('http');const { Server } = require('socket.io');const server = http.createServer();const io = new Server(server, { cors: { origin: '*' } }); const votes = { React: 0, Vue: 0, Angular: 0 }; io.on('connection', (socket) => { socket.emit('results', votes); // النتائج الحالية للقادم الجديد socket.on('vote', (option) => { if (!(option in votes)) return; votes[option]++; io.emit('results', votes); // بث النتائج الجديدة للجميع });}); server.listen(3000);