خادم إشارات WebRTC: دليل شامل مع أمثلة Node.js
WebRTC يتيح للمتصفحات إرسال الفيديو والصوت والبيانات مباشرة لبعضها البعض. بدون إضافات، بدون Flash، بدون خوادم وسيطة تلمس تدفقات الوسائط الخاصة بك. لكن إليك الشيء الذي يربك كل مطور للمرة الأولى: متصفحان لا يمكنهما ببساطة أن يجدا بعضهما البعض بشكل سحري على الإنترنت. يحتاجان إلى وسيط. هذا الوسيط هو خادم الإشارات.
لقد بنيت خوادم إشارات لمنصات الاستشارة بالفيديو، وألعاب المتصفح متعددة اللاعبين، وأدوات التحرير التعاوني. في كل مرة، طبقة الإشارات هي حيث يعيش الالتباس -- ليس لأنها صعبة، بل لأن WebRTC تتركها غير محددة بقصد. تخبرك المواصفات ما هو المعلومات التي يجب تبديلها ولا تقول شيئًا عن كيفية تبديلها. وهذا نعمة ولعنة في نفس الوقت.
يغطي هذا الدليل كل شيء: ما الذي تفعله الإشارات تحت السطح، بروتوكول العرض/الإجابة مع SDP، تبديل مرشحات ICE وعبور NAT، وبعد ذلك تطبيقان جاهزان للإنتاج -- أحدهما باستخدام Node.js + Socket.io والآخر يستخدم Supabase Realtime. سننظر إلى الكود الفعلي، وليس الكود الكاذب.
جدول المحتويات
- ماذا يفعل خادم الإشارات بالفعل؟
- بروتوكول العرض/الإجابة و SDP
- مرشحات ICE وعبور NAT
- خيارات نقل الإشارات مقارنة
- التطبيق 1: Node.js + Socket.io
- التطبيق 2: Supabase Realtime
- نصائح تقوية الإنتاج
- خوادم TURN: عندما تفشل الاتصالات المباشرة
- الأسئلة الشائعة

