سوکت‌های مستقیم

دمیان رنزولی
Demián Renzulli
اندرو رایسکی
Andrew Rayskiy
ولاد کروت
Vlad Krot

برنامه‌های وب استاندارد معمولاً به پروتکل‌های ارتباطی خاصی مانند HTTP و APIهایی مانند WebSocket و WebRTC محدود می‌شوند. اگرچه این‌ها قدرتمند هستند، اما طوری طراحی شده‌اند که برای جلوگیری از سوءاستفاده، کاملاً محدود باشند. آن‌ها نمی‌توانند اتصالات خام TCP یا UDP برقرار کنند، که این امر توانایی برنامه‌های وب را برای برقراری ارتباط با سیستم‌ها یا دستگاه‌های سخت‌افزاری قدیمی که از پروتکل‌های غیر وب خود استفاده می‌کنند، محدود می‌کند. به عنوان مثال، ممکن است بخواهید یک کلاینت SSH مبتنی بر وب بسازید، به یک چاپگر محلی متصل شوید یا ناوگانی از دستگاه‌های IoT را مدیریت کنید. از نظر تاریخی، این کار به افزونه‌های مرورگر یا برنامه‌های کمکی بومی نیاز داشت.

رابط برنامه‌نویسی کاربردی سوکت‌های مستقیم (Direct Sockets API) با فعال کردن برنامه‌های وب ایزوله (IWA) برای ایجاد اتصالات مستقیم TCP و UDP بدون سرور رله، این محدودیت را برطرف می‌کند. با IWAها، به لطف اقدامات امنیتی اضافی - مانند سیاست امنیتی محتوا (CSP) دقیق و جداسازی متقابل - این API می‌تواند با خیال راحت در معرض دید قرار گیرد.

موارد استفاده

چه زمانی باید از Direct Sockets به جای WebSockets استاندارد استفاده کنید؟

  • اینترنت اشیا و دستگاه‌های هوشمند: ارتباط با سخت‌افزاری که به جای HTTP از TCP/UDP خام استفاده می‌کند.
  • سیستم‌های قدیمی: اتصال به سرورهای ایمیل قدیمی (SMTP/IMAP)، سرورهای چت IRC یا چاپگرها.
  • دسکتاپ و ترمینال‌های ریموت: پیاده‌سازی کلاینت‌های SSH، Telnet یا RDP
  • سیستم‌های P2P: پیاده‌سازی جداول هش توزیع‌شده (DHT) یا ابزارهای همکاری انعطاف‌پذیر (مانند IPFS).
  • پخش رسانه‌ای: استفاده از UDP برای پخش همزمان محتوا به چندین نقطه پایانی (چندپخشی)، که موارد استفاده‌ای مانند پخش هماهنگ ویدیو در شبکه‌ای از کیوسک‌های خرده‌فروشی را امکان‌پذیر می‌کند.
  • قابلیت‌های سرور و شنونده: پیکربندی IWA برای عمل به عنوان یک نقطه پایانی دریافت‌کننده برای اتصالات TCP یا دیتاگرام‌های UDP ورودی با استفاده از TCPServerSocket یا UDPSocket متصل.

پیش‌نیازهای سوکت‌های مستقیم

قبل از استفاده از Direct Sockets، باید یک IWA کاربردی راه‌اندازی کنید . سپس می‌توانید Direct Sockets را در صفحات خود ادغام کنید.

افزودن خط‌مشی مجوز

برای استفاده از Direct Sockets، باید شیء permissions_policy را در مانیفست IWA خود پیکربندی کنید. برای فعال کردن صریح API، باید کلید direct-sockets را اضافه کنید. علاوه بر این، باید کلید cross-origin-isolated را نیز وارد کنید. این کلید مختص Direct Sockets نیست، اما برای همه IWAها مورد نیاز است و تعیین می‌کند که آیا سند می‌تواند به APIهایی که نیاز به جداسازی cross-origin دارند دسترسی داشته باشد یا خیر.

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

