WebSocketStream: שילוב של מקורות נתונים עם WebSocket API

ניתן למנוע מהאפליקציה לשקוע בהודעות של WebSocket או להציף שרת WebSocket בהודעות על ידי הפעלת לחיצה משנית.

תומאס שטיינר
תומאס סטיינר

רקע

WebSocket API

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

ה-API של Streams

באמצעות Streams API, JavaScript מאפשר לגשת באופן פרוגרמטי למקורות נתונים שהתקבלו דרך הרשת ולעבד אותם לפי הצורך. אחד מהעקרונות החשובים בהקשר של שידורים הוא backpressure (לחיצה לאחור). זהו התהליך שבו נחל יחיד או שרשרת צינורות מווסתים את מהירות הקריאה או הכתיבה. כשהשידור עצמו או השידור שבהמשך בשרשרת הצינור עדיין עמוס, ועדיין לא מוכן לקבל מקטעים נוספים, הוא שולח אות לאחור דרך השרשרת כדי להאט את המסירה בהתאם.

הבעיה ב-WebSocket API הנוכחי

לא ניתן להחיל לחיצה לאחור על הודעות שהתקבלו

באמצעות ה-WebSocket API הנוכחי, תגובה להודעה מתרחשת ב-WebSocket.onmessage, שנקרא EventHandler כאשר מתקבלת הודעה מהשרת.

נניח שיש לכם אפליקציה שצריכה לבצע פעולות כבדות של כיווץ נתונים בכל פעם שמתקבלת הודעה חדשה. סביר להניח שתרצו להגדיר את התהליך בדומה לקוד שבהמשך, ומכיוון שביצעתם await את התוצאה של הקריאה process(), סימן שאתם בסדר, נכון?

// A heavy data crunching operation.
const process = async (data) => {
  return new Promise((resolve) => {
    window.setTimeout(() => {
      console.log('WebSocket message processed:', data);
      return resolve('done');
    }, 1000);
  });
};

webSocket.onmessage = async (event) => {
  const data = event.data;
  // Await the result of the processing step in the message handler.
  await process(data);
};

לא נכון! הבעיה ב-WebSocket API הנוכחי היא שאין דרך להפעיל לחיצה לאחור. כשההודעות מגיעות מהר יותר מכפי ששיטת העיבוד process() יכולה לטפל בהן, תהליך העיבוד ימלא את הזיכרון על ידי אגירת הנתונים של ההודעות האלה, או שלא יגיבו בגלל שימוש ב-100% במעבד (CPU) או גם וגם.

הפעלת לחיצה לאחור על הודעות שנשלחו אינה ארגונומית

אפשר להפעיל לחיצה לאחור על הודעות שנשלחו, אבל תצטרכו לבצע דגימה של המאפיין WebSocket.bufferedAmount, שהוא לא יעיל ולא ארגונומי. המאפיין לקריאה בלבד מחזיר את מספר הבייטים שממתינים בתור באמצעות קריאות ל-WebSocket.send(), אבל עדיין לא הועברו לרשת. הערך מתאפס אחרי שליחת כל הנתונים שבתור, אבל אם תמשיכו להתקשר אל WebSocket.send(), הוא ימשיך לעלות.

מהו WebSocketStream API?

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

תרחישים לדוגמה לשימוש ב-WebSocketStream API

דוגמאות לאתרים שבהם ניתן להשתמש ב-API הזה:

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

הסטטוס הנוכחי

