WebSocketStream: การผสานรวมสตรีมกับ WebSocket API

ป้องกันไม่ให้แอปของคุณจมอยู่กับข้อความ WebSocket หรือท่วมเซิร์ฟเวอร์ WebSocket ด้วยข้อความโดยใช้การควบคุมอัตราการส่ง

ฉากหลัง

WebSocket API มีอินเทอร์เฟซ JavaScript สำหรับโปรโตคอล WebSocket ซึ่งช่วยให้เปิดเซสชันการสื่อสารแบบโต้ตอบ 2 ทาง ระหว่างเบราว์เซอร์ของผู้ใช้กับเซิร์ฟเวอร์ได้ API นี้ช่วยให้คุณส่งข้อความไปยังเซิร์ฟเวอร์และรับการตอบกลับที่ขับเคลื่อนด้วยเหตุการณ์ได้ โดยไม่ต้องสำรวจเซิร์ฟเวอร์เพื่อหาคำตอบ

Streams API

Streams API ช่วยให้ JavaScript เข้าถึงสตรีมของกลุ่มข้อมูลที่ได้รับผ่านเครือข่ายแบบเป็นโปรแกรม และประมวลผลได้ตามต้องการ แนวคิดที่สำคัญในบริบทของสตรีมคือแรงดันย้อนกลับ นี่คือกระบวนการที่สตรีมเดียวหรือเชนของไปป์ จะควบคุมความเร็วในการอ่านหรือเขียน เมื่อสตรีมเองหรือสตรีมในภายหลังในห่วงโซ่ไปป์ไลน์ยังคงทำงานอยู่ และยังไม่พร้อมรับก้อนข้อมูลเพิ่มเติม สตรีมจะส่งสัญญาณย้อนกลับผ่านห่วงโซ่เพื่อชะลอการนำส่งตามความเหมาะสม

ปัญหาเกี่ยวกับ 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()จะจัดการได้ กระบวนการแสดงผลจะเติมหน่วยความจำโดยการบัฟเฟอร์ข้อความเหล่านั้น ไม่ตอบสนองเนื่องจาก CPU ใช้งาน 100% หรือทั้ง 2 อย่าง

การใช้แรงดันย้อนกลับกับข้อความที่ส่งแล้วนั้นไม่สะดวก

การใช้แรงดันย้อนกลับกับข้อความที่ส่งเป็นไปได้ แต่ต้องมีการสำรวจพร็อพเพอร์ตี้ WebSocket.bufferedAmount ซึ่งไม่มีประสิทธิภาพและไม่สะดวก พร็อพเพอร์ตี้แบบอ่านอย่างเดียวนี้จะแสดงผลจำนวนไบต์ของข้อมูลที่เข้าคิว โดยใช้การเรียกไปยัง WebSocket.send() แต่ยังไม่ได้ส่งไปยังเครือข่าย ค่านี้จะรีเซ็ตเป็น 0 เมื่อส่งข้อมูลที่จัดคิวไว้ทั้งหมดแล้ว แต่หากคุณเรียกใช้ WebSocket.send() ต่อไป ค่านี้ก็จะเพิ่มขึ้นเรื่อยๆ

WebSocketStream API คืออะไร

WebSocketStream API แก้ปัญหาแรงดันย้อนกลับที่ไม่มีอยู่หรือไม่มีประสิทธิภาพ โดยการผสานรวมสตรีมกับ WebSocket API ซึ่งหมายความว่าสามารถใช้แรงดันย้อนกลับได้ "ฟรี" โดยไม่มีค่าใช้จ่ายเพิ่มเติม

กรณีการใช้งานที่แนะนำสำหรับ WebSocketStream API

ตัวอย่างเว็บไซต์ที่ใช้ API นี้ได้มีดังนี้

  • แอปพลิเคชัน WebSocket ที่มีแบนด์วิดท์สูงซึ่งต้องรักษาความสามารถในการโต้ตอบ โดยเฉพาะอย่างยิ่งวิดีโอและการแชร์หน้าจอ
  • ในทำนองเดียวกัน การจับภาพวิดีโอและแอปพลิเคชันอื่นๆ ที่สร้างข้อมูลจำนวนมากในเบราว์เซอร์ ซึ่งต้องอัปโหลดไปยังเซิร์ฟเวอร์ เมื่อใช้ Backpressure ไคลเอ็นต์จะหยุดสร้างข้อมูลแทนที่จะสะสมข้อมูลไว้ในหน่วยความจำ

สถานะปัจจุบัน

ขั้นตอน สถานะ
1. สร้างคำอธิบาย เสร็จสมบูรณ์
2. สร้างร่างข้อกำหนดเบื้องต้น กำลังดำเนินการ
3. รวบรวมความคิดเห็นและทำซ้ำการออกแบบ กำลังดำเนินการ
4. ช่วงทดลองใช้จากต้นทาง เสร็จสมบูรณ์
5. เปิดตัว ยังไม่เริ่ม

วิธีใช้ WebSocketStream API

WebSocketStream API เป็นแบบ Promise ซึ่งทำให้การจัดการ API นี้เป็นไปอย่างเป็นธรรมชาติ ในโลก JavaScript สมัยใหม่ คุณเริ่มต้นด้วยการสร้าง WebSocketStream ใหม่และส่ง URL ของเซิร์ฟเวอร์ WebSocket ไปยัง WebSocketStream จากนั้นรอให้การเชื่อมต่อเป็น 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() ขั้นตอน จะดำเนินการต่อเมื่อทำได้อย่างปลอดภัยเท่านั้น

