Direct Sockets

Demián Renzulli
Demián Renzulli
Andrew Rayskiy
Andrew Rayskiy
Vlad Krot
Vlad Krot

عادةً ما تكون تطبيقات الويب العادية محصورة ببروتوكولات اتصال معيّنة، مثل HTTP وواجهات برمجة التطبيقات، مثل WebSocket وWebRTC. وعلى الرغم من فعالية هذه الأدوات، تم تصميمها لتكون محدودة الإمكانات بشكل كبير من أجل منع إساءة استخدامها. ولا يمكنها إنشاء اتصالات TCP أو UDP غير معالَجة، ما يحدّ من قدرة تطبيقات الويب على التواصل مع الأنظمة القديمة أو الأجهزة التي تستخدم بروتوكولات غير بروتوكولات الويب. على سبيل المثال، قد تحتاج إلى إنشاء عميل SSH مستند إلى الويب أو الاتصال بطابعة محلية أو إدارة مجموعة من أجهزة إنترنت الأشياء. في السابق، كان ذلك يتطلّب إضافات للمتصفّح أو تطبيقات مساعدة أصلية.

تعالج واجهة برمجة التطبيقات Direct Sockets API هذا القيد من خلال السماح لتطبيقات الويب المعزولة بإنشاء اتصالات TCP وUDP مباشرةً بدون خادم وسيط. وبفضل إجراءات الأمان الإضافية، مثل سياسة أمان المحتوى (CSP) الصارمة والعزل بين المصادر، يمكن عرض واجهة برمجة التطبيقات هذه بأمان.

حالات الاستخدام

متى يجب استخدام Direct Sockets بدلاً من WebSockets العادية؟

  • أجهزة إنترنت الأشياء والأجهزة الذكية: التواصل مع أجهزة تستخدم بروتوكول TCP/UDP غير المعالَج بدلاً من HTTP
  • الأنظمة القديمة: الربط بخوادم البريد القديمة (SMTP/IMAP) أو خوادم محادثات IRC أو الطابعات
  • أجهزة الكمبيوتر الطرفية وأجهزة الكمبيوتر المكتبية البعيدة: تنفيذ برامج SSH أو Telnet أو RDP.
  • أنظمة P2P: تنفيذ جداول التجزئة الموزّعة (DHT) أو أدوات التعاون المرنة (مثل IPFS)
  • بث الوسائط: الاستفادة من بروتوكول UDP لبث المحتوى إلى نقاط نهاية متعددة في آنٍ واحد (البث المتعدد)، ما يتيح حالات استخدام مثل تشغيل الفيديو المنسّق على شبكة من أكشاك البيع بالتجزئة
  • إمكانات الخادم والاستماع: إعداد IWA ليعمل كنقطة نهاية استقبال لاتصالات TCP الواردة أو مخططات بيانات UDP باستخدام TCPServerSocket أو UDPSocket المرتبط.

المتطلبات الأساسية لاستخدام Direct Sockets

قبل استخدام Direct Sockets، عليك إعداد تطبيق ويب مثبت قابل للتثبيت. يمكنك بعد ذلك دمج Direct Sockets في صفحاتك.

إضافة سياسة الأذونات

لاستخدام Direct Sockets، عليك ضبط الكائن permissions_policy في ملف بيان تطبيق الويب المعزول. عليك إضافة المفتاح direct-sockets لتفعيل واجهة برمجة التطبيقات بشكل صريح. بالإضافة إلى ذلك، يجب تضمين المفتاح cross-origin-isolated. لا يرتبط هذا المفتاح بواجهة Direct Sockets API تحديدًا، ولكنّه مطلوب لجميع تطبيقات الويب المثبَّتة ويحدّد ما إذا كان بإمكان المستند الوصول إلى واجهات برمجة التطبيقات التي تتطلّب عزل المصادر المتعددة.

{
  "permissions_policy": {
    "direct-sockets": ["self"],
    "cross-origin-isolated": ["self"]
  }
}

يحدّد مفتاح direct-sockets ما إذا كان مسموحًا بإجراء مكالمات إلى new TCPSocket(...) أو new TCPServerSocket(...) أو new UDPSocket(...). في حال عدم ضبط هذه السياسة، سيتم رفض هذه الدوال الإنشائية على الفور مع ظهور الخطأ NotAllowedError.

تنفيذ TCPSocket

يمكن للتطبيقات طلب اتصال TCP من خلال إنشاء مثيل TCPSocket.

فتح اتصال

لفتح اتصال، استخدِم عامل التشغيل new وawait الوعد المفتوح.

يبدأ منشئ TCPSocket عملية الاتصال باستخدام remoteAddress وremotePort المحدّدين.

const remoteAddress = 'example.com';
const remotePort = 7;

