การใช้ SSE แทน WebSockets สำหรับการไหลของข้อมูลแบบทิศทางเดียวผ่าน HTTP/2

เผยแพร่แล้ว: 2022-03-10
สรุปอย่างรวดเร็ว ↬ เมื่อใดก็ตามที่เราออกแบบเว็บแอปพลิเคชันโดยใช้ข้อมูลตามเวลาจริง เราต้องพิจารณาว่าเราจะส่งข้อมูลของเราจากเซิร์ฟเวอร์ไปยังไคลเอนต์อย่างไร คำตอบเริ่มต้นมักจะเป็น “WebSockets” แต่มีวิธีที่ดีกว่านี้ไหม? ลองเปรียบเทียบสามวิธีที่แตกต่างกัน: การโพลแบบยาว, WebSockets และเหตุการณ์ที่เซิร์ฟเวอร์ส่ง เพื่อทำความเข้าใจข้อจำกัดในโลกแห่งความเป็นจริง คำตอบอาจทำให้คุณประหลาดใจ

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

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

ในปัจจุบัน มีสองสามวิธีในการดำเนินการเหล่านี้:

  • โพลแบบยาว/สั้น (ดึงไคลเอ็นต์)
  • WebSockets (เซิร์ฟเวอร์พุช)
  • เหตุการณ์ที่เซิร์ฟเวอร์ส่ง (การพุชเซิร์ฟเวอร์)

เราจะพิจารณาทางเลือกสามทางในเชิงลึกหลังจากที่เราได้กำหนดข้อกำหนดสำหรับกรณีธุรกิจของเราแล้ว

กรณีธุรกิจ

เพื่อให้สามารถนำเสนอวิดเจ็ตใหม่สำหรับแอปพลิเคชันตลาดหุ้นของเราได้อย่างรวดเร็วและ Plug'n'play โดยไม่ต้องปรับใช้ซ้ำของแพลตฟอร์มทั้งหมด เราจำเป็นต้องมีสิ่งเหล่านี้ในตัวเองและจัดการข้อมูล I/O ของตนเอง วิดเจ็ตไม่ได้เชื่อมต่อกันแต่อย่างใด ในกรณีที่เหมาะสมที่สุด ทุกคนจะสมัครรับข้อมูลปลายทางของ API และเริ่มรับข้อมูลจากปลายทาง นอกจากเวลาในการทำตลาดของคุณลักษณะใหม่ ๆ ที่เร็วขึ้น วิธีการนี้ยังช่วยให้เราสามารถส่งออกเนื้อหาไปยังเว็บไซต์บุคคลที่สามได้ ในขณะที่วิดเจ็ตของเรานำทุกสิ่งที่พวกเขาต้องการมาด้วยตัวเอง

เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

ข้อผิดพลาดหลักในที่นี้คือ จำนวนการเชื่อมต่อจะเพิ่มขึ้นเป็นเส้นตรงตามจำนวนวิดเจ็ตที่เรามี และเราจะใช้ขีดจำกัดของเบราว์เซอร์สำหรับจำนวนคำขอ HTTP ที่จัดการในครั้งเดียว

ข้อมูลที่วิดเจ็ตของเราจะได้รับนั้นส่วนใหญ่ประกอบด้วยตัวเลขและการอัปเดตตัวเลข: คำตอบเริ่มต้นประกอบด้วยหุ้นสิบตัวที่มีมูลค่าตลาดบางส่วนสำหรับพวกเขา ซึ่งรวมถึงการอัปเดตการเพิ่ม/การลบหุ้นและการอัพเดทมูลค่าตลาดของหุ้นที่นำเสนอในปัจจุบัน เราโอนสตริง JSON จำนวนเล็กน้อยสำหรับการอัปเดตทุกครั้งโดยเร็วที่สุด

HTTP/2 จัดเตรียมคำขอที่มาจากโดเมนเดียวกันแบบทวีคูณ ซึ่งหมายความว่าเราจะได้รับการเชื่อมต่อเพียงครั้งเดียวสำหรับการตอบกลับหลายครั้ง ดูเหมือนว่าจะสามารถแก้ปัญหาของเราได้ เราเริ่มต้นด้วยการสำรวจตัวเลือกต่างๆ เพื่อรับข้อมูลและดูว่าเราจะได้อะไรจากตัวเลือกเหล่านั้น

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

ทางเลือก

การเลือกตั้งแบบยาว

การเลือกตั้งแบบยาว

