เหตุการณ์ที่เซิร์ฟเวอร์ส่ง (SSE) จะส่งการอัปเดตอัตโนมัติไปยังไคลเอ็นต์จากเซิร์ฟเวอร์โดยใช้การเชื่อมต่อ HTTP เมื่อเชื่อมต่อแล้ว เซิร์ฟเวอร์สามารถเริ่มการส่งข้อมูลได้
คุณอาจต้องการใช้ SSE เพื่อส่งข้อความ Push จากเว็บแอป SSE จะส่งข้อมูลในทิศทางเดียว ดังนั้นคุณจะไม่ได้รับการอัปเดตจาก ไคลเอ็นต์
แนวคิดของ SSE อาจคุ้นเคยอยู่แล้ว เว็บแอปจะ "ติดตาม" สตรีมอัปเดตที่สร้างขึ้นโดยเซิร์ฟเวอร์ และเมื่อใดก็ตามที่เกิดเหตุการณ์ใหม่ ระบบจะส่งการแจ้งเตือนไปยังไคลเอ็นต์ แต่เพื่อให้เข้าใจเหตุการณ์ที่ส่งจากเซิร์ฟเวอร์ เราจำเป็นต้องเข้าใจ ข้อจำกัดของ AJAX รุ่นก่อนหน้า ซึ่งรวมถึงกรณีต่อไปนี้
การสำรวจความคิดเห็น: แอปพลิเคชันจะสำรวจข้อมูลในเซิร์ฟเวอร์ซ้ำๆ แอปพลิเคชัน AJAX ส่วนใหญ่มักใช้เทคนิคนี้ โปรโตคอล HTTP ทำให้การดึงข้อมูลเกี่ยวข้องกับคำขอและรูปแบบการตอบสนอง ไคลเอ็นต์จะส่งคำขอและรอให้เซิร์ฟเวอร์ตอบกลับพร้อมข้อมูล หากไม่มี ระบบจะแสดงผลการตอบกลับที่ว่างเปล่า แบบสำรวจเพิ่มเติมจะสร้างโอเวอร์เฮด HTTP มากขึ้น
การสำรวจความคิดเห็นที่ยาวนาน (Hanging GET / COMET): หากเซิร์ฟเวอร์ไม่มีข้อมูล เซิร์ฟเวอร์จะเก็บคำขอไว้จนกว่าจะมีการแสดงข้อมูลใหม่ ดังนั้น เทคนิคนี้มักจะเรียกว่า "Hanging GET" เมื่อมีข้อมูลพร้อมใช้งาน เซิร์ฟเวอร์จะตอบสนอง ปิดการเชื่อมต่อ และทำขั้นตอนซ้ำ ดังนั้น เซิร์ฟเวอร์จะตอบสนอง ด้วยข้อมูลใหม่อยู่ตลอดเวลา ในการตั้งค่านี้ นักพัฒนาซอฟต์แวร์มักจะใช้การแฮ็กต่างๆ เช่น การเพิ่มแท็กสคริปต์ลงใน iframe แบบ "ไม่รู้จบ"
กิจกรรมที่ส่งโดยเซิร์ฟเวอร์ได้รับการออกแบบมาตั้งแต่ต้นให้มีประสิทธิภาพ เมื่อสื่อสารกับ SSE เซิร์ฟเวอร์จะพุชข้อมูลไปยังแอปของคุณได้ทุกเมื่อที่ต้องการ โดยไม่จำเป็นต้องส่งคำขอเริ่มต้น กล่าวคือ คุณสามารถสตรีมการอัปเดตจากเซิร์ฟเวอร์ไปยังไคลเอ็นต์ได้ในขณะที่เกิดขึ้น SSE เปิดช่องทางเดียวแบบทิศทางเดียวระหว่างเซิร์ฟเวอร์และไคลเอ็นต์
ความแตกต่างที่สำคัญระหว่างกิจกรรมที่ส่งโดยเซิร์ฟเวอร์และการสำรวจระยะยาวคือ SSE จะได้รับการจัดการโดยตรงจากเบราว์เซอร์ และผู้ใช้ต้องคอยฟังข้อความเท่านั้น
เหตุการณ์ที่เซิร์ฟเวอร์ส่งกับ WebSocket
เหตุใดคุณจึงเลือกเหตุการณ์ที่เซิร์ฟเวอร์ส่งแทน WebSocket เป็นคำถามที่ดี
WebSockets มีโปรโตคอลที่สมบูรณ์ที่มีการสื่อสารแบบ 2 ทิศทางและ 2 ทิศทาง ช่องทางแบบสองทางนั้นเหมาะสำหรับเกม แอปรับส่งข้อความ และกรณีการใช้งานใดๆ ที่คุณต้องการการอัปเดตแบบเกือบเรียลไทม์ทั้ง 2 ทาง
แต่บางครั้งคุณต้องใช้การสื่อสารแบบทางเดียวจากเซิร์ฟเวอร์
เช่น เมื่อเพื่อนอัปเดตสถานะ ทิกเกอร์หุ้น ฟีดข่าว หรือกลไกการพุชข้อมูลอัตโนมัติอื่นๆ กล่าวคือ เป็นการอัปเดตฐานข้อมูล SQL ในเว็บฝั่งไคลเอ็นต์หรือที่เก็บออบเจ็กต์ IndexedDB
หากคุณต้องการส่งข้อมูลไปยังเซิร์ฟเวอร์ XMLHttpRequest
จะเป็นเพื่อนกันเสมอ
ระบบจะส่ง SSE ผ่าน HTTP ไม่ต้องใช้โปรโตคอลหรือเซิร์ฟเวอร์ พิเศษสำหรับการทำงาน WebSocket ต้องใช้การเชื่อมต่อแบบ Full-duplex และเซิร์ฟเวอร์ WebSocket ใหม่ในการจัดการโปรโตคอล
นอกจากนี้ เหตุการณ์ที่ส่งโดยเซิร์ฟเวอร์ยังมีฟีเจอร์มากมายที่ WebSockets ออกแบบไม่ได้ ไม่ว่าจะเป็นการเชื่อมต่ออีกครั้งโดยอัตโนมัติ รหัสเหตุการณ์ และความสามารถในการส่งเหตุการณ์ที่กําหนดเอง
สร้าง EventSource ด้วย JavaScript
หากต้องการสมัครรับข้อมูลสตรีมเหตุการณ์ ให้สร้างออบเจ็กต์ EventSource
แล้วส่งผ่าน URL ของสตรีม ดังนี้
const source = new EventSource('stream.php');
ถัดไป ให้ตั้งค่าเครื่องจัดการสำหรับกิจกรรม message
คุณสามารถเลือกฟัง open
และ error
ได้ดังนี้
source.addEventListener('message', (e) => {
console.log(e.data);
});
source.addEventListener('open', (e) => {
// Connection was opened.
});
source.addEventListener('error', (e) => {
if (e.readyState == EventSource.CLOSED) {
// Connection was closed.
}
});
เมื่อพุชอัปเดตจากเซิร์ฟเวอร์ ตัวแฮนเดิล onmessage
จะเริ่มทำงานและข้อมูลใหม่จะพร้อมใช้งานในพร็อพเพอร์ตี้ e.data
ส่วนที่ยอดเยี่ยมก็คือ เมื่อใดก็ตามที่ปิดการเชื่อมต่อ เบราว์เซอร์จะเชื่อมต่อกับต้นทางอีกครั้งโดยอัตโนมัติหลังจากผ่านไปประมาณ 3 วินาที การใช้งานเซิร์ฟเวอร์ของคุณยังสามารถควบคุมระยะหมดเวลาการเชื่อมต่อนี้ได้ด้วย
เท่านี้เอง ตอนนี้ลูกค้าประมวลผลเหตุการณ์จาก stream.php
ได้แล้ว
รูปแบบสตรีมเหตุการณ์
การส่งสตรีมเหตุการณ์จากแหล่งที่มาเป็นเรื่องของการสร้างคำตอบแบบข้อความธรรมดาที่แสดงด้วยประเภทเนื้อหา text/event-stream
ซึ่งเป็นไปตามรูปแบบ SSE
ในรูปแบบพื้นฐาน การตอบกลับควรมีบรรทัด data:
ตามด้วยข้อความ ตามด้วยอักขระ "\n" 2 ตัวเพื่อสิ้นสุดสตรีม
data: My message\n\n
ข้อมูลหลายบรรทัด
หากข้อความยาว คุณสามารถแบ่งข้อความได้โดยใช้ data:
หลายบรรทัด
บรรทัดติดกัน 2 บรรทัดขึ้นไปที่ขึ้นต้นด้วย data:
จะถือว่าเป็นข้อมูลชิ้นเดียว ซึ่งหมายความว่ามีเหตุการณ์ message
เพียง 1 รายการที่เริ่มทำงาน
แต่ละบรรทัดควรลงท้ายด้วย "\n" รายการเดียว (ยกเว้นรายการสุดท้ายซึ่งควรลงท้ายด้วย 2 ตัว) ผลลัพธ์ที่ส่งไปยังเครื่องจัดการ message
จะเป็นสตริงเดียวที่ต่อกันโดยอักขระขึ้นบรรทัดใหม่ เช่น
data: first line\n
data: second line\n\n</pre>
ทำให้เกิด "บรรทัดแรก\nบรรทัดที่ 2" ใน e.data
จากนั้น 1 สามารถใช้ e.data.split('\n').join('')
เพื่อสร้างอักขระของอีเมล "\n" ขึ้นมาใหม่
ส่งข้อมูล JSON
การใช้บรรทัดหลายบรรทัดจะช่วยให้คุณส่ง JSON ได้โดยไม่ทำให้ไวยากรณ์ผิดพลาด เช่น
data: {\n
data: "msg": "hello world",\n
data: "id": 12345\n
data: }\n\n
และโค้ดฝั่งไคลเอ็นต์ที่อาจจัดการสตรีมนั้นมีดังนี้
source.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
console.log(data.id, data.msg);
});
เชื่อมโยงรหัสกับกิจกรรม
คุณส่งรหัสที่ไม่ซ้ำกันพร้อมกับเหตุการณ์สตรีมได้โดยใส่บรรทัดที่ขึ้นต้นด้วย id:
:
id: 12345\n
data: GOOG\n
data: 556\n\n
การตั้งค่ารหัสทำให้เบราว์เซอร์ติดตามเหตุการณ์ล่าสุดที่เริ่มทำงานได้ ดังนั้นในกรณีที่ไม่มีการเชื่อมต่อกับเซิร์ฟเวอร์ ระบบจะตั้งค่าส่วนหัว HTTP พิเศษ (Last-Event-ID
) ด้วยคำขอใหม่ วิธีนี้ช่วยให้เบราว์เซอร์ระบุได้ว่าเหตุการณ์ใดเหมาะสมที่จะเริ่มทำงาน
เหตุการณ์ message
มีพร็อพเพอร์ตี้ e.lastEventId
ควบคุมการหมดเวลาการเชื่อมต่อใหม่
เบราว์เซอร์จะพยายามเชื่อมต่อกับต้นทางอีกครั้งประมาณ 3 วินาทีหลังจากที่ปิดการเชื่อมต่อแต่ละครั้ง คุณเปลี่ยนระยะหมดเวลาดังกล่าวได้โดยใส่บรรทัดที่ขึ้นต้นด้วย retry:
ตามด้วยจำนวนมิลลิวินาทีให้รอก่อนที่จะลองเชื่อมต่อใหม่
ตัวอย่างต่อไปนี้จะพยายามเชื่อมต่อใหม่หลังจากผ่านไป 10 วินาที
retry: 10000\n
data: hello world\n\n
ระบุชื่อเหตุการณ์
แหล่งที่มาของเหตุการณ์เดียวสามารถสร้างเหตุการณ์ประเภทต่างๆ ได้โดยการรวมชื่อเหตุการณ์ หากมีบรรทัดที่ขึ้นต้นด้วย event:
ตามด้วยชื่อที่ไม่ซ้ำกันของกิจกรรม เหตุการณ์จะเชื่อมโยงกับชื่อนั้น
ในไคลเอ็นต์ คุณสามารถตั้งค่า Listener เหตุการณ์ให้ฟังเหตุการณ์ที่เฉพาะเจาะจงนั้นได้
ตัวอย่างเช่น เอาต์พุตเซิร์ฟเวอร์ต่อไปนี้จะส่งเหตุการณ์ 3 ประเภท ได้แก่ เหตุการณ์ "message" ทั่วไป "userlogon" และ "update"
data: {"msg": "First message"}\n\n
event: userlogon\n
data: {"username": "John123"}\n\n
event: update\n
data: {"username": "John123", "emotion": "happy"}\n\n
เมื่อตั้งค่า Listener เหตุการณ์ในไคลเอ็นต์ ให้ทำดังนี้
source.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
console.log(data.msg);
});
source.addEventListener('userlogon', (e) => {
const data = JSON.parse(e.data);
console.log(`User login: ${data.username}`);
});
source.addEventListener('update', (e) => {
const data = JSON.parse(e.data);
console.log(`${data.username} is now ${data.emotion}`);
};
ตัวอย่างเซิร์ฟเวอร์
การใช้งานเซิร์ฟเวอร์ขั้นพื้นฐานใน PHP มีดังนี้
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache'); // recommended to prevent caching of event data.
/**
* Constructs the SSE data format and flushes that data to the client.
*
* @param string $id Timestamp/id of this connection.
* @param string $msg Line of text that should be transmitted.
**/
function sendMsg($id, $msg) {
echo "id: $id" . PHP_EOL;
echo "data: $msg" . PHP_EOL;
echo PHP_EOL;
ob_flush();
flush();
}
$serverTime = time();
sendMsg($serverTime, 'server time: ' . date("h:i:s", time()));
?>
ต่อไปนี้คือการใช้งานที่คล้ายกันบน Node JS ที่ใช้เครื่องจัดการ Express
app.get('/events', (req, res) => {
// Send the SSE header.
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// Sends an event to the client where the data is the current date,
// then schedules the event to happen again after 5 seconds.
const sendEvent = () => {
const data = (new Date()).toLocaleTimeString();
res.write("data: " + data + '\n\n');
setTimeout(sendEvent, 5000);
};
// Send the initial event immediately.
sendEvent();
});
sse-node.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<script>
const source = new EventSource('/events');
source.onmessage = (e) => {
const content = document.createElement('div');
content.textContent = e.data;
document.body.append(content);
};
</script>
</body>
</html>
ยกเลิกสตรีมเหตุการณ์
โดยปกติแล้ว เบราว์เซอร์จะเชื่อมต่อกับแหล่งที่มาของกิจกรรมอีกครั้งโดยอัตโนมัติเมื่อปิดการเชื่อมต่อ แต่การทำงานดังกล่าวจะยกเลิกได้จากไคลเอ็นต์หรือเซิร์ฟเวอร์ก็ได้
หากต้องการยกเลิกสตรีมจากไคลเอ็นต์ ให้เรียก
source.close();
หากต้องการยกเลิกสตรีมจากเซิร์ฟเวอร์ ให้ตอบกลับด้วย text/event-stream
ที่ไม่ใช่ Content-Type
หรือส่งคืนสถานะ HTTP อื่นที่ไม่ใช่ 200 OK
(เช่น 404 Not Found
)
ทั้งสองวิธีป้องกันไม่ให้เบราว์เซอร์ทำการเชื่อมต่อใหม่
คำแนะนำเกี่ยวกับความปลอดภัย
คำขอที่สร้างโดย EventSource จะขึ้นอยู่กับนโยบายต้นทางเดียวกันกับ API เครือข่ายอื่นๆ เช่น การดึงข้อมูล หากต้องการให้ปลายทาง SSE บนเซิร์ฟเวอร์เข้าถึงได้จากต้นทางอื่นๆ โปรดอ่านวิธีเปิดใช้ด้วยการแชร์ทรัพยากรข้ามต้นทาง (CORS)