Direct Sockets

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

בדרך כלל, אפליקציות אינטרנט רגילות מוגבלות לפרוטוקולי תקשורת ספציפיים כמו HTTP ולממשקי API כמו WebSocket ו-WebRTC. הכלים האלה עוצמתיים, אבל הם מתוכננים להיות מוגבלים מאוד כדי למנוע שימוש לרעה. הם לא יכולים ליצור חיבורי TCP או UDP גולמיים, מה שמגביל את היכולת של אפליקציות אינטרנט לתקשר עם מערכות מדור קודם או עם מכשירי חומרה שמשתמשים בפרוטוקולים משלהם שאינם פרוטוקולי אינטרנט. לדוגמה, יכול להיות שתרצו ליצור לקוח SSH מבוסס-אינטרנט, להתחבר למדפסת מקומית או לנהל קבוצה של מכשירי IoT. בעבר, היה צורך בתוספים לדפדפן או באפליקציות עזר מקומיות.

‫Direct Sockets API פותר את הבעיה הזו בכך שהוא מאפשר לאפליקציות אינטרנט מבודדות (IWA) ליצור חיבורי TCP ו-UDP ישירים בלי שרת ממסר. בזכות אמצעי אבטחה נוספים, כמו Content Security Policy‏ (CSP) מחמירה ובידוד בין מקורות שונים, אפשר לחשוף את ה-API הזה בבטחה.

תרחישים לדוגמה

מתי כדאי להשתמש ב-Direct Sockets במקום ב-WebSockets רגילים?

  • מכשירי IoT ומכשירים חכמים: תקשורת עם חומרה שמשתמשת ב-TCP/UDP גולמי ולא ב-HTTP.
  • מערכות מדור קודם: התחברות לשרתי אימייל ישנים יותר (SMTP/IMAP), לשרתי צ'אט IRC או למדפסות.
  • טרמינלים ומחשבים שולחניים מרוחקים: הטמעה של לקוחות SSH,‏ Telnet או RDP.
  • מערכות P2P: הטמעה של טבלאות גיבוב מבוזרות (DHT) או של כלי שיתוף פעולה עמידים (כמו IPFS).
  • שידור מדיה: שימוש ב-UDP כדי להזרים תוכן לכמה נקודות קצה בו-זמנית (multicasting), מה שמאפשר תרחישי שימוש כמו הפעלת וידאו מתואמת ברשת של קיוסקים קמעונאיים.
  • יכולות של שרת ומאזין: הגדרת IWA כך שיפעל כנקודת קצה לקבלת חיבורי TCP נכנסים או חבילות נתונים של UDP באמצעות TCPServerSocket או UDPSocket מאוגד.

דרישות מוקדמות לשימוש ב-Direct Sockets

לפני שמשתמשים ב-Direct Sockets, צריך להגדיר IWA פעיל. לאחר מכן תוכלו לשלב את Direct Sockets בדפים שלכם.

הוספת מדיניות הרשאות

כדי להשתמש ב-Direct Sockets, צריך להגדיר את האובייקט permissions_policy במניפסט של אפליקציית האינטרנט המבודדת. כדי להפעיל את ה-API באופן מפורש, צריך להוסיף את המפתח 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 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 מוגדר ל-720,000 מילישניות כדי לשמור על החיבור בתקופות של חוסר פעילות. מפתחים יכולים גם להגדיר כאן מאפיינים אחרים, כמו noDelay, שמשבית את האלגוריתם של Nagle כדי למנוע מהמערכת לאגד מנות קטנות – מה שעשוי להפחית את זמן האחזור – או sendBufferSize ו-receiveBufferSize כדי לנהל את קצב העברת הנתונים.

בחלק האחרון של קטע הקוד הקודם, הקוד ממתין להבטחה שנפתחה, שמוחזרת רק אחרי השלמת הלחיצה, ומחזיר אובייקט TCPSocketOpenInfo שמכיל את הזרמים שניתנים לקריאה ולכתיבה שנדרשים להעברת נתונים.

קריאה וכתיבה

אחרי שהסוקט נפתח, אפשר לקיים איתו אינטראקציה באמצעות ממשקי Streams API רגילים.

  • כתיבה: הזרם שניתן לכתיבה מקבל BufferSource (כמו ArrayBuffer).
  • Reading: הזרם שניתן לקריאה מניב נתונים של 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 (הבאת מאגר משלך). במקום לאפשר לדפדפן להקצות מאגר חדש לכל נתח נתונים שמתקבל, אפשר להעביר מאגר שהוקצה מראש לקורא. כך מצמצמים את התקורה של איסוף האשפה על ידי כתיבת נתונים ישירות לזיכרון הקיים.