Client Pull คือการใช้ซอฟต์แวร์ที่เทียบเท่ากับเด็กที่น่ารำคาญซึ่งนั่งอยู่ที่เบาะหลังของรถคุณถามอยู่ตลอดเวลาว่า "เราอยู่ที่นั่นหรือยัง" กล่าวโดยย่อ ลูกค้าขอข้อมูลจากเซิร์ฟเวอร์ เซิร์ฟเวอร์ไม่มีข้อมูลและรอสักครู่ก่อนที่จะส่งการตอบกลับ:

  • หากมีบางอย่างปรากฏขึ้นระหว่างรอ เซิร์ฟเวอร์จะส่งและปิดคำขอ
  • หากไม่มีอะไรให้ส่งและมีเวลารอสูงสุด เซิร์ฟเวอร์จะส่งการตอบกลับว่าไม่มีข้อมูล
  • ในทั้งสองกรณี ลูกค้าเปิดคำขอข้อมูลต่อไป
  • ฟองล้างทำซ้ำ

การเรียก AJAX ทำงานบนโปรโตคอล HTTP ซึ่งหมายความว่าคำขอไปยังโดเมนเดียวกันควรได้รับมัลติเพล็กซ์ตามค่าเริ่มต้น อย่างไรก็ตาม เราพบปัญหาหลายอย่างในการพยายามทำให้มันทำงานตามที่ต้องการ ข้อผิดพลาดบางประการที่เราระบุด้วยวิธีวิดเจ็ตของเรา:

  • ค่าใช้จ่ายส่วนหัว
    ทุกคำขอและการตอบสนองของโพลเป็นข้อความ HTTP ที่สมบูรณ์และมีส่วนหัว HTTP ครบชุดในกรอบข้อความ ในกรณีของเราที่เรามีข้อความเล็ก ๆ น้อย ๆ บ่อยครั้ง ส่วนหัวแสดงถึงเปอร์เซ็นต์ที่ใหญ่กว่าของข้อมูลที่ส่ง เพย์โหลดที่มีประโยชน์จริงนั้นน้อยกว่าจำนวนไบต์ที่ส่งทั้งหมดมาก (เช่น ส่วนหัว 15 KB สำหรับข้อมูล 5 KB)

  • เวลาในการตอบสนองสูงสุด
    หลังจากที่เซิร์ฟเวอร์ตอบสนอง จะไม่สามารถส่งข้อมูลไปยังไคลเอนต์ได้อีกต่อไปจนกว่าไคลเอนต์จะส่งคำขอครั้งต่อไป แม้ว่าเวลาแฝงเฉลี่ยสำหรับการทำโพลแบบยาวจะใกล้เคียงกับการส่งผ่านเครือข่ายหนึ่งครั้ง แต่เวลาแฝงสูงสุดจะมากกว่าการส่งผ่านเครือข่ายสามรายการ ได้แก่ การตอบสนอง คำขอ การตอบสนอง อย่างไรก็ตาม เนื่องจากแพ็กเก็ตสูญหายและการส่งข้อมูลซ้ำ เวลาแฝงสูงสุดสำหรับโปรโตคอล TCP/IP ใดๆ จะมีการส่งผ่านเครือข่ายมากกว่าสามครั้ง (หลีกเลี่ยงได้เมื่อใช้ไปป์ไลน์ HTTP) ในขณะที่เชื่อมต่อ LAN โดยตรง นี่ไม่ใช่ปัญหาใหญ่ แต่จะกลายเป็นหนึ่งในขณะที่กำลังเคลื่อนที่และเปลี่ยนเซลล์เครือข่าย ในระดับหนึ่ง จะสังเกตได้จาก SSE และ WebSockets แต่ผลจะดีที่สุดเมื่อใช้โพล

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

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

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

  • มัลติเพล็กซ์
    สิ่งนี้สามารถเกิดขึ้นได้หากการตอบสนองเกิดขึ้นพร้อมกันผ่านการเชื่อมต่อ HTTP/2 แบบถาวร การดำเนินการนี้อาจทำได้ยากเนื่องจากคำตอบของโพลไม่สามารถซิงค์ได้จริงๆ

สามารถดูข้อมูลเพิ่มเติมเกี่ยวกับปัญหาในโลกแห่งความเป็นจริงที่อาจประสบกับการสำรวจความคิดเห็นแบบยาวได้ที่นี่

WebSockets

WebSockets

ตัวอย่างแรกของวิธีการ พุชของเซิร์ฟเวอร์ เราจะไปดูที่ WebSockets

ผ่าน MDN:

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

