การใช้ SSE แทน WebSockets สำหรับการไหลของข้อมูลแบบทิศทางเดียวผ่าน HTTP/2
เผยแพร่แล้ว: 2022-03-10เมื่อสร้างเว็บแอปพลิเคชัน เราต้องพิจารณาว่าจะใช้กลไกการจัดส่งแบบใด สมมติว่าเรามีแอปพลิเคชันข้ามแพลตฟอร์มที่ทำงานกับข้อมูลแบบเรียลไทม์ แอปพลิเคชั่นตลาดหุ้นที่ให้ความสามารถในการซื้อหรือขายหุ้นแบบเรียลไทม์ แอปพลิเคชั่นนี้ประกอบด้วยวิดเจ็ตที่นำคุณค่าที่แตกต่างกันมาสู่ผู้ใช้ที่แตกต่างกัน
เมื่อพูดถึงการส่งข้อมูลจากเซิร์ฟเวอร์ไปยังไคลเอนต์ เราจำกัดวิธีการทั่วไปสองวิธี: การ ดึงไคลเอนต์ หรือ การพุชของเซิร์ฟเวอร์ ตัวอย่างง่ายๆ กับเว็บแอปพลิเคชันใดๆ ไคลเอ็นต์คือเว็บเบราว์เซอร์ เมื่อเว็บไซต์ในเบราว์เซอร์ของคุณขอข้อมูลจากเซิร์ฟเวอร์ สิ่งนี้เรียกว่าการ ดึงไคลเอ็นต์ ในทางกลับกัน เมื่อเซิร์ฟเวอร์ผลักดันการอัปเดตไปยังเว็บไซต์ของคุณในเชิงรุก เซิร์ฟเวอร์จะเรียกว่า การพุชเซิร์ฟเวอร์
ในปัจจุบัน มีสองสามวิธีในการดำเนินการเหล่านี้:
- โพลแบบยาว/สั้น (ดึงไคลเอ็นต์)
- 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
ผ่าน MDN:
WebSockets เป็นเทคโนโลยีขั้นสูงที่ทำให้สามารถเปิดเซสชันการสื่อสารแบบโต้ตอบระหว่างเบราว์เซอร์ของผู้ใช้และเซิร์ฟเวอร์ได้ ด้วย API นี้ คุณสามารถส่งข้อความไปยังเซิร์ฟเวอร์และรับการตอบสนองตามเหตุการณ์โดยไม่ต้องสำรวจเซิร์ฟเวอร์เพื่อตอบกลับ
นี่คือโปรโตคอลการสื่อสารที่ให้ช่องทางการสื่อสารฟูลดูเพล็กซ์ผ่านการเชื่อมต่อ TCP เดียว
ทั้ง HTTP และ WebSockets อยู่ที่เลเยอร์แอปพลิเคชันจากโมเดล OSI และขึ้นอยู่กับ TCP ที่เลเยอร์ 4
- แอปพลิเคชัน
- การนำเสนอ
- การประชุม
- ขนส่ง
- เครือข่าย
- ดาต้าลิงค์
- ทางกายภาพ
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 แล้ว ทั้งหมดจะถูกปฏิบัติเหมือนเป็นส่วนตัวและไม่สามารถเข้าถึงได้จากภายนอก
บิลด์อินเหตุการณ์:
- เปิด
- ข้อความ
- ข้อผิดพลาด
การจัดการการเชื่อมต่อ 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 }
เรากำหนดฟังก์ชันที่จะจัดการกับการตอบสนอง:
- ตั้งค่าส่วนหัว
- สร้างข้อความ
- ส่ง
โปรดทราบว่าคุณไม่เห็นการเรียกเมธอด send()
หรือ push()
เนื่องจากมาตรฐานกำหนดว่าข้อความจะถูกส่งทันทีที่ได้รับอักขระ \n\n
สองตัวดังในตัวอย่าง: response.write("data: " + data + '\n\n');
. สิ่งนี้จะส่งข้อความถึงลูกค้าทันที โปรดทราบว่า data
ต้องเป็นสตริงที่ใช้ Escape และไม่มีอักขระขึ้นบรรทัดใหม่ต่อท้าย
การสร้างข้อความ
ตามที่กล่าวไว้ก่อนหน้านี้ ข้อความสามารถมีคุณสมบัติสองสามอย่าง:
- ไอดี
หากค่าฟิลด์ไม่มีค่า U+0000 NULL ให้ตั้งค่าบัฟเฟอร์ ID เหตุการณ์สุดท้ายเป็นค่าฟิลด์ มิฉะนั้น ละเว้นฟิลด์ - ข้อมูล
เพิ่มค่าฟิลด์ต่อท้ายบัฟเฟอร์ข้อมูล จากนั้นผนวกอักขระ U+000A LINE FEED (LF) ตัวเดียวต่อท้ายบัฟเฟอร์ข้อมูล - เหตุการณ์
ตั้งค่าบัฟเฟอร์ประเภทเหตุการณ์เป็นค่าฟิลด์ สิ่งนี้นำไปสู่การevent.type
รับชื่อเหตุการณ์ที่กำหนดเองของคุณ - ลองอีกครั้ง
หากค่าของฟิลด์ประกอบด้วยตัวเลข 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 ครั้ง
- คุณสามารถยอมรับคำขอ EventSource ได้ก็ต่อเมื่อคำขอ HTTP ระบุว่าสามารถยอมรับประเภท MIME ของสตรีมเหตุการณ์ได้
- คุณต้องรักษารายชื่อผู้ใช้ที่เชื่อมต่อทั้งหมดเพื่อปล่อยกิจกรรมใหม่
- คุณควรฟังการเชื่อมต่อที่หลุดและลบออกจากรายชื่อผู้ใช้ที่เชื่อมต่อ
- คุณควรเลือกที่จะรักษาประวัติข้อความเพื่อให้ไคลเอ็นต์ที่เชื่อมต่อใหม่สามารถติดตามข้อความที่ไม่ได้รับได้
มันทำงานได้ตามที่คาดไว้และดูเหมือนเวทมนตร์ในตอนแรก เราได้รับทุกอย่างที่เราต้องการเพื่อให้แอปพลิเคชันของเราทำงานอย่างมีประสิทธิภาพ เช่นเดียวกับทุกสิ่งที่ดูดีเกินจริง บางครั้งเราประสบปัญหาบางอย่างที่ต้องแก้ไข อย่างไรก็ตาม การใช้งานหรือดำเนินการไม่ซับซ้อน:
ในบางกรณี พร็อกซีเซิร์ฟเวอร์รุ่นเก่าจะยกเลิกการเชื่อมต่อ HTTP หลังจากหมดเวลาสั้นๆ เพื่อป้องกันพร็อกซีเซิร์ฟเวอร์ดังกล่าว ผู้เขียนสามารถใส่บรรทัดความคิดเห็น (บรรทัดที่ขึ้นต้นด้วยอักขระ ':') ทุกๆ 15 วินาทีหรือมากกว่านั้น
ผู้เขียนที่ต้องการเชื่อมโยงแหล่งที่มาของเหตุการณ์เชื่อมต่อถึงกันหรือกับเอกสารเฉพาะก่อนหน้านี้อาจพบว่าการใช้ที่อยู่ IP นั้นใช้งานไม่ได้ เนื่องจากไคลเอนต์แต่ละรายสามารถมีที่อยู่ IP ได้หลายอัน (เนื่องจากมีพร็อกซีเซิร์ฟเวอร์หลายตัว) และที่อยู่ IP แต่ละรายการสามารถมีได้ ไคลเอนต์หลายตัว (เนื่องจากการแชร์พร็อกซีเซิร์ฟเวอร์) เป็นการดีกว่าที่จะรวมตัวระบุที่ไม่ซ้ำในเอกสารเมื่อมีการให้บริการ และส่งผ่านตัวระบุนั้นโดยเป็นส่วนหนึ่งของ URL เมื่อมีการสร้างการเชื่อมต่อ
ผู้เขียนยังได้รับคำเตือนด้วยว่ากลุ่ม HTTP อาจมีผลกระทบด้านลบที่ไม่คาดคิดต่อความน่าเชื่อถือของโปรโตคอลนี้ โดยเฉพาะอย่างยิ่ง หากการแบ่งส่วนข้อมูลทำได้โดยเลเยอร์อื่นโดยไม่ทราบข้อกำหนดด้านเวลา หากเป็นปัญหา คุณสามารถปิดใช้กลุ่มเพื่อให้บริการสตรีมเหตุการณ์ได้
ไคลเอนต์ที่รองรับข้อจำกัดการเชื่อมต่อเซิร์ฟเวอร์ของ HTTP อาจประสบปัญหาเมื่อเปิดหลายหน้าจากไซต์ หากแต่ละหน้ามี EventSource ไปยังโดเมนเดียวกัน ผู้เขียนสามารถหลีกเลี่ยงสิ่งนี้ได้โดยใช้กลไกที่ค่อนข้างซับซ้อนของการใช้ชื่อโดเมนที่ไม่ซ้ำกันต่อการเชื่อมต่อ หรือโดยอนุญาตให้ผู้ใช้เปิดใช้งานหรือปิดใช้งานฟังก์ชัน EventSource แบบต่อหน้า หรือโดยการแบ่งปันวัตถุ EventSource เดียวโดยใช้ผู้ปฏิบัติงานที่ใช้ร่วมกัน
การสนับสนุนเบราว์เซอร์และ Polyfills: Edge นั้นล้าหลังการใช้งานนี้ แต่มี polyfill ที่สามารถช่วยคุณประหยัดได้ อย่างไรก็ตาม กรณีที่สำคัญที่สุดสำหรับ SSE เกิดขึ้นกับอุปกรณ์พกพาที่ IE/Edge ไม่มีส่วนแบ่งการตลาดที่เป็นไปได้
polyfills ที่มีอยู่บางส่วน:
- Yaffle
- amvtek
- เรมี่
การกดแบบไม่มีการเชื่อมต่อและคุณสมบัติอื่นๆ
ตัวแทนผู้ใช้ที่ทำงานในสภาพแวดล้อมที่มีการควบคุม เช่น เบราว์เซอร์บนโทรศัพท์มือถือที่เชื่อมโยงกับผู้ให้บริการเฉพาะ อาจลดการจัดการการเชื่อมต่อไปยังพร็อกซีบนเครือข่าย ในสถานการณ์เช่นนี้ ตัวแทนผู้ใช้เพื่อวัตถุประสงค์ในการปฏิบัติตามจะถูกพิจารณาให้รวมทั้งซอฟต์แวร์ของเครื่องโทรศัพท์และพร็อกซีเครือข่าย
ตัวอย่างเช่น เบราว์เซอร์บนอุปกรณ์มือถือ หลังจากสร้างการเชื่อมต่อแล้ว อาจตรวจพบว่าเบราว์เซอร์อยู่บนเครือข่ายที่รองรับ และขอให้พร็อกซีเซิร์ฟเวอร์บนเครือข่ายเข้ามาแทนที่การจัดการการเชื่อมต่อ เส้นเวลาสำหรับสถานการณ์ดังกล่าวอาจเป็นดังนี้:
- เบราว์เซอร์เชื่อมต่อกับเซิร์ฟเวอร์ HTTP ระยะไกลและร้องขอทรัพยากรที่ระบุโดยผู้สร้างในตัวสร้าง EventSource
- เซิร์ฟเวอร์ส่งข้อความเป็นครั้งคราว
- ระหว่างสองข้อความ เบราว์เซอร์ตรวจพบว่าไม่มีการใช้งาน ยกเว้นกิจกรรมเครือข่ายที่เกี่ยวข้องกับการรักษาการเชื่อมต่อ TCP ให้คงอยู่ และตัดสินใจเปลี่ยนไปใช้โหมดสลีปเพื่อประหยัดพลังงาน
- เบราว์เซอร์ยกเลิกการเชื่อมต่อจากเซิร์ฟเวอร์
- เบราว์เซอร์ติดต่อบริการบนเครือข่ายและขอให้บริการ "พุชพรอกซี" รักษาการเชื่อมต่อแทน
- บริการ “push proxy” จะติดต่อกับเซิร์ฟเวอร์ HTTP ระยะไกลและร้องขอทรัพยากรที่ระบุโดยผู้เขียนในตัวสร้าง EventSource (อาจรวมถึงส่วนหัว HTTP
Last-Event-ID
เป็นต้น) - เบราว์เซอร์อนุญาตให้อุปกรณ์เคลื่อนที่เข้าสู่โหมดสลีป
- เซิร์ฟเวอร์ส่งข้อความอื่น
- บริการ “พุชพร็อกซี่” ใช้เทคโนโลยี เช่น การพุช OMA เพื่อถ่ายทอดเหตุการณ์ไปยังอุปกรณ์มือถือ ซึ่งปลุกให้ตื่นเพียงเพียงพอเพื่อประมวลผลเหตุการณ์แล้วกลับสู่โหมดสลีป
ซึ่งสามารถลดการใช้ข้อมูลทั้งหมด และอาจส่งผลให้ประหยัดพลังงานได้มาก
เช่นเดียวกับการนำ API ที่มีอยู่ไปใช้และรูปแบบการโยงข้อความ/เหตุการณ์-สตรีมตามที่กำหนดโดยข้อกำหนดและในรูปแบบการกระจายเพิ่มเติม (ตามที่อธิบายข้างต้น) รูปแบบของกรอบเหตุการณ์ที่กำหนดโดยข้อกำหนดอื่นๆ ที่เกี่ยวข้องอาจได้รับการสนับสนุน
สรุป
หลังจาก POC ที่ใช้เวลานานและละเอียดถี่ถ้วนรวมถึงการใช้งานเซิร์ฟเวอร์และไคลเอนต์ ดูเหมือนว่า SSE จะเป็นคำตอบสำหรับปัญหาของเราเกี่ยวกับการส่งข้อมูล มีข้อผิดพลาดบางอย่างในเรื่องนี้เช่นกัน แต่พวกเขาพิสูจน์แล้วว่าแก้ไขได้เล็กน้อย
นี่คือลักษณะการตั้งค่าการผลิตของเราในตอนท้าย:
เราได้รับสิ่งต่อไปนี้จาก 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