ماذا يفعل خادم الإشارات بالفعل؟
دعونا نزيل المصطلحات. خادم الإشارات هو مجرد ناقل رسائل. هذا كل شيء. يحتاج النظيران إلى تبديل بضعة كيلوبايتات من النص قبل أن يتمكنا من الاتصال المباشر. يحمل خادم الإشارات تلك الرسائل ذهابًا وإيابًا.
على وجه التحديد، يتعامل خادم الإشارات مع:
- اكتشاف النظير -- المستخدم أ يحتاج إلى أن يخبر المستخدم ب "أريد الاتصال بك." يجب على شخص ما توجيه تلك الرسالة.
- تبديل SDP -- يقوم كلا النظيرين بإنشاء كائنات بروتوكول وصف الجلسة التي تصف قدراتهما على الوسائط (المكودات، التشفير، إلخ). يجب أن تنتقل هذه من أ إلى ب والعودة.
- ناقل مرشح ICE -- يكتشف كل نظير مسارات شبكة محتملة (مرشحات) ويرسلها إلى النظير الآخر عبر خادم الإشارات.
- دورة حياة الجلسة -- بدء وإعادة التفاوض وإيقاف الاتصالات.
إليك ما لا يفعله خادم الإشارات: لا يلمس أبدًا بيانات الصوت أو الفيديو الخاصة بك. بمجرد أن ينشئ النظيران اتصالاً مباشرًا، تكون وظيفة خادم الإشارات قد انتهت بشكل أساسي. تتدفق الوسائط من نظير إلى نظير.
فكر فيها مثل تعريف شخصين في حفلة. تمشي، وتقول "مرحباً سارة، هذا مايك، تحبان التسلق." ثم تمشي بعيدًا. يتولى سارة ومايك من هناك. أنت خادم الإشارات في هذا السيناريو.
لماذا WebRTC لا توحد الإشارات
هذا اختيار مقصود، وليس غلطة. أدرك مؤلفو مواصفات WebRTC أن معظم التطبيقات لديها بالفعل قناة اتصال بين المستخدمين -- نظام دردشة، خدمة مطابقة، منصة تعاون. فرض بروتوكول إشارات محدد يعني أن كل تطبيق يحتاج إلى تطبيق طبقتي اتصال بدلاً من الاستفادة من ما هو موجود بالفعل.
تطبيق المواعدة لديه بالفعل بنية تحتية للرسائل. منصة الطب عن بعد لديها بالفعل أنظمة المواعيد. منصة الألعاب لديها بالفعل خوادم الرهان. يتيح WebRTC لك استخدام ما لديك بالفعل لتمرير رسائل SDP و ICE حول.
يمكنك حرفياً استخدام حمام ينقل عصي USB إذا كان التأخير لا يهم. (إنه مهم، لكن النقطة قائمة.)
بروتوكول العرض/الإجابة و SDP
يتبع التبادل الأساسي للإشارات نمطًا يسمى نموذج العرض/الإجابة، مستعارًا من SIP (بروتوكول بدء الجلسة). إليك التدفق:
- النظير أ ينشئ
RTCPeerConnectionويستدعيcreateOffer() - يعين النظير أ العرض كوصف محلي له عبر
setLocalDescription() - يرسل النظير أ العرض (كائن SDP) إلى النظير ب عبر خادم الإشارات
- النظير ب يتلقى العرض ويعينه كوصف بعيد عبر
setRemoteDescription() - يستدعي النظير ب
createAnswer() - يعين النظير ب الإجابة كوصف محلي له
- يرسل النظير ب الإجابة مرة أخرى عبر خادم الإشارات
- يتلقى النظير أ الإجابة ويعينها كوصف بعيد له
بعد هذا التبادل، يعرف كلا النظيرين قدرات الوسائط لدى الآخر. لكن لا يزالان غير قادرين على الاتصال -- يحتاجان إلى معلومات مسار الشبكة، التي تأتي من ICE.
ما الذي يوجد داخل كائن SDP؟
SDP هي صيغة نصية تبدو وكأنها تم تصميمها عام 1996. لأنها كذلك. إليك مثال مختصر:
v=0
o=- 4625943584070133116 2 IN IP4 127.0.0.1
s=-
t=0 0
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:aR7B
a=ice-pwd:hN3Y2mDoS68sGLprHvBGNbKp
a=fingerprint:sha-256 D1:3C:A0:...
a=setup:actpass
a=mid:0
a=rtpmap:96 VP8/90000
a=rtpmap:97 H264/90000
الأجزاء المهمة: فهرس المكودات المدعومة (VP8، H264)، بصمات التشفير، بيانات اعتماد ICE، وأنواع الوسائط. لا يحتاج خادم الإشارات إلى تحليل أي من هذا. يمرر الكائن ببساطة كسلسلة معتمة.
العرض/الإجابة SDP في الكود
إليك JavaScript من جانب العميل للنظير الذي يقدم:
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:your-turn-server.com:3478', username: 'user', credential: 'pass' }
]
});
// أضف مسارات الوسائط المحلية
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach(track => pc.addTrack(track, stream));
// إنشء وإرسال عرض
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// أرسل إلى خادم الإشارات
signalingServer.send({
type: 'offer',
sdp: pc.localDescription,
targetPeerId: 'peer-b-id'
});
والنظير الذي يجيب:
signalingServer.on('offer', async (data) => {
await pc.setRemoteDescription(new RTCSessionDescription(data.sdp));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
signalingServer.send({
type: 'answer',
sdp: pc.localDescription,
targetPeerId: data.fromPeerId
});
});
مرشحات ICE وعبور NAT
يتعامل تفاوض SDP مع ما هي الوسائط المراد إرسالها. يتعامل ICE مع كيفية الوصول إلى النظير الآخر على الشبكة. هنا حيث تصبح الأشياء مثيرة -- وحيث تفشل معظم اتصالات WebRTC بالفعل في العالم الحقيقي.
مشكلة NAT
تجلس معظم الأجهزة خلف جهاز توجيه يقوم بـ NAT (ترجمة عنوان الشبكة). قد يكون لجهاز الكمبيوتر المحمول الخاص بك عنوان IP محلي 192.168.1.42، لكن العالم الخارجي يرى عنوان IP العام لجهاز التوجيه الخاص بك. عندما يجلس كلا النظيرين خلف NATs، لا أحد منهما يعرف كيف يصل إلى الآخر مباشرة.
يحل ICE (Interactive Connectivity Establishment) هذا عن طريق جمع مسارات مرشح متعددة والمحاولة الجميع:
| نوع المرشح | المصدر | معدل النجاح | الكمون |
|---|---|---|---|
| Host | واجهة الشبكة المحلية | يعمل فقط على نفس LAN | الأقل |
| Server-reflexive (srflx) | مكتشف عبر خادم STUN | ~80-85% من الاتصالات | منخفض |
| Relay | مخصص عبر خادم TURN | ~99%+ (احتياطي) | أعلى (موصول) |
تقطير مرشح ICE
إليك تحسين حاسم: تقطير ICE. بدلاً من الانتظار حتى يتم جمع جميع المرشحات قبل إرسالها، تُرسل كل مرشح إلى النظير البعيد بمجرد اكتشافه. يمكن أن يوفر هذا ثواني من إنشاء الاتصال.
// جانب المتصل: أرسل المرشحات عند وصولها
pc.onicecandidate = (event) => {
if (event.candidate) {
signalingServer.send({
type: 'ice-candidate',
candidate: event.candidate,
targetPeerId: remotePeerId
});
}
};
// جانب المتلقي: أضف المرشحات عند وصولها
signalingServer.on('ice-candidate', async (data) => {
try {
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
} catch (err) {
// يمكن أن تصل المرشحات قبل تعيين الوصف البعيد
// ضعهم في المخزن المؤقت وأضفهم لاحقًا
console.error('Failed to add ICE candidate:', err);
}
});
كتلة catch تلمح إلى مشكلة في العالم الحقيقي: يمكن أن تصل مرشحات ICE قبل استدعاء setRemoteDescription(). تحتاج إلى تخزينها مؤقتًا. سنتعامل مع هذا بشكل صحيح في التطبيقات الجاهزة للإنتاج أدناه.
تخزين مرشح ICE مؤقتًا
هذا شيء تخطيه كل برنامج تعليمي تقريبًا، وهو يسبب أخطاء في الإنتاج. إليك النمط:
let pendingCandidates = [];
let remoteDescriptionSet = false;
signalingServer.on('ice-candidate', async (data) => {
if (remoteDescriptionSet) {
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
} else {
pendingCandidates.push(data.candidate);
}
});
// بعد تعيين الوصف البعيد:
await pc.setRemoteDescription(remoteDesc);
remoteDescriptionSet = true;
for (const candidate of pendingCandidates) {
await pc.addIceCandidate(new RTCIceCandidate(candidate));
}
pendingCandidates = [];

