مقدمة إلى مكتبة ws
مكتبة ws هي تطبيق WebSocket سريع ومتوافق مع المعايير لـ Node.js. إنها واحدة من أكثر مكتبات WebSocket شهرة مع حمل بسيط وخصائص أداء ممتازة.
لماذا ws؟
- خفيفة الوزن: لا توجد تبعيات خارجية، تجريد بسيط
- سريعة: محسّنة بشكل كبير للأداء
- متوافقة مع المعايير: تنفذ بالكامل RFC 6455
- جاهزة للإنتاج: تستخدمها الآلاف من تطبيقات الإنتاج
- صيانة نشطة: يتم تحديثها بانتظام ومدعومة جيدًا
تثبيت ws
قم بتثبيت مكتبة ws باستخدام npm أو yarn:
# استخدام npm
npm install ws
# استخدام yarn
yarn add ws
# التحقق من الإصدار المثبت
npm list ws
إنشاء خادم WebSocket أساسي
لنقم بإنشاء خادم WebSocket بسيط يعيد الرسائل إلى العملاء:
// server.js
const WebSocket = require('ws');
// إنشاء خادم WebSocket على المنفذ 8080
const wss = new WebSocket.Server({ port: 8080 });
console.log('تم بدء خادم WebSocket على ws://localhost:8080');
// الاستماع للاتصالات
wss.on('connection', (ws) => {
console.log('عميل جديد متصل');
// الاستماع للرسائل من هذا العميل
ws.on('message', (message) => {
console.log('تم الاستلام:', message.toString());
// إعادة صدى الرسالة
ws.send(`صدى: ${message.toString()}`);
});
// إرسال رسالة ترحيب
ws.send('مرحبًا بك في خادم WebSocket!');
});
قم بتشغيل الخادم:
node server.js
خيارات خادم WebSocket
يقبل منشئ WebSocket.Server خيارات تكوين مختلفة:
const wss = new WebSocket.Server({
port: 8080, // منفذ الخادم
host: '0.0.0.0', // المضيف للربط به (0.0.0.0 لجميع الواجهات)
path: '/socket', // مسار URL (مثل ws://host:port/socket)
perMessageDeflate: true, // تمكين الضغط
clientTracking: true, // تتبع العملاء المتصلين (افتراضي: true)
maxPayload: 100 * 1024 * 1024, // حجم الرسالة الأقصى (100MB)
verifyClient: (info, cb) => { // دالة التحقق المخصصة
// التحقق من العميل قبل قبول الاتصال
const token = new URL(info.req.url, 'http://localhost').searchParams.get('token');
if (token === 'valid-token') {
cb(true); // قبول الاتصال
} else {
cb(false, 401, 'غير مصرح'); // رفض الاتصال
}
}
});
التحقق المخصص: استخدم خيار verifyClient لتنفيذ المصادقة أو تحديد المعدل أو تصفية IP قبل قبول اتصالات WebSocket. هذا يمنع العملاء غير المصرح لهم من إنشاء الاتصالات.
الإرفاق بخادم HTTP موجود
بدلاً من إنشاء خادم مستقل، يمكنك إرفاق خادم WebSocket بخادم HTTP/HTTPS موجود:
const http = require('http');
const WebSocket = require('ws');
const express = require('express');
// إنشاء تطبيق Express
const app = express();
// مسارات HTTP عادية
app.get('/', (req, res) => {
res.send('خادم HTTP يعمل');
});
app.get('/api/data', (req, res) => {
res.json({ message: 'نقطة نهاية REST API' });
});
// إنشاء خادم HTTP
const server = http.createServer(app);
// إرفاق خادم WebSocket بخادم HTTP
const wss = new WebSocket.Server({ server, path: '/socket' });
wss.on('connection', (ws) => {
console.log('عميل WebSocket متصل');
ws.on('message', (message) => {
console.log('تم الاستلام:', message.toString());
});
});
// بدء الخادم
server.listen(8080, () => {
console.log('الخادم يستمع على http://localhost:8080');
console.log('WebSocket متاح على ws://localhost:8080/socket');
});
فوائد المنفذ المشترك: تشغيل WebSocket و HTTP على نفس المنفذ يبسط النشر، ويعمل بشكل أفضل مع جدران الحماية وموازنات التحميل، ويتيح لك مشاركة شهادات SSL/TLS.
معالجة اتصالات العملاء
يوفر حدث الاتصال الوصول إلى مثيل WebSocket لكل عميل متصل:
wss.on('connection', (ws, request) => {
// ws: مثيل WebSocket لهذا الاتصال
// request: كائن طلب ترقية HTTP
// الوصول إلى معلومات الطلب
console.log('IP العميل:', request.socket.remoteAddress);
console.log('User-Agent:', request.headers['user-agent']);
console.log('URL:', request.url);
// تحليل معلمات الاستعلام
const url = new URL(request.url, `http://${request.headers.host}`);
const userId = url.searchParams.get('userId');
const token = url.searchParams.get('token');
console.log('معرف المستخدم:', userId);
console.log('الرمز:', token);
// تخزين خصائص مخصصة على مثيل WebSocket
ws.userId = userId;
ws.isAuthenticated = token === 'valid-token';
// إرسال رسالة ترحيب مخصصة
ws.send(JSON.stringify({
type: 'welcome',
message: `مرحبًا مستخدم ${userId}!`,
timestamp: Date.now()
}));
});
استلام وإرسال الرسائل
استلام الرسائل
ws.on('message', (data, isBinary) => {
// data: Buffer أو سلسلة
// isBinary: منطقي يشير إلى ما إذا كانت الرسالة ثنائية
if (isBinary) {
console.log('تم استلام بيانات ثنائية:', data.length, 'بايت');
// معالجة البيانات الثنائية
const view = new Uint8Array(data);
console.log('البايت الأول:', view[0]);
} else {
// رسالة نصية
const message = data.toString();
console.log('تم استلام نص:', message);
// تحليل JSON إذا كان قابلاً للتطبيق
try {
const json = JSON.parse(message);
console.log('JSON محلل:', json);
// معالجة أنواع الرسائل المختلفة
switch (json.type) {
case 'chat':
handleChatMessage(ws, json);
break;
case 'ping':
ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
break;
default:
console.log('نوع رسالة غير معروف:', json.type);
}
} catch (e) {
console.log('ليست رسالة JSON');
}
}
});
إرسال الرسائل
// إرسال رسالة نصية
ws.send('مرحبًا أيها العميل!');
// إرسال JSON
ws.send(JSON.stringify({ type: 'notification', message: 'تحديث جديد' }));
// إرسال بيانات ثنائية
const buffer = Buffer.from([1, 2, 3, 4, 5]);
ws.send(buffer);
// الإرسال مع رد اتصال
ws.send('رسالة مهمة', (error) => {
if (error) {
console.error('فشل الإرسال:', error);
} else {
console.log('تم إرسال الرسالة بنجاح');
}
});
// الإرسال مع خيارات
ws.send('رسالة مضغوطة', {
compress: true, // ضغط هذه الرسالة
binary: false, // الإرسال كإطار نصي
fin: true // جزء نهائي
}, (error) => {
if (error) console.error('خطأ في الإرسال:', error);
});
بث الرسائل
البث يرسل رسالة إلى جميع العملاء المتصلين:
// بث بسيط لجميع العملاء
function broadcast(message) {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// البث باستثناء المرسل
function broadcastExcept(message, sender) {
wss.clients.forEach((client) => {
if (client !== sender && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// البث لمستخدمين محددين
function broadcastToUsers(message, userIds) {
wss.clients.forEach((client) => {
if (userIds.includes(client.userId) && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// الاستخدام في معالج الرسائل
ws.on('message', (data) => {
const message = data.toString();
// البث لجميع العملاء باستثناء المرسل
broadcastExcept(JSON.stringify({
type: 'chat',
from: ws.userId,
message: message,
timestamp: Date.now()
}), ws);
});
اعتبار الأداء: البث إلى آلاف العملاء بشكل متزامن يمكن أن يعطل حلقة الأحداث. بالنسبة للتطبيقات واسعة النطاق، ضع في اعتبارك استخدام العمال أو التجميع أو قوائم انتظار الرسائل (Redis Pub/Sub) لتوزيع الحمل.
معالجة الأخطاء
قم دائمًا بمعالجة الأخطاء لمنع تعطل الخادم:
// معالجة الأخطاء على مستوى الاتصال
ws.on('error', (error) => {
console.error('خطأ WebSocket:', error);
// تسجيل تفاصيل الخطأ
console.error('كود الخطأ:', error.code);
console.error('رسالة الخطأ:', error.message);
// لا تحاول الإرسال على اتصال به خطأ
// سيتم إغلاق الاتصال تلقائيًا
});
// معالجة الأخطاء على مستوى الخادم
wss.on('error', (error) => {
console.error('خطأ خادم WebSocket:', error);
// معالجة أخطاء الخادم (المنفذ قيد الاستخدام بالفعل، إلخ)
if (error.code === 'EADDRINUSE') {
console.error('المنفذ قيد الاستخدام بالفعل');
process.exit(1);
}
});
// معالجة الاستثناءات غير المعلقة
process.on('uncaughtException', (error) => {
console.error('استثناء غير معلق:', error);
// إيقاف سلس
wss.close(() => {
process.exit(1);
});
});
أحداث دورة حياة الاتصال
ws.on('open', () => {
console.log('تم فتح الاتصال');
// ملاحظة: لا يتم إصدار هذا الحدث على جانب الخادم
// فقط حدث 'connection' على wss يشير إلى اتصال جديد
});
ws.on('close', (code, reason) => {
console.log('تم إغلاق الاتصال');
console.log('كود الإغلاق:', code);
console.log('سبب الإغلاق:', reason.toString());
// تنظيف الموارد
if (ws.userId) {
console.log(`المستخدم ${ws.userId} قطع الاتصال`);
// إزالة من قائمة المستخدمين النشطين، إلخ
}
});
ws.on('ping', (data) => {
console.log('تم استلام ping من العميل');
// يتم إرسال Pong تلقائيًا بواسطة مكتبة ws
});
ws.on('pong', (data) => {
console.log('تم استلام pong من العميل');
ws.isAlive = true; // وضع علامة على الاتصال على أنه حي
});
نبضات القلب Ping/Pong
قم بتنفيذ آلية نبضات القلب لاكتشاف الاتصالات المعطلة:
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// فترة نبضات القلب (30 ثانية)
const HEARTBEAT_INTERVAL = 30000;
wss.on('connection', (ws) => {
// وضع علامة على الاتصال على أنه حي
ws.isAlive = true;
// إعادة تعيين علامة حي عند استلام pong
ws.on('pong', () => {
ws.isAlive = true;
});
ws.on('message', (message) => {
console.log('تم الاستلام:', message.toString());
});
});
// ping جميع العملاء بشكل دوري
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
// الاتصال ميت، أنهِه
console.log('إنهاء الاتصال الميت');
return ws.terminate();
}
// وضع علامة على أنه ميت محتمل
ws.isAlive = false;
// إرسال ping
ws.ping();
});
}, HEARTBEAT_INTERVAL);
// تنظيف الفترة عند إغلاق الخادم
wss.on('close', () => {
clearInterval(interval);
});
أفضل ممارسة لنبضات القلب: قم دائمًا بتنفيذ نبضات القلب ping/pong في الإنتاج. هذا يكتشف فشل الشبكة ومهلات الوكيل وتعطل العميل التي لن تؤدي إلى حدث الإغلاق بطريقة أخرى.
مثال خادم دردشة كامل
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// تخزين المستخدمين النشطين
const users = new Map();
console.log('تم بدء خادم الدردشة على ws://localhost:8080');
wss.on('connection', (ws, request) => {
const clientIp = request.socket.remoteAddress;
console.log('اتصال جديد من:', clientIp);
// تهيئة الاتصال
ws.isAlive = true;
ws.userId = null;
// معالجة pong
ws.on('pong', () => {
ws.isAlive = true;
});
// معالجة الرسائل
ws.on('message', (data) => {
try {
const message = JSON.parse(data.toString());
switch (message.type) {
case 'join':
// المستخدم ينضم إلى الدردشة
ws.userId = message.userId;
ws.username = message.username;
users.set(ws.userId, ws);
// بث انضمام المستخدم
broadcast({
type: 'user-joined',
userId: ws.userId,
username: ws.username,
timestamp: Date.now()
});
// إرسال قائمة المستخدمين النشطين
ws.send(JSON.stringify({
type: 'users-list',
users: Array.from(users.values()).map(u => ({
userId: u.userId,
username: u.username
}))
}));
break;
case 'chat-message':
// بث رسالة الدردشة
broadcast({
type: 'chat-message',
from: ws.username,
message: message.content,
timestamp: Date.now()
});
break;
case 'typing':
// بث مؤشر الكتابة
broadcastExcept({
type: 'user-typing',
username: ws.username
}, ws);
break;
default:
console.log('نوع رسالة غير معروف:', message.type);
}
} catch (error) {
console.error('خطأ في معالجة الرسالة:', error);
ws.send(JSON.stringify({
type: 'error',
message: 'تنسيق رسالة غير صالح'
}));
}
});
// معالجة قطع الاتصال
ws.on('close', () => {
console.log('قطع اتصال العميل:', ws.username);
if (ws.userId) {
users.delete(ws.userId);
// بث مغادرة المستخدم
broadcast({
type: 'user-left',
userId: ws.userId,
username: ws.username,
timestamp: Date.now()
});
}
});
// معالجة الأخطاء
ws.on('error', (error) => {
console.error('خطأ WebSocket:', error);
});
});
// مساعد البث
function broadcast(data) {
const message = JSON.stringify(data);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// البث باستثناء المرسل
function broadcastExcept(data, sender) {
const message = JSON.stringify(data);
wss.clients.forEach((client) => {
if (client !== sender && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// نبضات القلب
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('close', () => {
clearInterval(interval);
});
تمرين: قم بتوسيع خادم الدردشة ليشمل:
- المراسلة الخاصة بين مستخدمين محددين
- غرف/قنوات الدردشة التي يمكن للمستخدمين الانضمام إليها
- سجل الرسائل المخزن في الذاكرة أو قاعدة البيانات
- مصادقة المستخدم برموز JWT
- تحديد المعدل لمنع البريد العشوائي
الملخص
توفر مكتبة ws أساسًا قويًا لبناء خوادم WebSocket في Node.js. تشمل المفاهيم الرئيسية إنشاء الخوادم مع خيارات التكوين، ومعالجة الاتصالات والرسائل، والبث إلى عملاء متعددين، وتنفيذ معالجة الأخطاء، واستخدام نبضات القلب ping/pong لمراقبة صحة الاتصال. مع هذه اللبنات الأساسية، يمكنك إنشاء تطبيقات في الوقت الفعلي جاهزة للإنتاج. في الدرس التالي، سنستكشف Socket.io، وهي مكتبة عالية المستوى تضيف ميزات مثل إعادة الاتصال التلقائي ودعم الغرف.