// Configure options like keepAlive or buffering
const options = {
  keepAlive: true,
  keepAliveDelay: 720000
};

let tcpSocket = new TCPSocket(remoteAddress, remotePort, options);

// Wait for the connection to be established
let { readable, writable } = await tcpSocket.opened;

يتيح عنصر الإعدادات الاختياري التحكّم الدقيق في الشبكة. في هذه الحالة المحدّدة، تم ضبط keepAliveDelay على 720000 ملي ثانية للحفاظ على الاتصال خلال فترات عدم النشاط. يمكن للمطوّرين أيضًا ضبط خصائص أخرى هنا، مثل noDelay، الذي يوقف خوارزمية Nagle لمنع النظام من تجميع الحِزم الصغيرة، ما قد يقلّل من وقت الاستجابة، أو sendBufferSize وreceiveBufferSize لإدارة معدل نقل البيانات.

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

القراءة والكتابة

بعد فتح المقبس، تفاعَل معه باستخدام واجهات Streams API العادية.

  • الكتابة: يقبل مصدر البيانات القابل للكتابة BufferSource (مثل ArrayBuffer).
  • القراءة: ينتج مصدر البيانات القابل للقراءة بيانات Uint8Array.
// Writing data
const writer = writable.getWriter();
const encoder = new TextEncoder();
await writer.write(encoder.encode("Hello Server"));

// Call when done
writer.releaseLock();

// Reading data
const reader = readable.getReader();
const { value, done } = await reader.read();
if (!done) {
    const decoder = new TextDecoder();
    console.log("Received:", decoder.decode(value));
}

// Call when done
reader.releaseLock();

قراءة محسَّنة باستخدام ميزة "إحضار كتابك الخاص"

بالنسبة إلى التطبيقات العالية الأداء التي تكون فيها إدارة تخصيص الذاكرة أمرًا بالغ الأهمية، تتيح واجهة برمجة التطبيقات القراءة باستخدام ميزة "استخدام المخزن المؤقت الخاص بك" (BYOB). بدلاً من السماح للمتصفّح بتخصيص مخزن مؤقت جديد لكل جزء من البيانات التي يتم تلقّيها، يمكنك تمرير مخزن مؤقت مخصّص مسبقًا إلى القارئ. يقلّل ذلك من الحمل الزائد لعملية جمع البيانات غير المرغوب فيها من خلال كتابة البيانات مباشرةً في الذاكرة الحالية.

// 1. Get a BYOB reader explicitly
const reader = readable.getReader({ mode: 'byob' });

// 2. Allocate a reusable buffer (e.g., 4KB)
let buffer = new Uint8Array(4096);

// 3. Read directly into the existing buffer
const { value, done } = await reader.read(buffer);

if (!done) {
  // 'value' is a view of the data written directly into your buffer
  console.log("Bytes received:", value.byteLength);
}

reader.releaseLock();

تنفيذ UDPSocket

تتيح الفئة UDPSocket إمكانية التواصل عبر UDP. تعمل هذه الميزة بوضعَين مختلفَين، وذلك حسب طريقة ضبط الخيارات.

الوضع "متصل"

في هذا الوضع، يتواصل المقبس مع وجهة محدّدة واحدة. وهي مفيدة للمهام العادية التي تتطلّب تفاعلاً بين العميل والخادم.

// Connect to a specific remote host
let udpSocket = new UDPSocket({
    remoteAddress: 'example.com',
    remotePort: 7 });

let { readable, writable } = await udpSocket.opened;

الوضع المقيّد

في هذا الوضع، يتم ربط المقبس بنقطة نهاية IP محلية. ويمكنه تلقّي حزم البيانات من مصادر عشوائية وإرسالها إلى وجهات عشوائية. ويُستخدَم هذا النوع غالبًا لبروتوكولات البحث المحلية أو السلوك المشابه للخادم.

// Bind to all interfaces (IPv6)
let udpSocket = new UDPSocket({
    localAddress: '::'
    // omitting localPort lets the OS pick one
});

// localPort will tell you the OS-selected port.
let { readable, writable, localPort } = await udpSocket.opened;

معالجة رسائل UDP

على عكس دفق البايتات TCP، تتعامل دفقات UDP مع كائنات UDPMessage التي تحتوي على البيانات ومعلومات العنوان البعيد. يوضّح الرمز التالي كيفية التعامل مع عمليات الإدخال/الإخراج عند استخدام UDPSocket في "الوضع المرتبط".

// Writing (Bound Mode requires specifying destination)
const writer = writable.getWriter();
await writer.write({
    data: new TextEncoder().encode("Ping"),
    remoteAddress: '192.168.1.50',
    remotePort: 8080
});

// Reading
const reader = readable.getReader();
const { value } = await reader.read();
// value contains: { data, remoteAddress, remotePort }
console.log(`Received from ${value.remoteAddress}:`, value.data);