خيارات نقل الإشارات مقارنة
يمكنك استخدام أي قناة اتصال ثنائية الاتجاه تقريبًا للإشارات. إليك كيفية مقارنة الخيارات الشائعة:
| النقل | المميزات | العيوب | الأفضل لـ |
|---|---|---|---|
| WebSocket (خام) | كمون منخفض، ثنائي الاتجاه كامل، خفيف الوزن | منطق إعادة الاتصال اليدوي، بدون غرف | تطبيقات 1:1 بسيطة |
| Socket.io | إعادة الاتصال التلقائي، الغرف، الرجوع إلى الاستطلاع | حجم المكتبة أكبر (~45KB)، وليس معيار | معظم تطبيقات الويب |
| Supabase Realtime | بنية تحتية مدارة، مصادقة مدمجة، تكامل Postgres | قفل البائع، حدود حجم الرسالة | التطبيقات الموجودة بالفعل على Supabase |
| Firebase Realtime DB | مدارة، توثيق جيد، دعم غير متصل | قفل البائع، التسعير عند النطاق | تطبيقات قائمة على Firebase |
| استطلاع HTTP | يعمل في كل مكان، بسيط في التطبيق | كمون عالي، حمل الخادم | البيئات القديمة |
| SIP عبر WebSocket | التوافقية مع أنظمة الهاتفية | معقد، زيادة في معظم تطبيقات الويب | تكامل VoIP |
بالنسبة لمعظم المشاريع، Socket.io أو خدمة realtime مُدارة مثل Supabase هو الخيار الصحيح. دعونا نبني كليهما.
التطبيق 1: Node.js + Socket.io
هذا هو النهج الأكثر شيوعًا والنهج الذي أوصي به للفرق التي تريد السيطرة الكاملة على بنية الإشارات الخاصة بها. نحن نبني خادم إشارات يدعم غرف بها عدة نظراء.
إعداد الخادم
import { createServer } from 'http';
import { Server } from 'socket.io';
const httpServer = createServer();
const io = new Server(httpServer, {
cors: {
origin: process.env.CLIENT_ORIGIN || 'http://localhost:3000',
methods: ['GET', 'POST']
}
});
// تتبع الغرف والمشاركين فيها
const rooms = new Map();
io.on('connection', (socket) => {
console.log(`Peer connected: ${socket.id}`);
socket.on('join-room', (roomId) => {
socket.join(roomId);
if (!rooms.has(roomId)) {
rooms.set(roomId, new Set());
}
rooms.get(roomId).add(socket.id);
// أخبر النظراء الموجودين في الغرفة
socket.to(roomId).emit('peer-joined', { peerId: socket.id });
// أرسل للنظير الجديد قائمة بالنظراء الموجودين
const existingPeers = [...rooms.get(roomId)].filter(id => id !== socket.id);
socket.emit('existing-peers', { peers: existingPeers });
});
socket.on('offer', ({ targetPeerId, sdp }) => {
io.to(targetPeerId).emit('offer', {
sdp,
fromPeerId: socket.id
});
});
socket.on('answer', ({ targetPeerId, sdp }) => {
io.to(targetPeerId).emit('answer', {
sdp,
fromPeerId: socket.id
});
});
socket.on('ice-candidate', ({ targetPeerId, candidate }) => {
io.to(targetPeerId).emit('ice-candidate', {
candidate,
fromPeerId: socket.id
});
});
socket.on('disconnect', () => {
// تنظيف الغرف
for (const [roomId, peers] of rooms.entries()) {
if (peers.has(socket.id)) {
peers.delete(socket.id);
socket.to(roomId).emit('peer-left', { peerId: socket.id });
if (peers.size === 0) {
rooms.delete(roomId);
}
}
}
});
});
const PORT = process.env.PORT || 3001;
httpServer.listen(PORT, () => {
console.log(`Signaling server running on port ${PORT}`);
});
تكامل جانب العميل
import { io } from 'socket.io-client';
const socket = io('wss://your-signaling-server.com');
const peerConnections = new Map();
const ICE_SERVERS = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
// أضف خادم TURN للإنتاج
]
};
function createPeerConnection(remotePeerId) {
const pc = new RTCPeerConnection(ICE_SERVERS);
let pendingCandidates = [];
let isRemoteDescSet = false;
pc.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('ice-candidate', {
targetPeerId: remotePeerId,
candidate: event.candidate
});
}
};
pc.ontrack = (event) => {
// أرفق التدفق البعيد إلى عنصر فيديو
const remoteVideo = document.getElementById(`video-${remotePeerId}`);
if (remoteVideo) {
remoteVideo.srcObject = event.streams[0];
}
};
pc.onconnectionstatechange = () => {
if (pc.connectionState === 'failed' || pc.connectionState === 'disconnected') {
console.warn(`Connection to ${remotePeerId} ${pc.connectionState}`);
// طبق منطق إعادة الاتصال هنا
}
};
// قم بتخزين الاتصال وحالته
peerConnections.set(remotePeerId, {
pc,
pendingCandidates,
isRemoteDescSet,
setRemoteDescDone() {
this.isRemoteDescSet = true;
this.pendingCandidates.forEach(c => pc.addIceCandidate(new RTCIceCandidate(c)));
this.pendingCandidates = [];
}
});
return pc;
}
// انضم إلى غرفة
socket.emit('join-room', 'my-room-id');
// عندما ينضم نظير جديد، أنشئ عرض
socket.on('peer-joined', async ({ peerId }) => {
const pc = createPeerConnection(peerId);
localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.emit('offer', { targetPeerId: peerId, sdp: pc.localDescription });
});
socket.on('offer', async ({ sdp, fromPeerId }) => {
const pc = createPeerConnection(fromPeerId);
localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
await pc.setRemoteDescription(new RTCSessionDescription(sdp));
peerConnections.get(fromPeerId).setRemoteDescDone();
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('answer', { targetPeerId: fromPeerId, sdp: pc.localDescription });
});
socket.on('answer', async ({ sdp, fromPeerId }) => {
const conn = peerConnections.get(fromPeerId);
await conn.pc.setRemoteDescription(new RTCSessionDescription(sdp));
conn.setRemoteDescDone();
});
socket.on('ice-candidate', ({ candidate, fromPeerId }) => {
const conn = peerConnections.get(fromPeerId);
if (conn.isRemoteDescSet) {
conn.pc.addIceCandidate(new RTCIceCandidate(candidate));
} else {
conn.pendingCandidates.push(candidate);
}
});
يتعامل هذا مع عدة نظراء في غرفة، ويخزن مرشحات ICE بشكل صحيح، وينظف عند قطع الاتصال. إنه النمط الذي نستخدمه عند بناء ميزات في الوقت الفعلي في تطبيقات Next.js.
التطبيق 2: Supabase Realtime
إذا كنت تستخدم بالفعل Supabase (أو تريد تجنب تشغيل خادم WebSocket الخاص بك)، فإن قنوات Supabase Realtime تعمل بشكل جميل للإشارات. يستخدم هذا النهج ميزة البث في Supabase -- لا حاجة إلى كتابات قاعدة البيانات.
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
const roomId = 'video-call-room-123';
const myPeerId = crypto.randomUUID();
const channel = supabase.channel(roomId, {
config: { broadcast: { self: false } }
});
// استمع إلى رسائل الإشارات
channel
.on('broadcast', { event: 'offer' }, ({ payload }) => {
if (payload.targetPeerId === myPeerId) {
handleOffer(payload);
}
})
.on('broadcast', { event: 'answer' }, ({ payload }) => {
if (payload.targetPeerId === myPeerId) {
handleAnswer(payload);
}
})
.on('broadcast', { event: 'ice-candidate' }, ({ payload }) => {
if (payload.targetPeerId === myPeerId) {
handleIceCandidate(payload);
}
})
.on('broadcast', { event: 'peer-joined' }, ({ payload }) => {
if (payload.peerId !== myPeerId) {
initiateConnection(payload.peerId);
}
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// أعلن عن الحضور
await channel.send({
type: 'broadcast',
event: 'peer-joined',
payload: { peerId: myPeerId }
});
}
});
// أرسل عرض
async function sendOffer(targetPeerId, sdp) {
await channel.send({
type: 'broadcast',
event: 'offer',
payload: { sdp, fromPeerId: myPeerId, targetPeerId }
});
}
// أرسل إجابة
async function sendAnswer(targetPeerId, sdp) {
await channel.send({
type: 'broadcast',
event: 'answer',
payload: { sdp, fromPeerId: myPeerId, targetPeerId }
});
}
// أرسل مرشح ICE
async function sendIceCandidate(targetPeerId, candidate) {
await channel.send({
type: 'broadcast',
event: 'ice-candidate',
payload: { candidate, fromPeerId: myPeerId, targetPeerId }
});
}
لدى نهج Supabase ميزة ملحوظة: تحصل على تتبع الوجود مجانًا باستخدام ميزة Presence في Supabase. يمكنك رؤية من في الغرفة بدون كود إضافي. المقايضة هي أن رسائل البث مرئية لجميع مشتركي القناة -- أنت تصفي حسب targetPeerId من جانب العميل، وهذا جيد للإشارات (إنها ليست بيانات حساسة؛ يتم تشفير الوسائط الفعلية).
للفرق التي تبني على Supabase، هذا يزيل خادم كامل من البنية التحتية الخاصة بك. استخدمنا هذا النمط في المشاريع حيث كان التعاون في الوقت الفعلي مطلبًا.
نصائح تقوية الإنتاج
الحصول على الإشارات للعمل في التطوير مباشر. إبقاءها موثوقة في الإنتاج هو حيث يبدأ العمل الفعلي.
1. ترتيب الرسائل وإلغاء التكرار
يمكن أن تصل رسائل WebSocket بدون ترتيب أثناء إعادة الاتصال. أضف أرقام تسلسل إلى رسائلك واتعامل مع إعادة الترتيب على جانب العميل:
let messageSeq = 0;
function sendSignalingMessage(type, payload) {
socket.emit(type, { ...payload, seq: ++messageSeq, timestamp: Date.now() });
}
2. النبضات وإعادة الاتصال
يتعامل Socket.io مع إعادة الاتصال تلقائيًا، لكن عليك إعادة الانضمام إلى الغرف بعد إعادة الاتصال:
socket.on('connect', () => {
if (currentRoom) {
socket.emit('join-room', currentRoom);
}
});
3. تحديد السعر
يمكن لتقطير مرشح ICE أن ينتج عشرات الرسائل في تعاقب سريع. على خادم مشغول، هذا يضيف. طبق تحديد السعر لكل مقبس:
import { RateLimiterMemory } from 'rate-limiter-flexible';
const rateLimiter = new RateLimiterMemory({
points: 50, // رسائل
duration: 10, // لكل 10 ثوانٍ
});
io.on('connection', (socket) => {
socket.use(async ([event, ...args], next) => {
try {
await rateLimiter.consume(socket.id);
next();
} catch {
next(new Error('Rate limit exceeded'));
}
});
});
4. المصادقة
لا تشغل خادم الإشارات بدون مصادقة في الإنتاج. مع Socket.io، استخدم البرامج الوسيطة:
io.use(async (socket, next) => {
const token = socket.handshake.auth.token;
try {
const user = await verifyJWT(token);
socket.data.userId = user.id;
next();
} catch {
next(new Error('Authentication failed'));
}
});
5. التوسع الأفقي
يمكن لعملية Node.js واحدة أن تتعامل مع آلاف اتصالات الإشارات (الإشارات خفيفة الوزن -- فقط رسائل نصية). عندما تحتاج إلى التوسع خارج خادم واحد، استخدم @socket.io/redis-adapter:
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
هذا يضمن أن socket.to(peerId).emit() يعمل عبر عدة نسخ من الخادم.
خوادم TURN: عندما تفشل الاتصالات المباشرة
يعمل STUN لحوالي 80-85% من الاتصالات. بالنسبة للـ 15-20% المتبقية (NATs المتماثلة، جدران الحماية المقيدة، شبكات الشركات)، تحتاج إلى خادم TURN لتوصيل حركة الوسائط.
في عام 2026، خياراتك لـ TURN تشمل:
| المزود | السعر (تقريبي) | ملاحظات |
|---|---|---|
| Cloudflare Calls TURN | طبقة مجانية متاحة، قائم على الاستخدام | أفضل قيمة لمعظم المشاريع |
| Twilio TURN | $0.40/GB | موثوق، موثق جيدًا |
| Metered.ca | 500GB/شهر مجانًا، ثم $0.40/GB | شهير لمشاريع المستقلين |
| موقع ذاتي coturn | تكاليف الخادم فقط | تحكم كامل، عبء تشغيلي |
| Xirsys | من $24.99/شهر | نقاط وجود عالمية |
قم دائمًا بتضمين خادم TURN واحد على الأقل في تكوين ICE الخاص بك. إخبار المستخدمين "يعمل بالنسبة لمعظم الناس" غير مقبول عندما يحاول نائب الرئيس عرض منتجك على WiFi الفندق.
إذا كنت تبني شيئًا يحتاج إلى العمل بشكل موثوق عبر شبكات المؤسسات، فيرجى التواصل معنا -- ساعدنا الفرق على بناء حلول WebRTC تتعامل مع حالات الحافة المعقدة.
الأسئلة الشائعة
ما هو خادم الإشارات في WebRTC؟ خادم الإشارات هو ناقل رسائل يساعد نظيرين من WebRTC في تبديل المعلومات التي يحتاجانها لإنشاء اتصال مباشر. يحمل عروض وإجابات SDP (التي تصف قدرات الوسائط) ومرشحات ICE (التي تصف مسارات الشبكة). لا يلمس خادم الإشارات أبدًا بيانات الصوت أو الفيديو -- بمجرد اتصال النظراء، تتدفق الوسائط مباشرة بينهما.
لماذا لا تتضمن WebRTC بروتوكول إشارات قياسي؟ تترك مواصفات WebRTC الإشارات غير محددة بقصد لأن معظم التطبيقات لديها بالفعل قناة اتصال بين المستخدمين. تطبيق الدردشة أو خدمة المطابقة أو أداة التعاون يمكنها إعادة استخدام البنية التحتية الموجودة للإشارات. هذا المرونة تعني أن يمكنك استخدام WebSockets أو HTTP أو SIP أو XMPP أو أي نقل آخر يمكنه حمل رسائل نصية.
ما هو الفرق بين عرض SDP وإجابة SDP؟ عرض SDP ينشأ من قبل النظير الذي يبدأ الاتصال. يصف قدرات الوسائط لذلك النظير -- المكودات المدعومة، وطرق التشفير، وأنواع الوسائط. إجابة SDP ينشأ من قبل النظير المستقبل وتؤكد أي قدرات يدعمها من العرض. معًا، يتفاوضان على المعاملات المشتركة اللذين سيستخدمهما كلا النظيرين لجلسة الوسائط.
ما هي مرشحات ICE ولماذا يجب أن يتم تبديلها؟ مرشحات ICE هي مسارات شبكة محتملة يمكن الوصول إليها من نظير. يكتشف كل نظير مرشحين عن طريق فحص واجهات الشبكة المحلية، والاستعلام من خوادم STUN للعناوين الموجهة للجمهور، وتخصيص عناوين الرجوع على خوادم TURN. يجب إرسال هذه المرشحات إلى النظير الآخر عبر خادم الإشارات حتى يتمكن كلا الطرفين من محاولة مسارات متعددة والعثور على واحد يعمل عبر NATs وجدران الحماية.
هل يمكنني استخدام Supabase Realtime كخادم إشارات WebRTC؟ نعم. ميزة البث في Supabase Realtime تعمل بشكل جيد للإشارات لأنها توفر تسليم رسائل منخفض الكمون بين العملاء المتصلين بدون الحاجة إلى أي كتابات قاعدة بيانات. تنشئ قناة لكل غرفة، وتبث رسائل العرض/الإجابة/ICE، وتصفي حسب معرّف النظير المستهدف من جانب العميل. إنها خيار قوي للمشاريع التي تستخدم بالفعل Supabase والتي تريد تجنب تشغيل خادم إشارات منفصل.
كم عدد الاتصالات المتزامنة التي يمكن لخادم الإشارات التعامل معها؟ يمكن لعملية Node.js واحدة + Socket.io عادة التعامل مع 10,000–50,000 اتصالات إشارات متزامنة، اعتمادًا على تكرار الرسائل وموارد الخادم. حركة الإشارات خفيفة الوزن -- فقط رسائل JSON صغيرة. للحجم الأكبر، استخدم محول Redis مع Socket.io لتوزيع الاتصالات عبر نسخ خادم متعددة. الاختناق في WebRTC نادرًا ما يكون خادم الإشارات؛ إنه عادة نطاق ترحيل TURN.
هل أحتاج إلى خادم TURN في الإنتاج؟ نعم. بينما يتعامل STUN مع حوالي 80-85% من الاتصالات، يجلس حوالي 15-20% من المستخدمين خلف NATs متماثلة أو جدران حماية مقيدة تمنع الاتصالات المباشرة. بدون خادم TURN، لا يمكن لهؤلاء المستخدمين ببساطة الاتصال. في بيئات المؤسسات، يمكن أن يكون معدل الفشل بدون TURN أعلى حتى. تقدم خدمات مثل Cloudflare Calls و Twilio و Metered.ca TURN مع طبقات مجانية.
ماذا يحدث إذا انقطع خادم الإشارات أثناء المكالمة النشطة؟ بمجرد إنشاء اتصال WebRTC، فهو لا يعتمد على خادم الإشارات بعد الآن. ستستمر المكالمة الموجودة في العمل إذا انهار خادم الإشارات. ومع ذلك، لا يمكن إنشاء اتصالات جديدة، وإذا احتاجت الاتصالات الموجودة إلى إعادة التفاوض (على سبيل المثال، إضافة مشاركة الشاشة)، فسيفشل هذا. يجب عليك تصميم خادم الإشارات الخاص بك لتوفر عالي باستخدام التجميع أو الخدمات المُدارة.