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

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

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

Streams API

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

ปัญหาเกี่ยวกับ WebSocket API ปัจจุบัน

ไม่สามารถใช้ Backpressure กับข้อความที่ได้รับ

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

การใช้แรงดันย้อนกลับกับข้อความที่ส่งไม่เป็นไปตามหลักสรีรศาสตร์

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

WebSocketStream API คืออะไร

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

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

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

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

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

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

วิธีใช้ WebSocketStream API

WebSocketStream API ทำงานตามสัญญา ซึ่งทำให้การใช้งาน 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);
  }

แรงดันย้อนกลับ

แล้วฟีเจอร์ Backpressure ที่สัญญาไว้ล่ะ คุณจะได้รับ "ฟรี" โดยไม่จำเป็นต้องดำเนินการใดๆ เพิ่มเติม หาก 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 ข้อมูลทั้งหมดเกี่ยวกับการเชื่อมต่อแบบเรียลไทม์จะมาจากคำมั่นสัญญานี้ เนื่องจากไม่เกี่ยวข้องหากการเชื่อมต่อไม่สำเร็จ

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

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 เป็นเบราว์เซอร์เดียวที่ใช้ WebSocketStream API คุณจะไม่สามารถควบคุมแบ็กเพรสเซอร์สำหรับข้อความที่ได้รับได้หากต้องการใช้ร่วมกันกับ WebSocket API แบบคลาสสิก คุณใช้ Backpressure กับข้อความที่ส่งได้ แต่จะต้องทำการสำรวจพร็อพเพอร์ตี้ WebSocket.bufferedAmount ซึ่งไม่มีประสิทธิภาพและไม่เหมาะกับการใช้งาน

การตรวจหาองค์ประกอบ

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

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

สาธิต

ในเบราว์เซอร์ที่รองรับ คุณสามารถดู WebSocketStream API ทำงานได้ใน iframe ที่ฝัง หรือใน Glitch โดยตรง

ความคิดเห็น

ทีม Chrome อยากทราบความคิดเห็นของคุณเกี่ยวกับ WebSocketStream API

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

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

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

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

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

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

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

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

ขอขอบคุณ

Adam Rice และ Yutaka Hirano เป็นผู้ติดตั้งใช้งาน WebSocketStream API