على عكس "وضع الاتصال" الذي يتم فيه قفل المقبس على جهاز معيّن، يتيح وضع الربط للمقبس التواصل مع وجهات عشوائية. وبالتالي، عند كتابة البيانات في مصدر البيانات القابل للكتابة، يجب تمرير عنصر UDPMessage يحدّد بشكل صريح remoteAddress وremotePort لكل حزمة، ما يوجّه المقبس إلى المكان الذي يجب توجيه حزمة البيانات المحددة إليه. وبالمثل، عند القراءة من مصدر البيانات القابل للقراءة، تتضمّن القيمة المعروضة ليس فقط حمولة البيانات، بل أيضًا remoteAddress وremotePort الخاصَين بالمرسِل، ما يتيح لتطبيقك تحديد مصدر كل حزمة واردة.

ملاحظة: عند استخدام UDPSocket في "وضع الاتصال"، يتم قفل المقبس بشكل فعّال على جهاز نظير معيّن، ما يؤدي إلى تبسيط عملية الإدخال والإخراج. في هذا الوضع، لن يكون للسمتَين remoteAddress وremotePort أي تأثير عند الكتابة، لأنّ الوجهة تكون ثابتة. وبالمثل، عند قراءة الرسائل، ستعرض هذه الخصائص القيمة null، لأنّ المصدر مضمون أن يكون الجهاز المتصل.

التوافق مع البث المتعدد

بالنسبة إلى حالات الاستخدام، مثل مزامنة تشغيل الفيديو على أكشاك متعددة أو تنفيذ ميزة "اكتشاف الأجهزة المحلية" (على سبيل المثال، mDNS)، تتيح Direct Sockets استخدام بروتوكول UDP للبث المتعدد. يتيح ذلك إرسال الرسائل إلى عنوان "مجموعة" واستلامها من قِبل جميع المشتركين على الشبكة، بدلاً من نظير واحد محدّد.

أذونات البث المتعدد

لاستخدام إمكانات البث المتعدد، يجب إضافة إذن direct-sockets-multicast المحدّد إلى بيان تطبيق الويب المثبَّت. يختلف هذا الإذن عن إذن المقابس المباشرة العادي، وهو ضروري لأنّ البث المتعدد يُستخدم فقط في الشبكات الخاصة.

{
  "permissions_policy": {
    "direct-sockets": ["self"],
    "direct-sockets-multicast": ["self"],
    "direct-sockets-private": ["self"],
    "cross-origin-isolated": ["self"]
  }
}

إرسال حزم بيانات البث المتعدد

يشبه الإرسال إلى مجموعة البث المتعدد وضع UDP العادي "المتصل"، مع إضافة خيارات محددة للتحكم في سلوك الحزمة.

const MULTICAST_GROUP = '239.0.0.1';
const PORT = 12345;

const socket = new UDPSocket({
  remoteAddress: MULTICAST_GROUP,
  remotePort: PORT,
  // Time To Live: How many router hops the packet can survive (default: 1)
  multicastTimeToLive: 5,
  // Loopback: Whether to receive your own packets (default: true)
  multicastLoopback: true
});

const { writable } = await socket.opened;
// Write to the stream as usual...

تلقّي حزم بيانات البث المتعدد

لتلقّي بيانات الإرسال المتعدّد، يجب فتح UDPSocket في "وضع الربط" (عادةً ما يتم الربط بـ 0.0.0.0 أو ::)، ثم الانضمام إلى مجموعة معيّنة باستخدام MulticastController. يمكنك أيضًا استخدام الخيار multicastAllowAddressSharing (مشابه للخيار SO_REUSEADDR في نظام التشغيل Unix)، وهو ضروري لبروتوكولات اكتشاف الأجهزة التي تحتاج فيها تطبيقات متعددة على الجهاز نفسه إلى الاستماع إلى المنفذ نفسه.

const socket = new UDPSocket({
  localAddress: '0.0.0.0', // Listen on all interfaces
  localPort: 12345,
  multicastAllowAddressSharing: true // Allow multiple applications to bind to the same address / port pair.
});

// The open info contains the MulticastController
const { readable, multicastController } = await socket.opened;

// Join the group to start receiving packets
await multicastController.joinGroup('239.0.0.1');

const reader = readable.getReader();

// Read the stream...
const { value } = await reader.read();
console.log(`Received multicast from ${value.remoteAddress}`);

// When finished, you can leave the group (this is an optional, but recommended practice)
await multicastController.leaveGroup('239.0.0.1');

إنشاء خادم

تتيح واجهة برمجة التطبيقات أيضًا استخدام TCPServerSocket لقبول اتصالات TCP الواردة، ما يسمح لتطبيق الويب المثبَّت بالعمل كخادم محلي. يوضّح الرمز التالي كيفية إنشاء خادم TCP باستخدام واجهة TCPServerSocket.

