เหตุการณ์ที่เซิร์ฟเวอร์ส่ง (SSE) จะส่งการอัปเดตอัตโนมัติไปยังไคลเอ็นต์จากเซิร์ฟเวอร์ด้วยการเชื่อมต่อ HTTP เมื่อสร้างการเชื่อมต่อแล้ว เซิร์ฟเวอร์จะเริ่มส่งข้อมูลได้
คุณอาจต้องใช้ SSE เพื่อส่งข้อความ Push จากเว็บแอป เนื่องจาก SSE จะส่งข้อมูลเพียงทางเดียว คุณจึงจะไม่ได้รับข้อมูลอัปเดตจากไคลเอ็นต์
แนวคิดของ SSE อาจคุ้นเคย เว็บแอป "สมัครรับข้อมูล" สตรีมการอัปเดตที่สร้างโดยเซิร์ฟเวอร์ และเมื่อมีเหตุการณ์ใหม่เกิดขึ้น ระบบจะส่งการแจ้งเตือนไปยังไคลเอ็นต์ แต่หากต้องการทําความเข้าใจเหตุการณ์ที่เซิร์ฟเวอร์ส่งอย่างแท้จริง เราต้องเข้าใจข้อจํากัดของ AJAX รุ่นก่อนหน้า ซึ่งรวมถึงเนื้อหาต่อไปนี้
การหยั่งสัญญาณ: แอปพลิเคชันจะสำรวจข้อมูลในเซิร์ฟเวอร์ซ้ำหลายครั้ง แอปพลิเคชัน AJAX ส่วนใหญ่ใช้เทคนิคนี้ เมื่อใช้โปรโตคอล HTTP การดึงข้อมูลจะเกี่ยวข้องกับรูปแบบคำขอและการตอบกลับ ไคลเอ็นต์จะส่งคำขอและรอให้เซิร์ฟเวอร์ตอบกลับด้วยข้อมูล หากไม่มี ระบบจะแสดงผลลัพธ์ว่าง การสำรวจเพิ่มเติมจะทำให้เกิดค่าใช้จ่ายเพิ่มเติมใน HTTP
การโพลลิงแบบนาน (GET / COMET ที่รอดำเนินการ): หากเซิร์ฟเวอร์ไม่มีข้อมูล เซิร์ฟเวอร์จะเก็บคำขอไว้จนกว่าจะมีข้อมูลใหม่ ดังนั้น เทคนิคนี้มักเรียกว่า "Hanging GET" เมื่อข้อมูลพร้อมใช้งาน เซิร์ฟเวอร์จะตอบกลับ ปิดการเชื่อมต่อ แล้วทำขั้นตอนซ้ำ ดังนั้นเซิร์ฟเวอร์จะตอบกลับด้วยข้อมูลใหม่อย่างต่อเนื่อง ในการตั้งค่าขั้นตอนนี้ นักพัฒนาซอฟต์แวร์มักจะใช้การแฮ็กต่างๆ เช่น การใส่แท็กสคริปต์ต่อท้าย iframe แบบ "ไม่มีที่สิ้นสุด"
เหตุการณ์ที่ส่งจากเซิร์ฟเวอร์ได้รับการออกแบบตั้งแต่ต้นเพื่อให้มีประสิทธิภาพ เมื่อสื่อสารกับ SSE เซิร์ฟเวอร์จะพุชข้อมูลไปยังแอปได้ทุกเมื่อที่ต้องการโดยไม่ต้องส่งคำขอครั้งแรก กล่าวคือ คุณสามารถสตรีมการอัปเดตจากเซิร์ฟเวอร์ไปยังไคลเอ็นต์ได้ในขณะที่มีการอัปเดต SSE จะเปิดแชแนลเดียวแบบทิศทางเดียวระหว่างเซิร์ฟเวอร์และไคลเอ็นต์
ความแตกต่างหลักๆ ระหว่างเหตุการณ์ที่ส่งโดยเซิร์ฟเวอร์และแบบสำรวจแบบยาวคือ SSE ได้รับการจัดการโดยเบราว์เซอร์โดยตรง และผู้ใช้เพียงแค่คอยฟังข้อความเท่านั้น
เหตุการณ์ที่ส่งโดยเซิร์ฟเวอร์เทียบกับ WebSockets
เหตุใดคุณจึงเลือกเหตุการณ์ที่ส่งโดยเซิร์ฟเวอร์มากกว่า WebSockets เป็นคำถามที่ดี
WebSocket มีโปรโตคอลที่สมบูรณ์พร้อมการสื่อสารแบบ 2 ทิศทางแบบ Full-Duplex ช่องทางแบบ 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
Content-Type ที่เป็นไปตามรูปแบบ SSE
ในรูปแบบพื้นฐาน คำตอบควรมีบรรทัด data:
ตามด้วยข้อความของคุณ ตามด้วยอักขระ "\n" 2 ตัวเพื่อสิ้นสุดสตรีม
data: My message\n\n
ข้อมูลหลายบรรทัด
หากข้อความยาว คุณสามารถแบ่งข้อความโดยใช้data:
บรรทัดได้หลายบรรทัด
บรรทัดที่ติดกัน 2 บรรทัดขึ้นไปที่เริ่มต้นด้วย data:
จะถือว่าเป็นข้อมูลชิ้นเดียว ซึ่งหมายความว่าจะมีเหตุการณ์ message
เพียง 1 เหตุการณ์เท่านั้นที่เริ่มทํางาน
แต่ละบรรทัดควรสิ้นสุดด้วย "\n" 1 ตัว (ยกเว้นบรรทัดสุดท้ายที่ควรสิ้นสุดด้วย 2 ตัว) ผลลัพธ์ที่ส่งไปยังตัวแฮนเดิล message
จะเป็นสตริงเดียวที่ต่อเชื่อมกันด้วยอักขระขึ้นบรรทัดใหม่ เช่น
data: first line\n
data: second line\n\n</pre>
ซึ่งจะแสดงผลเป็น "บรรทัดแรก\nบรรทัดที่สอง" ใน e.data
จากนั้นก็ใช้ e.data.split('\n').join('')
เพื่อสร้างอักขระของข้อความ San "\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
)
ทั้ง 2 วิธีนี้จะช่วยป้องกันไม่ให้เบราว์เซอร์สร้างการเชื่อมต่ออีกครั้ง
คำเตือนเกี่ยวกับความปลอดภัย
คำขอที่ EventSource สร้างขึ้นอยู่ภายใต้นโยบายต้นทางเดียวกันกับ API เครือข่ายอื่นๆ เช่น fetch หากต้องการให้ปลายทาง SSE ในเซิร์ฟเวอร์เข้าถึงได้จากต้นทางอื่น โปรดอ่านวิธีเปิดใช้ด้วยกลไกการแชร์ทรัพยากรข้ามโดเมน (CORS)