นี่คือโปรโตคอลการสื่อสารที่ให้ช่องทางการสื่อสารฟูลดูเพล็กซ์ผ่านการเชื่อมต่อ TCP เดียว

ทั้ง HTTP และ WebSockets อยู่ที่เลเยอร์แอปพลิเคชันจากโมเดล OSI และขึ้นอยู่กับ TCP ที่เลเยอร์ 4

  1. แอปพลิเคชัน
  2. การนำเสนอ
  3. การประชุม
  4. ขนส่ง
  5. เครือข่าย
  6. ดาต้าลิงค์
  7. ทางกายภาพ

RFC 6455 ระบุว่า WebSocket "ได้รับการออกแบบให้ทำงานผ่านพอร์ต HTTP 80 และ 443 ตลอดจนรองรับพร็อกซี HTTP และตัวกลาง" ดังนั้นจึงทำให้เข้ากันได้กับโปรโตคอล HTTP เพื่อให้บรรลุความเข้ากันได้ WebSocket handshake จะใช้ส่วนหัว HTTP Upgrade เพื่อเปลี่ยนจากโปรโตคอล HTTP เป็นโปรโตคอล WebSocket

นอกจากนี้ยังมีบทความที่ดีมากที่อธิบายทุกสิ่งที่คุณจำเป็นต้องรู้เกี่ยวกับ WebSockets บน Wikipedia ฉันขอแนะนำให้คุณอ่าน

หลังจากที่พบว่าซ็อกเก็ตสามารถทำงานให้เราได้จริง เราก็เริ่มสำรวจความสามารถของพวกมันในกรณีธุรกิจของเรา และเจาะเข้าไปอีกเรื่อยๆ

  • พร็อกซีเซิร์ฟเวอร์ : โดยทั่วไป WebSockets และพร็อกซีจะมีปัญหาที่แตกต่างกันเล็กน้อย:

    • ข้อแรกเกี่ยวข้องกับผู้ให้บริการอินเทอร์เน็ตและวิธีจัดการเครือข่าย ปัญหากับพร็อกซีรัศมีถูกบล็อกพอร์ต เป็นต้น
    • ปัญหาประเภทที่สองเกี่ยวข้องกับวิธีกำหนดค่าพร็อกซีเพื่อจัดการกับการรับส่งข้อมูล HTTP ที่ไม่ปลอดภัยและการเชื่อมต่อที่มีอายุการใช้งานยาวนาน (ผลกระทบจะลดลงด้วย HTTPS)
    • ปัญหาที่สาม “กับ WebSockets คุณถูกบังคับให้เรียกใช้พร็อกซี TCP ตรงข้ามกับพร็อกซี HTTP พร็อกซี TCP ไม่สามารถฉีดส่วนหัว เขียน URL ใหม่หรือดำเนินการหลายบทบาทที่เราให้พร็อกซี HTTP ของเราดูแลตามปกติ”
  • การเชื่อมต่อจำนวนหนึ่ง : ขีดจำกัดการเชื่อมต่อที่มีชื่อเสียงสำหรับคำขอ HTTP ที่หมุนรอบหมายเลข 6 ใช้ไม่ได้กับ WebSockets 50 ซ็อกเก็ต = 50 การเชื่อมต่อ แท็บเบราว์เซอร์สิบแท็บโดย 50 ซ็อกเก็ต = การเชื่อมต่อ 500 รายการเป็นต้น เนื่องจาก WebSocket เป็นโปรโตคอลที่แตกต่างกันสำหรับการส่งข้อมูล จึงไม่มัลติเพล็กซ์โดยอัตโนมัติผ่านการเชื่อมต่อ HTTP/2 (ไม่ได้ทำงานบน HTTP เลย) การใช้มัลติเพล็กซ์แบบกำหนดเองทั้งบนเซิร์ฟเวอร์และไคลเอนต์นั้นซับซ้อนเกินไปที่จะทำให้ซ็อกเก็ตมีประโยชน์ในกรณีธุรกิจที่ระบุ ยิ่งไปกว่านั้น สิ่งนี้จะรวมวิดเจ็ตของเราเข้ากับแพลตฟอร์มของเรา เนื่องจากพวกเขาต้องการ API บางอย่างบนไคลเอนต์เพื่อสมัครรับข้อมูล และเราไม่สามารถแจกจ่ายได้หากไม่มีมัน

  • การทำโหลดบาลานซ์ (ไม่มีมัลติเพล็กซ์) : หากผู้ใช้ทุกคนเปิดซ็อกเก็ตจำนวน n อัน การบาลานซ์โหลดที่เหมาะสมนั้นซับซ้อนมาก เมื่อเซิร์ฟเวอร์ของคุณทำงานหนักเกินไป และคุณจำเป็นต้องสร้างอินสแตนซ์ใหม่และยุติอินสแตนซ์เก่าโดยขึ้นอยู่กับการใช้งานซอฟต์แวร์ของคุณ การดำเนินการที่เกิดขึ้นในการ "เชื่อมต่อใหม่" สามารถทริกเกอร์การรีเฟรชจำนวนมากและคำขอข้อมูลใหม่ที่จะทำให้ระบบของคุณทำงานหนักเกินไป . WebSockets จำเป็นต้องได้รับการดูแลทั้งบนเซิร์ฟเวอร์และบนไคลเอนต์ เป็นไปไม่ได้ที่จะย้ายการเชื่อมต่อซ็อกเก็ตไปยังเซิร์ฟเวอร์อื่นหากเซิร์ฟเวอร์ปัจจุบันมีภาระงานสูง พวกเขาจะต้องปิดและเปิดใหม่

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

  • การสร้างวงล้อใหม่ : ด้วย WebSockets เราจะต้องจัดการกับปัญหามากมายที่ได้รับการดูแลใน HTTP ด้วยตนเอง