کلید direct-sockets تعیین می‌کند که آیا فراخوانی‌های new TCPSocket(...) ، new TCPServerSocket(...) یا new UDPSocket(...) مجاز هستند یا خیر. اگر این خط‌مشی تنظیم نشود، این سازنده‌ها بلافاصله با NotAllowedError رد می‌شوند.

پیاده‌سازی TCPSocket

برنامه‌ها می‌توانند با ایجاد یک نمونه TCPSocket درخواست اتصال TCP کنند.

باز کردن یک اتصال

برای باز کردن یک اتصال، از عملگر new استفاده کنید و await promise باز شده بمانید.

سازنده 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 روی ۷۲۰۰۰۰ میلی‌ثانیه تنظیم شده است تا اتصال را در دوره‌های عدم فعالیت حفظ کند. توسعه‌دهندگان همچنین می‌توانند ویژگی‌های دیگری را در اینجا پیکربندی کنند، مانند noDelay که الگوریتم Nagle را غیرفعال می‌کند تا سیستم از دسته‌بندی بسته‌های کوچک - که به طور بالقوه باعث کاهش تأخیر می‌شود - جلوگیری کند، یا sendBufferSize و receiveBufferSize را برای مدیریت توان عملیاتی تنظیم کنند.

در بخش آخر قطعه کد قبلی، کد در انتظار promise باز شده است که تنها پس از تکمیل handshake اجرا می‌شود و یک شیء 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

برای برنامه‌های کاربردی با کارایی بالا که مدیریت تخصیص حافظه بسیار مهم است، API از خواندن "BYOB" (Bring Your Own Buffer) پشتیبانی می‌کند. به جای اینکه به مرورگر اجازه دهید برای هر قطعه داده دریافتی، یک بافر جدید اختصاص دهد، می‌توانید یک بافر از پیش اختصاص داده شده را به خواننده منتقل کنید. این کار با نوشتن مستقیم داده‌ها در حافظه موجود، سربار جمع‌آوری زباله را کاهش می‌دهد.

// 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 در "حالت متصل"، سوکت عملاً به یک همتای خاص قفل می‌شود و فرآیند I/O را ساده می‌کند. در این حالت، ویژگی‌های remoteAddress و remotePort هنگام نوشتن عملاً بدون عملیات هستند، زیرا مقصد از قبل مشخص شده است. به طور مشابه، هنگام خواندن پیام‌ها، این ویژگی‌ها مقدار null را برمی‌گردانند، زیرا تضمین می‌شود که منبع، همتای متصل باشد.

پشتیبانی چندپخشی

برای مواردی مانند همگام‌سازی پخش ویدیو در چندین کیوسک یا پیاده‌سازی کشف دستگاه محلی (برای مثال، mDNS)، Direct Sockets از Multicast UDP پشتیبانی می‌کند. این امر به پیام‌ها اجازه می‌دهد تا به یک آدرس "گروهی" ارسال شوند و توسط همه مشترکین در شبکه دریافت شوند، نه یک همتای خاص.

مجوزهای چندپخشی

برای استفاده از قابلیت‌های چندپخشی، باید مجوز خاص direct-sockets-multicast را به مانیفست IWA خود اضافه کنید. این با مجوز استاندارد direct-sockets متفاوت است و ضروری است زیرا چندپخشی فقط در شبکه‌های خصوصی استفاده می‌شود.