// 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 תומך ב-Multicast UDP. כך אפשר לשלוח הודעות לכתובת של 'קבוצה' ולקבל אותן אצל כל המנויים ברשת, ולא רק אצל עמית ספציפי.

הרשאות מולטיקאסט

כדי להשתמש ביכולות של שידור מרובה משתתפים, צריך להוסיף את ההרשאה הספציפית direct-sockets-multicast למניפסט של ה-IWA. ההרשאה הזו שונה מההרשאה הרגילה לשימוש בשקעים ישירים, והיא נדרשת כי נעשה שימוש בשידור מרובה משתתפים רק ברשתות פרטיות.

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

יצירת שרת

ה-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 מסורתיים של שרתים שמבוססים על קריאה חוזרת (callback), ה-API הזה משתמש בתבנית של Streams API באינטרנט: חיבורים נכנסים מועברים כ-ReadableStream. כשמבצעים קריאה ל-reader.read(), האפליקציה מחכה לחיבור הבא מהתור ומאשרת אותו, ומחזירה ערך שהוא מופע TCPSocket שמוכן לתקשורת דו-כיוונית עם הלקוח הספציפי הזה.

ניפוי באגים של Direct Sockets באמצעות כלי הפיתוח ל-Chrome

החל מגרסה Chrome 138, אפשר לנפות באגים בתנועה של Direct Sockets ישירות בחלונית Network בכלי הפיתוח ל-Chrome, בלי להשתמש בכלי חיצוני לניתוח חבילות. הכלים האלה מאפשרים לכם לעקוב אחרי חיבורי TCPSocket וגם אחרי תנועת UDPSocket (במצב מאוגד ובמצב מחובר) לצד בקשות HTTP רגילות.

כדי לבדוק את פעילות הרשת של האפליקציה:

  1. פותחים את החלונית Network בכלי הפיתוח ל-Chrome.
  2. מאתרים את חיבור השקע בטבלת הבקשות ובוחרים אותו.
  3. פותחים את הכרטיסייה הודעות כדי לראות יומן של כל הנתונים שהועברו והתקבלו.

הנתונים בכרטיסייה Messages בכלי הפיתוח.

בתצוגה הזו יש כלי לבדיקת הקסדצימלי, שמאפשר לכם לבדוק את מטען הנתונים הבינארי הגולמי של הודעות TCP ו-UDP, כדי לוודא שההטמעה של הפרוטוקול מדויקת.

הדגמה (דמו)

ב-IWA Kitchen Sink יש אפליקציה עם כמה כרטיסיות, שכל אחת מהן מדגימה API אחר של IWA, כמו Direct Sockets,‏ Controlled Frame ועוד.

לחלופין, הדמו של לקוח telnet כולל אפליקציית אינטרנט מבודדת שמאפשרת למשתמש להתחבר לשרת TCP/IP דרך טרמינל אינטראקטיבי. במילים אחרות, לקוח Telnet.

סיכום

Direct Sockets API מאפשר לאפליקציות אינטרנט לטפל בפרוטוקולי רשת גולמיים, שבעבר לא היה אפשר לתמוך בהם בלי עטיפות מקוריות. הוא לא מסתפק בקישוריות פשוטה של לקוח, אלא מאפשר לאפליקציות להאזין לחיבורים נכנסים, ומציע מצבים גמישים לתקשורת בין עמיתים ולגילוי רשת מקומית.TCPServerSocketUDPSocket

היכולות האלה של TCP ו-UDP גלויות דרך Streams API המודרני, כך שאפשר עכשיו ליצור הטמעות מלאות של פרוטוקולים מדור קודם – כמו SSH,‏ RDP או תקנים מותאמים אישית של IoT – ישירות ב-JavaScript. לממשק ה-API הזה יש השלכות משמעותיות על האבטחה, כי הוא מעניק גישה לרשת ברמה נמוכה. לכן, הגישה מוגבלת לאפליקציות אינטרנט מבודדות (IWA), כדי להבטיח שההרשאה הזו תינתן רק לאפליקציות מהימנות שהותקנו באופן מפורש ומיישמות מדיניות אבטחה מחמירה. האיזון הזה מאפשר לכם ליצור אפליקציות עוצמתיות שמתמקדות במכשיר, תוך שמירה על הבטיחות שהמשתמשים מצפים לה מפלטפורמת האינטרנט.

משאבים