อ่านเพิ่มเติมเกี่ยวกับปัญหาที่เกิดขึ้นจริงกับ WebSockets ได้ที่นี่

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

ผลกระทบ

เราได้รับค่าใช้จ่ายในการดำเนินงานที่เพิ่มขึ้นในแง่ของการพัฒนา การทดสอบ และการปรับขนาด ซอฟต์แวร์และโครงสร้างพื้นฐานด้านไอทีที่มีทั้งการสำรวจและ WebSockets

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

แต่ทำไมเรายังมีปัญหากับอุปกรณ์มือถือ?

ลองพิจารณาว่าอุปกรณ์เคลื่อนที่เริ่มต้นเชื่อมต่อกับอินเทอร์เน็ตอย่างไร:

อุปกรณ์เคลื่อนที่ต้องผ่านห่วงสองสามห่วงก่อนจึงจะสามารถเชื่อมต่ออินเทอร์เน็ตได้

คำอธิบายที่ตรงไปตรงมาเกี่ยวกับวิธีการทำงานของเครือข่ายมือถือ: โดยปกติ อุปกรณ์เคลื่อนที่จะมีเสาอากาศกำลังต่ำซึ่งอาจรับข้อมูลจากเซลล์ ด้วยวิธีนี้ เมื่ออุปกรณ์ได้รับข้อมูลจากสายเรียกเข้า อุปกรณ์จะบู๊ตเสาอากาศฟูลดูเพล็กซ์เพื่อเริ่มต้นการโทร ใช้เสาอากาศเดียวกันทุกครั้งที่ คุณ ต้องการโทรออกหรือเข้าถึงอินเทอร์เน็ต (หากไม่มี WiFi) เสาอากาศฟูลดูเพล็กซ์จำเป็นต้องสร้างการเชื่อมต่อกับเครือข่ายเซลลูลาร์และทำการตรวจสอบสิทธิ์บางอย่าง เมื่อสร้างการเชื่อมต่อแล้ว จะมีการสื่อสารบางอย่างระหว่างอุปกรณ์ของคุณกับเซลล์เพื่อทำการร้องขอเครือข่ายของเรา เราถูกเปลี่ยนเส้นทางไปยังพร็อกซีภายในของผู้ให้บริการมือถือซึ่งจัดการคำขอทางอินเทอร์เน็ต ตั้งแต่นั้นมา ขั้นตอนก็เป็นที่รู้จักแล้ว: จะถาม DNS ว่าจริง ๆ แล้ว www.domainname.ext อยู่ที่ไหน รับ URI ไปยังทรัพยากร และสุดท้ายก็ถูกเปลี่ยนเส้นทางไปยังขั้นตอนนั้น

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

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

เมื่อถึงเวลาที่สิ่งต่าง ๆ ดูเหมือนเยือกเย็น ดูเหมือนว่าเราจะต้องพิจารณาข้อกำหนดทางธุรกิจสำหรับการสมัครของเราใหม่ เราพลาดอะไรไปหรือเปล่า?

SSE

เหตุการณ์ที่เซิร์ฟเวอร์ส่ง

ผ่าน MDN:

“อินเทอร์เฟซ EventSource ใช้เพื่อรับเหตุการณ์ที่เซิร์ฟเวอร์ส่ง มันเชื่อมต่อกับเซิร์ฟเวอร์ผ่าน HTTP และรับเหตุการณ์ในรูปแบบข้อความ/กระแสเหตุการณ์โดยไม่ต้องปิดการเชื่อมต่อ”

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

ผ่าน html5doctor.com:

เหตุการณ์ที่เซิร์ฟเวอร์ส่งเป็นเหตุการณ์ตามเวลาจริงที่เซิร์ฟเวอร์ส่งและรับโดยเบราว์เซอร์ คล้ายกับ WebSockets โดยเกิดขึ้นแบบเรียลไทม์ แต่เป็นวิธีการสื่อสารทางเดียวจากเซิร์ฟเวอร์เป็นอย่างมาก

มันดูแปลกๆ แต่หลังจากพิจารณาแล้ว กระแสข้อมูลหลักของเรามาจากเซิร์ฟเวอร์ไปยังไคลเอนต์ และมีโอกาสน้อยกว่ามากจากไคลเอนต์ไปยังเซิร์ฟเวอร์

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

คุณสมบัติเฉพาะ

  • สตรีมการเชื่อมต่อมาจากเซิร์ฟเวอร์และเป็นแบบอ่านอย่างเดียว
  • พวกเขาใช้คำขอ HTTP ปกติสำหรับการเชื่อมต่อแบบถาวร ไม่ใช่โปรโตคอลพิเศษ รับมัลติเพล็กซ์บน HTTP/2 นอกกรอบ
  • หากการเชื่อมต่อลดลง EventSource จะส่งเหตุการณ์ข้อผิดพลาดและพยายามเชื่อมต่อใหม่โดยอัตโนมัติ เซิร์ฟเวอร์ยังสามารถควบคุมการหมดเวลาก่อนที่ไคลเอนต์จะพยายามเชื่อมต่อใหม่ (อธิบายในรายละเอียดเพิ่มเติมในภายหลัง)
  • ลูกค้าสามารถส่ง ID เฉพาะพร้อมข้อความ เมื่อไคลเอนต์พยายามเชื่อมต่อใหม่หลังจากการเชื่อมต่อหลุด ไคลเอ็นต์จะส่ง ID ที่รู้จักล่าสุด จากนั้นเซิร์ฟเวอร์จะเห็นว่าไคลเอ็นต์พลาดข้อความจำนวน n ข้อความและส่งข้อความที่ไม่ได้รับในการเชื่อมต่อใหม่

ตัวอย่างการใช้งานไคลเอ็นต์

เหตุการณ์เหล่านี้คล้ายกับเหตุการณ์ JavaScript ทั่วไปที่เกิดขึ้นในเบราว์เซอร์ — เช่นเหตุการณ์การคลิก — ยกเว้นว่าเราสามารถควบคุมชื่อของเหตุการณ์และข้อมูลที่เกี่ยวข้องได้

มาดูการแสดงตัวอย่างโค้ดอย่างง่ายสำหรับฝั่งไคลเอ็นต์:

 // subscribe for messages var source = new EventSource('URL'); // handle messages source.onmessage = function(event) { // Do something with the data: event.data; };

สิ่งที่เราเห็นจากตัวอย่างคือฝั่งไคลเอ็นต์ค่อนข้างเรียบง่าย มันเชื่อมต่อกับแหล่งที่มาของเราและรอรับข้อความ

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

การใช้งานไคลเอ็นต์สำหรับ WebSockets มีลักษณะคล้ายกับสิ่งนี้มาก ความซับซ้อนของซ็อกเก็ตอยู่ในโครงสร้างพื้นฐานด้านไอทีและการใช้งานเซิร์ฟเวอร์

แหล่งที่มาของเหตุการณ์

แต่ละอ็อบเจ็กต์ EventSource มีสมาชิกดังต่อไปนี้:

  • URL: ตั้งค่าระหว่างการก่อสร้าง
  • คำขอ: เริ่มต้นเป็นโมฆะ
  • เวลาเชื่อมต่อใหม่: ค่าในหน่วย ms (ค่าที่กำหนดโดยตัวแทนผู้ใช้)
  • รหัสเหตุการณ์ล่าสุด: เริ่มแรกเป็นสตริงว่าง
  • สถานะพร้อม: สถานะของการเชื่อมต่อ
    • กำลังเชื่อมต่อ (0)
    • เปิด (1)
    • ปิด (2)

นอกเหนือจาก URL แล้ว ทั้งหมดจะถูกปฏิบัติเหมือนเป็นส่วนตัวและไม่สามารถเข้าถึงได้จากภายนอก