{
  "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 در یونیکس) استفاده کنید، که برای پروتکل‌های کشف دستگاه که در آن‌ها چندین برنامه در یک دستگاه باید به یک پورت گوش دهند، ضروری است.

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');

ایجاد یک سرور

این API همچنین از TCPServerSocket برای پذیرش اتصالات TCP ورودی پشتیبانی می‌کند و عملاً به IWA شما اجازه می‌دهد تا به عنوان یک سرور محلی عمل کند. کد زیر نحوه ایجاد یک سرور 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 موجود متصل می‌شود تا به تلاش‌های ورودی گوش دهد. برخلاف APIهای سرور مبتنی بر فراخوانی سنتی، این API از الگوی Streams API وب استفاده می‌کند: اتصالات ورودی به صورت ReadableStream ارائه می‌شوند. وقتی reader.read() را فراخوانی می‌کنید، برنامه منتظر اتصال بعدی از صف می‌ماند و آن را می‌پذیرد و به مقداری تبدیل می‌کند که یک نمونه TCPSocket کاملاً کاربردی است که برای ارتباط دو طرفه با آن کلاینت خاص آماده است.

اشکال‌زدایی سوکت‌های مستقیم با Chrome DevTools

از کروم ۱۳۸، می‌توانید ترافیک Direct Sockets را مستقیماً در پنل Network در Chrome DevTools اشکال‌زدایی کنید و نیاز به شنودگرهای بسته خارجی را از بین ببرید. این ابزار به شما امکان می‌دهد اتصالات TCPSocket و همچنین ترافیک UDPSocket (در هر دو حالت متصل و متصل) را در کنار درخواست‌های HTTP استاندارد خود رصد کنید.

برای بررسی فعالیت شبکه برنامه خود:

  1. پنل شبکه را در Chrome DevTools باز کنید.
  2. اتصال سوکت را در جدول درخواست‌ها پیدا کرده و انتخاب کنید.
  3. برای مشاهده گزارش تمام داده‌های ارسالی و دریافتی، برگه پیام‌ها را باز کنید.

داده‌های موجود در تب پیام‌ها در DevTools.

این نما یک نمایشگر هگز (Hex Viewer) ارائه می‌دهد که به شما امکان می‌دهد بار داده‌ی باینری خام پیام‌های TCP و UDP خود را بررسی کنید و از بی‌نقص بودن پیاده‌سازی پروتکل خود اطمینان حاصل کنید.

نسخه آزمایشی

سینک آشپزخانه IWA دارای برنامه‌ای با چندین تب است که هر کدام یک API IWA متفاوت مانند سوکت‌های مستقیم، قاب کنترل‌شده و موارد دیگر را نشان می‌دهند.

از طرف دیگر، نسخه آزمایشی کلاینت تل‌نت شامل یک برنامه وب ایزوله است که به کاربر اجازه می‌دهد از طریق یک ترمینال تعاملی به یک سرور TCP/IP متصل شود. به عبارت دیگر، یک کلاینت تل‌نت.

نتیجه‌گیری

رابط برنامه‌نویسی کاربردی Direct Sockets با قادر ساختن برنامه‌های وب به مدیریت پروتکل‌های خام شبکه که قبلاً بدون پوشش‌دهنده‌های بومی پشتیبانی از آنها غیرممکن بود، یک شکاف عملکردی حیاتی را پر می‌کند. این فراتر از اتصال ساده کلاینت است؛ با TCPServerSocket ، برنامه‌ها می‌توانند به اتصالات ورودی گوش دهند، در حالی که UDPSocket حالت‌های انعطاف‌پذیری را هم برای ارتباط نظیر به نظیر و هم برای کشف شبکه محلی ارائه می‌دهد.

با افشای این قابلیت‌های خام TCP و UDP از طریق API مدرن Streams، اکنون می‌توانید پیاده‌سازی‌های کاملی از پروتکل‌های قدیمی - مانند SSH، RDP یا استانداردهای سفارشی IoT - را مستقیماً در جاوا اسکریپت بسازید. از آنجا که این API دسترسی سطح پایین به شبکه را اعطا می‌کند، پیامدهای امنیتی قابل توجهی را به همراه دارد. بنابراین، به برنامه‌های وب ایزوله (IWA) محدود شده است و تضمین می‌کند که چنین قدرتی فقط به برنامه‌های قابل اعتماد و صریح نصب شده که سیاست‌های امنیتی سختگیرانه‌ای را اعمال می‌کنند، اعطا می‌شود. این تعادل به شما امکان می‌دهد برنامه‌های قدرتمند و دستگاه محور بسازید و در عین حال ایمنی مورد انتظار کاربران از پلتفرم وب را حفظ کنید.

منابع