| שלב | סטטוס | | ------------------------------------------ | ---------------------------- | | 1. יוצרים הסבר | [Complete][explainer] | | 2. יוצרים טיוטה ראשונית של המפרט | [בתהליך][spec] | | 3. אוספים משוב ומשפרים את העיצוב | [בתהליך](#feedback) | | 4. גרסת מקור לניסיון | [Complete][ot] | | 5. השקה | לא התחיל |

איך משתמשים ב-WebSocketStream API

דוגמה ראשונית

ה-WebSocketStream API מבוסס על הבטחות, כך שהטיפול בו נראה טבעי בעולם ה-JavaScript המודרני. מתחילים בבניית WebSocketStream חדש והעברת כתובת ה-URL של שרת ה-WebSocket. לאחר מכן, מחכים שהחיבור יהיה opened, וכתוצאה מכך ייפתח ReadableStream ו/או WritableStream.

אחרי שתפעילו את השיטה ReadableStream.getReader(), אתם מקבלים בסופו של דבר ReadableStreamDefaultReader, ואז read() נתונים ממנו עד לסיום השידור, כלומר עד שמחזירים אובייקט מהטופס ({value: undefined, done: true}).

בהתאם לכך, כשאתם מפעילים את השיטה WritableStream.getWriter(), אתם מקבלים בסופו של דבר WritableStreamDefaultWriter, שאליו תוכלו write() נתונים.

  const wss = new WebSocketStream(WSS_URL);
  const {readable, writable} = await wss.opened;
  const reader = readable.getReader();
  const writer = writable.getWriter();

  while (true) {
    const {value, done} = await reader.read();
    if (done) {
      break;
    }
    const result = await process(value);
    await writer.write(result);
  }

לחץ אחורי

מה לגבי תכונת הלחץ לאחור שהובטחה? כפי שכתבתי למעלה, אפשר לקבל אותו "בחינם", ללא צורך בפעולות נוספות. אם יידרש ל-process() זמן נוסף, ההודעה הבאה תנוצל רק כשצינור עיבוד הנתונים יהיה מוכן. באופן דומה, השלב WritableStreamDefaultWriter.write() ימשיך רק אם אפשר לעשות זאת בצורה בטוחה.

דוגמאות מתקדמות

הארגומנט השני של WebSocketStream הוא תיק אפשרויות שמיועד להארכה עתידית. בשלב זה האפשרות היחידה היא protocols, שמתפקדת כמו הארגומנט השני של בנאי WebSocket:

const chatWSS = new WebSocketStream(CHAT_URL, {protocols: ['chat', 'chatv2']});
const {protocol} = await chatWSS.opened;

הערכים protocol שנבחרו וכן extensions פוטנציאליים הם חלק מהמילון שזמין דרך ההבטחה WebSocketStream.opened. ההבטחה הזו תספק לך את כל המידע על החיבור בזמן אמת, כי הוא לא רלוונטי במקרה שהחיבור נכשל.

const {readable, writable, protocol, extensions} = await chatWSS.opened;

מידע על חיבור WebSocketStream סגור

המידע שהיה זמין מהאירועים WebSocket.onclose ו-WebSocket.onerror ב-WebSocket API זמין עכשיו דרך ההבטחה WebSocketStream.closed. ההבטחה נדחית במקרה של סגירה לא נקייה, אבל היא תואמת לקוד ולסיבה שנשלחו על ידי השרת.

ברשימה של קודי הסטטוס CloseEvent יש הסבר על כל קודי הסטטוס האפשריים והמשמעות שלהם.

const {code, reason} = await chatWSS.closed;

סגירת חיבור WebSocketStream

אפשר לסגור WebSocketStream באמצעות AbortController. לכן, מעבירים את AbortSignal לבנאי WebSocketStream.

const controller = new AbortController();
const wss = new WebSocketStream(URL, {signal: controller.signal});
setTimeout(() => controller.abort(), 1000);

אפשר גם להשתמש בשיטה WebSocketStream.close(), אבל המטרה העיקרית שלה היא לאפשר לציין את הקוד ואת הסיבה שיישלחו לשרת.

wss.close({code: 4000, reason: 'Game over'});

שיפור הדרגתי ויכולת פעולה הדדית

נכון לעכשיו, Chrome הוא הדפדפן היחיד שמטמיע את ממשק ה-API של WebSocketStream. ליכולת פעולה הדדית עם WebSocket API הקלאסי, לא ניתן להחיל לחץ לאחור על הודעות שהתקבלו. אפשר להפעיל לחיצה לאחור על הודעות שנשלחו, אבל תצטרכו לבצע דגימה של המאפיין WebSocket.bufferedAmount, שהוא לא יעיל ולא ארגונומי.

זיהוי תכונות

כדי לבדוק אם WebSocketStream API נתמך, צריך להשתמש ב:

if ('WebSocketStream' in window) {
  // `WebSocketStream` is supported!
}

הדגמה (דמו)

בדפדפנים תומכים, אפשר לראות איך WebSocketStream API פועל ב-iframe המוטמע או ישירות ב-Glitch.

משוב

צוות Chrome רוצה לשמוע על החוויה שלך עם WebSocketStream API.

נשמח לקבל ממך מידע על תכנון ה-API

האם יש משהו ב-API שלא פועל כצפוי? או האם חסרות שיטות או מאפיינים שנדרשים ליישום הרעיון שלך? יש לך שאלה או הערה לגבי מודל האבטחה? שולחים בעיה במפרט במאגר המתאים ב-GitHub או כותבים את דעתכם על בעיה קיימת.

דיווח על בעיה בהטמעה

האם מצאת באג בהטמעה של Chrome? או שההטמעה שונה מהמפרט? אפשר לדווח על באג בכתובת new.crbug.com. חשוב לכלול כמה שיותר פרטים, הוראות פשוטות לשחזור, ולהזין את הערך Blink>Network>WebSockets בתיבה Components. Glitch היא כלי מעולה לשיתוף של מקרי שחזור מהירים וקלים.

הבעת תמיכה ב-API

האם בכוונתך להשתמש ב-WebSocketStream API? התמיכה הציבורית שלכם עוזרת לצוות Chrome לתעדף את התכונות, ומראה לספקי דפדפנים אחרים עד כמה חיוני התמיכה בהן.

שלח ציוץ אל @ChromiumDev באמצעות ה-hashtag #WebSocketStream וספר לנו איפה ואיך אתם משתמשים בו.

קישורים שימושיים

אישורים

ה-WebSocketStream API הוטמע על ידי אדם רייס ו-Yutaka Hirano. תמונה ראשית (Hero) מאת Daan Mooij באתר Unwash.