บิลด์อินเหตุการณ์:

  1. เปิด
  2. ข้อความ
  3. ข้อผิดพลาด

การจัดการการเชื่อมต่อ Drops

เบราว์เซอร์จะสร้างการเชื่อมต่อใหม่โดยอัตโนมัติหากการเชื่อมต่อหลุด เซิร์ฟเวอร์อาจส่งการหมดเวลาเพื่อลองใหม่หรือปิดการเชื่อมต่ออย่างถาวร ในกรณีเช่นนี้ เบราว์เซอร์จะปฏิบัติตามการพยายามเชื่อมต่อใหม่หลังจากหมดเวลาหรือไม่ลองเลยหากการเชื่อมต่อได้รับข้อความยุติ ดูเหมือนค่อนข้างง่าย — และจริงๆ แล้วมันก็เป็นเช่นนั้น

การใช้งานเซิร์ฟเวอร์ตัวอย่าง

ถ้าไคลเอนต์ง่ายขนาดนั้น บางทีการใช้งานเซิร์ฟเวอร์อาจซับซ้อน?

ตัวจัดการเซิร์ฟเวอร์สำหรับ SSE อาจมีลักษณะดังนี้:

 function handler(response) { // setup headers for the response in order to get the persistent HTTP connection response.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); // compose the message response.write('id: UniqueID\n'); response.write("data: " + data + '\n\n'); // whenever you send two new line characters the message is sent automatically }

เรากำหนดฟังก์ชันที่จะจัดการกับการตอบสนอง:

  1. ตั้งค่าส่วนหัว
  2. สร้างข้อความ
  3. ส่ง

โปรดทราบว่าคุณไม่เห็นการเรียกเมธอด send() หรือ push() เนื่องจากมาตรฐานกำหนดว่าข้อความจะถูกส่งทันทีที่ได้รับอักขระ \n\n สองตัวดังในตัวอย่าง: response.write("data: " + data + '\n\n'); . สิ่งนี้จะส่งข้อความถึงลูกค้าทันที โปรดทราบว่า data ต้องเป็นสตริงที่ใช้ Escape และไม่มีอักขระขึ้นบรรทัดใหม่ต่อท้าย

การสร้างข้อความ

ตามที่กล่าวไว้ก่อนหน้านี้ ข้อความสามารถมีคุณสมบัติสองสามอย่าง:

  1. ไอดี
    หากค่าฟิลด์ไม่มีค่า U+0000 NULL ให้ตั้งค่าบัฟเฟอร์ ID เหตุการณ์สุดท้ายเป็นค่าฟิลด์ มิฉะนั้น ละเว้นฟิลด์
  2. ข้อมูล
    เพิ่มค่าฟิลด์ต่อท้ายบัฟเฟอร์ข้อมูล จากนั้นผนวกอักขระ U+000A LINE FEED (LF) ตัวเดียวต่อท้ายบัฟเฟอร์ข้อมูล
  3. เหตุการณ์
    ตั้งค่าบัฟเฟอร์ประเภทเหตุการณ์เป็นค่าฟิลด์ สิ่งนี้นำไปสู่การ event.type รับชื่อเหตุการณ์ที่กำหนดเองของคุณ
  4. ลองอีกครั้ง
    หากค่าของฟิลด์ประกอบด้วยตัวเลข ASCII เท่านั้น ให้แปลค่าของฟิลด์เป็นจำนวนเต็มในฐานสิบ และตั้งค่าเวลาในการเชื่อมต่อใหม่ของสตรีมเหตุการณ์เป็นจำนวนเต็มนั้น มิฉะนั้น ละเว้นฟิลด์

สิ่งอื่นใดจะถูกละเลย เราไม่สามารถแนะนำสาขาของเราเองได้

ตัวอย่างที่มี event เพิ่ม :

 response.write('id: UniqueID\n'); response.write('event: add\n'); response.write('retry: 10000\n'); response.write("data: " + data + '\n\n');

จากนั้นบนไคลเอนต์ สิ่งนี้ถูกจัดการด้วย addEventListener ดังนี้:

 source.addEventListener("add", function(event) { // do stuff with data event.data; });

คุณสามารถส่งข้อความหลายข้อความโดยคั่นด้วยบรรทัดใหม่ ตราบใดที่คุณระบุ ID ที่แตกต่างกัน

 ... id: 54 event: add data: "[{SOME JSON DATA}]" id: 55 event: remove data: JSON.stringify(some_data) id: 56 event: remove data: { data: "msg" : "JSON data"\n data: "field": "value"\n data: "field2": "value2"\n data: }\n\n ...

สิ่งนี้ทำให้สิ่งที่เราสามารถทำได้กับข้อมูลของเราง่ายขึ้นอย่างมาก

ข้อกำหนดเฉพาะของเซิร์ฟเวอร์

ระหว่าง POC สำหรับแบ็กเอนด์ เราพบว่ามีข้อมูลเฉพาะบางอย่างที่จำเป็นต้องได้รับการแก้ไขเพื่อให้มีการนำ SSE ไปใช้จริง กรณีที่ดีที่สุดที่คุณจะใช้เซิร์ฟเวอร์แบบวนรอบเหตุการณ์ เช่น NodeJS, Kestrel หรือ Twisted แนวคิดที่ว่าด้วยโซลูชันแบบอิงตามเธรด คุณจะมีเธรดต่อการเชื่อมต่อ → การเชื่อมต่อ 1,000 ครั้ง = 1,000 เธรด ด้วยโซลูชันวนรอบเหตุการณ์ คุณจะมีหนึ่งเธรดสำหรับการเชื่อมต่อ 1,000 ครั้ง

  1. คุณสามารถยอมรับคำขอ EventSource ได้ก็ต่อเมื่อคำขอ HTTP ระบุว่าสามารถยอมรับประเภท MIME ของสตรีมเหตุการณ์ได้
  2. คุณต้องรักษารายชื่อผู้ใช้ที่เชื่อมต่อทั้งหมดเพื่อปล่อยกิจกรรมใหม่
  3. คุณควรฟังการเชื่อมต่อที่หลุดและลบออกจากรายชื่อผู้ใช้ที่เชื่อมต่อ
  4. คุณควรเลือกที่จะรักษาประวัติข้อความเพื่อให้ไคลเอ็นต์ที่เชื่อมต่อใหม่สามารถติดตามข้อความที่ไม่ได้รับได้

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

  • ในบางกรณี พร็อกซีเซิร์ฟเวอร์รุ่นเก่าจะยกเลิกการเชื่อมต่อ HTTP หลังจากหมดเวลาสั้นๆ เพื่อป้องกันพร็อกซีเซิร์ฟเวอร์ดังกล่าว ผู้เขียนสามารถใส่บรรทัดความคิดเห็น (บรรทัดที่ขึ้นต้นด้วยอักขระ ':') ทุกๆ 15 วินาทีหรือมากกว่านั้น

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

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

  • ไคลเอนต์ที่รองรับข้อจำกัดการเชื่อมต่อเซิร์ฟเวอร์ของ HTTP อาจประสบปัญหาเมื่อเปิดหลายหน้าจากไซต์ หากแต่ละหน้ามี EventSource ไปยังโดเมนเดียวกัน ผู้เขียนสามารถหลีกเลี่ยงสิ่งนี้ได้โดยใช้กลไกที่ค่อนข้างซับซ้อนของการใช้ชื่อโดเมนที่ไม่ซ้ำกันต่อการเชื่อมต่อ หรือโดยอนุญาตให้ผู้ใช้เปิดใช้งานหรือปิดใช้งานฟังก์ชัน EventSource แบบต่อหน้า หรือโดยการแบ่งปันวัตถุ EventSource เดียวโดยใช้ผู้ปฏิบัติงานที่ใช้ร่วมกัน

  • การสนับสนุนเบราว์เซอร์และ Polyfills: Edge นั้นล้าหลังการใช้งานนี้ แต่มี polyfill ที่สามารถช่วยคุณประหยัดได้ อย่างไรก็ตาม กรณีที่สำคัญที่สุดสำหรับ SSE เกิดขึ้นกับอุปกรณ์พกพาที่ IE/Edge ไม่มีส่วนแบ่งการตลาดที่เป็นไปได้

polyfills ที่มีอยู่บางส่วน:

  • Yaffle
  • amvtek
  • เรมี่

การกดแบบไม่มีการเชื่อมต่อและคุณสมบัติอื่นๆ

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

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

  1. เบราว์เซอร์เชื่อมต่อกับเซิร์ฟเวอร์ HTTP ระยะไกลและร้องขอทรัพยากรที่ระบุโดยผู้สร้างในตัวสร้าง EventSource
  2. เซิร์ฟเวอร์ส่งข้อความเป็นครั้งคราว
  3. ระหว่างสองข้อความ เบราว์เซอร์ตรวจพบว่าไม่มีการใช้งาน ยกเว้นกิจกรรมเครือข่ายที่เกี่ยวข้องกับการรักษาการเชื่อมต่อ TCP ให้คงอยู่ และตัดสินใจเปลี่ยนไปใช้โหมดสลีปเพื่อประหยัดพลังงาน
  4. เบราว์เซอร์ยกเลิกการเชื่อมต่อจากเซิร์ฟเวอร์
  5. เบราว์เซอร์ติดต่อบริการบนเครือข่ายและขอให้บริการ "พุชพรอกซี" รักษาการเชื่อมต่อแทน
  6. บริการ “push proxy” จะติดต่อกับเซิร์ฟเวอร์ HTTP ระยะไกลและร้องขอทรัพยากรที่ระบุโดยผู้เขียนในตัวสร้าง EventSource (อาจรวมถึงส่วนหัว HTTP Last-Event-ID เป็นต้น)
  7. เบราว์เซอร์อนุญาตให้อุปกรณ์เคลื่อนที่เข้าสู่โหมดสลีป
  8. เซิร์ฟเวอร์ส่งข้อความอื่น
  9. บริการ “พุชพร็อกซี่” ใช้เทคโนโลยี เช่น การพุช OMA เพื่อถ่ายทอดเหตุการณ์ไปยังอุปกรณ์มือถือ ซึ่งปลุกให้ตื่นเพียงเพียงพอเพื่อประมวลผลเหตุการณ์แล้วกลับสู่โหมดสลีป

ซึ่งสามารถลดการใช้ข้อมูลทั้งหมด และอาจส่งผลให้ประหยัดพลังงานได้มาก

เช่นเดียวกับการนำ API ที่มีอยู่ไปใช้และรูปแบบการโยงข้อความ/เหตุการณ์-สตรีมตามที่กำหนดโดยข้อกำหนดและในรูปแบบการกระจายเพิ่มเติม (ตามที่อธิบายข้างต้น) รูปแบบของกรอบเหตุการณ์ที่กำหนดโดยข้อกำหนดอื่นๆ ที่เกี่ยวข้องอาจได้รับการสนับสนุน

สรุป

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

นี่คือลักษณะการตั้งค่าการผลิตของเราในตอนท้าย:

ภาพรวมสถาปัตยกรรม
ภาพรวมสถาปัตยกรรมขั้นสุดท้าย ปลายทาง API ทั้งหมดอยู่เบื้องหลัง nginx ดังนั้นไคลเอ็นต์จึงได้รับการตอบกลับแบบมัลติเพล็กซ์

เราได้รับสิ่งต่อไปนี้จาก NGINX:

  • พร็อกซีไปยังจุดปลาย API ในที่ต่างๆ
  • HTTP/2 และประโยชน์ทั้งหมด เช่น มัลติเพล็กซ์สำหรับการเชื่อมต่อ
  • โหลดบาลานซ์;
  • เอสเอสแอล

วิธีนี้ทำให้เราจัดการการส่งข้อมูลและใบรับรองในที่เดียว แทนที่จะดำเนินการบนปลายทางทุกจุดแยกกัน

ประโยชน์หลักที่เราได้รับจากแนวทางนี้คือ:

  • ข้อมูลมีประสิทธิภาพ
  • การใช้งานที่ง่ายกว่า
  • มันถูกมัลติเพล็กซ์โดยอัตโนมัติผ่าน HTTP/2;
  • จำกัดจำนวนการเชื่อมต่อสำหรับข้อมูลบนไคลเอนต์เป็นหนึ่ง
  • ให้กลไกในการประหยัดแบตเตอรี่โดยการถ่ายการเชื่อมต่อไปยังพร็อกซี่

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

ตรวจสอบการสาธิตโค้ด หากคุณต้องการใช้งานไคลเอนต์-เซิร์ฟเวอร์อย่างง่าย

ทรัพยากร

  • “ปัญหาที่ทราบและแนวทางปฏิบัติที่ดีที่สุดสำหรับการใช้การโพลแบบยาวและการสตรีมใน HTTP แบบสองทิศทาง” IETF (PDF)
  • คำแนะนำ W3C, W3C
  • “ WebSocket จะรอดจาก HTTP/2 หรือไม่” Allan Denis, InfoQ
  • “สตรีมอัปเดตด้วยเหตุการณ์ที่เซิร์ฟเวอร์ส่ง” Eric Bidelman, HTML5 Rocks
  • “แอปพุชข้อมูลด้วย HTML5 SSE” Darren Cook, O'Reilly Media