// Listen on all interfaces (IPv6)
let tcpServerSocket = new TCPServerSocket('::');

// Accept connections via the readable stream
let { readable } = await tcpServerSocket.opened;
let reader = readable.getReader();

// Wait for a client to connect
let { value: clientSocket } = await reader.read();

// 'clientSocket' is a standard TCPSocket you can now read/write to

من خلال إنشاء مثيل للفئة باستخدام العنوان '::'، يرتبط الخادم بجميع واجهات شبكة IPv6 المتاحة للاستماع إلى المحاولات الواردة. على عكس واجهات برمجة التطبيقات التقليدية للخادم المستندة إلى عمليات رد الاتصال، تستخدم واجهة برمجة التطبيقات هذه نمط Streams API على الويب: يتم تسليم الاتصالات الواردة كـ ReadableStream. عند استدعاء reader.read()، ينتظر التطبيق ويقبل الاتصال التالي من قائمة الانتظار، ويتم تحويله إلى قيمة تمثّل مثيلاً TCPSocket يعمل بكامل طاقته وجاهزًا للتواصل في الاتجاهين مع هذا العميل تحديدًا.

تصحيح أخطاء Direct Sockets باستخدام "أدوات مطوّري البرامج في Chrome"

بدءًا من الإصدار 138 من Chrome، يمكنك تصحيح أخطاء زيارات Direct Sockets مباشرةً ضمن لوحة الشبكة في "أدوات مطوّري البرامج في Chrome"، ما يغنيك عن استخدام أدوات خارجية لتتبُّع الحِزم. تتيح لك هذه الأدوات مراقبة عمليات ربط TCPSocket بالإضافة إلى عدد الزيارات UDPSocket (في الوضعَين المرتبط وغير المرتبط) إلى جانب طلبات HTTP العادية.

لفحص نشاط تطبيقك على الشبكة، اتّبِع الخطوات التالية:

  1. افتح لوحة الشبكة في "أدوات مطوّري البرامج في Chrome".
  2. ابحث عن اتصال المقبس واختَره في جدول الطلبات.
  3. افتح علامة التبويب الرسائل لعرض سجلّ بجميع البيانات المرسَلة والمستلَمة.

البيانات في علامة التبويب "الرسائل" ضمن "أدوات مطوّري البرامج"

يوفر هذا العرض أداة Hex Viewer، ما يتيح لك فحص حمولة البيانات الثنائية الأولية لرسائل TCP وUDP، ما يضمن تنفيذ البروتوكول بشكل مثالي.

عرض توضيحي

تتضمّن ميزات IWA Kitchen Sink تطبيقًا يتضمّن علامات تبويب متعددة، تعرض كل منها واجهة برمجة تطبيقات مختلفة لتطبيقات الويب المعزولة، مثل Direct Sockets وControlled Frame وغيرها.

بدلاً من ذلك، يحتوي عرض توضيحي لبرنامج telnet الخادم على تطبيق ويب معزول يتيح للمستخدم الاتصال بخادم TCP/IP من خلال وحدة طرفية تفاعلية. بعبارة أخرى، هو عميل Telnet.

الخاتمة

تسدّ Direct Sockets API فجوة كبيرة في الوظائف من خلال السماح لتطبيقات الويب بمعالجة بروتوكولات الشبكة الأولية التي كان من المستحيل توفيرها بدون برامج تضمين أصلية. وهي تتجاوز إمكانية الاتصال البسيط بالأجهزة، فباستخدام TCPServerSocket، يمكن للتطبيقات الاستماع إلى الاتصالات الواردة، بينما يوفّر UDPSocket أوضاعًا مرنة للتواصل بين الأجهزة واكتشاف الشبكات المحلية.

من خلال إتاحة إمكانات TCP وUDP الأساسية هذه من خلال واجهة برمجة التطبيقات الحديثة Streams API، يمكنك الآن إنشاء عمليات تنفيذ كاملة الميزات للبروتوكولات القديمة، مثل SSH أو RDP أو معايير إنترنت الأشياء المخصّصة، مباشرةً في JavaScript. وبما أنّ واجهة برمجة التطبيقات هذه تمنح إذن الوصول إلى الشبكة على مستوى منخفض، فإنّها تنطوي على تداعيات أمنية كبيرة. لذلك، يقتصر على تطبيقات الويب المعزولة (IWAs)، ما يضمن عدم منح هذه الإمكانية إلا للتطبيقات الموثوق بها التي تم تثبيتها بشكل صريح والتي تفرض سياسات أمان صارمة. ويتيح لك هذا التوازن إنشاء تطبيقات قوية تركّز على الأجهزة مع الحفاظ على مستوى الأمان الذي يتوقّعه المستخدمون من منصة الويب.

الموارد