ตัวอย่างขั้นสูง

อาร์กิวเมนต์ที่ 2 ของ WebSocketStream คือออบเจ็กต์ตัวเลือกที่อนุญาตให้ขยายในอนาคต ตัวเลือกเดียวคือ protocols ซึ่งทำงานเหมือนกับ อาร์กิวเมนต์ที่ 2 ของตัวสร้าง WebSocket

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

protocolที่เลือกไว้ รวมถึงextensionsที่เป็นไปได้จะอยู่ในพจนานุกรม ที่พร้อมใช้งานผ่านพรอมิส WebSocketStream.opened ข้อมูลทั้งหมดเกี่ยวกับการเชื่อมต่อแบบสดจะมาจาก Promise นี้ เนื่องจากจะไม่เกี่ยวข้องหากการเชื่อมต่อล้มเหลว

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

ข้อมูลเกี่ยวกับการเชื่อมต่อ WebSocketStream ที่ปิดแล้ว

ข้อมูลที่เคยใช้ได้จากเหตุการณ์ WebSocket.onclose และ WebSocket.onerror ใน WebSocket API ตอนนี้พร้อมใช้งานผ่านสัญญา WebSocketStream.closed แล้ว Promise จะปฏิเสธในกรณีที่ปิดการเชื่อมต่อไม่ถูกต้อง มิเช่นนั้นจะเปลี่ยนเป็นรหัสและเหตุผลที่เซิร์ฟเวอร์ส่งมา

รหัสสถานะทั้งหมดที่เป็นไปได้และความหมายของรหัสสถานะจะอธิบายไว้ใน รายการรหัสสถานะCloseEvent

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

การปิดการเชื่อมต่อ WebSocketStream

ปิด WebSocketStream ได้ด้วย AbortController ดังนั้น ให้ส่ง AbortSignal ไปยังตัวสร้าง WebSocketStream AbortController.abort() จะทำงานก่อนแฮนด์เชคเท่านั้น ไม่ใช่หลังจากนั้น

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

หรือคุณจะใช้วิธี WebSocketStream.close() ก็ได้ แต่จุดประสงค์หลักของวิธีนี้คือการอนุญาตให้ระบุรหัส และเหตุผลที่จะส่งไปยังเซิร์ฟเวอร์

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

การเพิ่มประสิทธิภาพแบบต่อเนื่องและการทำงานร่วมกัน

ปัจจุบัน Chrome เป็นเบราว์เซอร์เดียวที่ใช้ WebSocketStream API เนื่องจากความสามารถในการทำงานร่วมกันกับ WebSocket API แบบคลาสสิก จึงไม่สามารถใช้การควบคุมอัตราการส่งข้อมูลกับข้อความที่ได้รับ การใช้แรงดันย้อนกลับกับข้อความที่ส่งเป็นไปได้ แต่ต้องมีการสำรวจพร็อพเพอร์ตี้ WebSocket.bufferedAmount ซึ่งไม่มีประสิทธิภาพและไม่สะดวก

การตรวจหาฟีเจอร์

หากต้องการตรวจสอบว่าระบบรองรับ WebSocketStream API หรือไม่ ให้ใช้คำสั่งต่อไปนี้

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

สาธิต

ในเบราว์เซอร์ที่รองรับ คุณจะเห็น WebSocketStream API ในการทำงาน หรือดู iframe ที่ฝังไว้

ความคิดเห็น

ทีม Chrome อยากทราบความคิดเห็นของคุณเกี่ยวกับประสบการณ์การใช้งาน WebSocketStream API

บอกเราเกี่ยวกับการออกแบบ API

มีอะไรเกี่ยวกับ API ที่ไม่ทำงานตามที่คุณคาดหวังไว้ไหม หรือมีเมธอดหรือพร็อพเพอร์ตี้ที่ขาดหายไปซึ่งคุณต้องใช้เพื่อนำแนวคิดไปใช้ไหม หากมีคำถามหรือความคิดเห็นเกี่ยวกับโมเดลความปลอดภัย แจ้งปัญหาเกี่ยวกับข้อกำหนดในที่เก็บ GitHub ที่เกี่ยวข้อง หรือแสดงความคิดเห็นในปัญหาที่มีอยู่

รายงานปัญหาเกี่ยวกับการติดตั้งใช้งาน

หากพบข้อบกพร่องในการใช้งาน Chrome หรือการติดตั้งใช้งานแตกต่างจากข้อกำหนด ยื่นข้อบกพร่องที่ new.crbug.com โปรดใส่รายละเอียดให้มากที่สุดเท่าที่จะทำได้ วิธีง่ายๆ ในการทำซ้ำ และป้อน Blink>Network>WebSockets ในช่องคอมโพเนนต์

แสดงการสนับสนุน API

คุณวางแผนที่จะใช้ WebSocketStream API ใช่ไหม การสนับสนุนแบบสาธารณะของคุณจะช่วยให้ทีม Chrome จัดลําดับความสําคัญของฟีเจอร์ และแสดงให้ผู้ให้บริการเบราว์เซอร์รายอื่นๆ เห็นว่าการสนับสนุนฟีเจอร์เหล่านี้มีความสําคัญเพียงใด

ส่งทวีตถึง @ChromiumDev โดยใช้แฮชแท็ก #WebSocketStream และแจ้งให้เราทราบว่าคุณใช้ฟีเจอร์นี้ที่ไหนและอย่างไร

ลิงก์ที่มีประโยชน์

การรับทราบ

Adam Rice และ Yutaka Hirano เป็นผู้ใช้ WebSocketStream API