Sự kiện do máy chủ gửi (SSE) gửi các bản cập nhật tự động đến máy khách từ một máy chủ bằng HTTP kết nối. Sau khi thiết lập kết nối, máy chủ có thể khởi tạo dữ liệu truyền dữ liệu.
Bạn nên dùng SSE để gửi thông báo đẩy qua ứng dụng web. Các SSE gửi thông tin theo một hướng nên bạn sẽ không nhận được thông tin cập nhật từ khách hàng.
Khái niệm về SSE có thể quen thuộc. Một ứng dụng web "đăng ký" đến một luồng các bản cập nhật do máy chủ tạo ra và mỗi khi có sự kiện mới xảy ra, thông báo sẽ được gửi đến máy khách. Nhưng để thực sự hiểu được các sự kiện do máy chủ gửi, chúng ta cần hiểu được những hạn chế của các phiên bản AJAX trước đó. Nội dung như vậy bao gồm:
Thăm dò ý kiến: Ứng dụng liên tục thăm dò ý kiến máy chủ để lấy dữ liệu. Kỹ thuật này được phần lớn các ứng dụng AJAX sử dụng. Với giao thức HTTP, việc tìm nạp dữ liệu xoay quanh định dạng yêu cầu và phản hồi. Khách hàng đưa ra yêu cầu và chờ máy chủ phản hồi bằng dữ liệu. Nếu không có giá trị nào, thì trường này trống được trả về. Việc thăm dò thêm sẽ tạo ra mức hao tổn HTTP lớn hơn.
Thăm dò ý kiến trong thời gian dài (tạm ngưng hoạt động GET / COMET): Nếu máy chủ không có dữ liệu máy chủ sẽ yêu cầu mở cho đến khi có dữ liệu mới. Do đó, kỹ thuật này thường được gọi là "treo GET". Thời gian thông tin xuất hiện, máy chủ phản hồi, đóng kết nối, và quy trình này lặp lại. Do đó, máy chủ liên tục phản hồi với dữ liệu mới. Để thiết lập điều này, các nhà phát triển thường sử dụng các kỹ thuật tấn công như phần thêm các thẻ tập lệnh thành mã "vô hạn" iframe.
Các sự kiện do máy chủ gửi đã được thiết kế từ đầu để mang lại hiệu quả. Khi giao tiếp với SSE, máy chủ có thể đẩy dữ liệu đến ứng dụng của bạn bất cứ khi nào ứng dụng đó muốn mà không cần đưa ra yêu cầu ban đầu. Nói cách khác, các bản cập nhật có thể được truyền trực tuyến từ máy chủ này sang máy khách khi chúng diễn ra. SSE mở một kênh một chiều giữa máy chủ và ứng dụng.
Điểm khác biệt chính giữa sự kiện do máy chủ gửi và cuộc thăm dò ý kiến dài là SSE được trình duyệt xử lý trực tiếp và người dùng chỉ phải nghe thông báo.
Sự kiện do máy chủ gửi so với WebSockets
Tại sao bạn nên chọn sự kiện do máy chủ gửi thay vì WebSockets? Đây là một câu hỏi hay.
WebSockets có một giao thức đa dạng thức với giao tiếp hai chiều, song công hoàn toàn. Kênh hai chiều sẽ phù hợp hơn cho trò chơi, ứng dụng nhắn tin và bất cứ trường hợp sử dụng nào khi bạn cần cập nhật gần như theo thời gian thực trong cả hai hướng.
Tuy nhiên, đôi khi bạn chỉ cần giao tiếp một chiều từ máy chủ.
Ví dụ: khi một người bạn cập nhật trạng thái, bảng giá cổ phiếu, nguồn cấp tin tức hoặc
các cơ chế đẩy dữ liệu tự động khác. Nói cách khác,
bản cập nhật cho Cơ sở dữ liệu Web SQL phía máy khách hoặc kho lưu trữ đối tượng IndexedDB.
Nếu bạn cần gửi dữ liệu đến một máy chủ, XMLHttpRequest
luôn là một người bạn.
Các SSE được gửi qua HTTP. Không có giao thức hoặc máy chủ đặc biệt bắt đầu triển khai. WebSockets yêu cầu song công hoàn toàn các kết nối và máy chủ WebSocket mới để xử lý giao thức.
Ngoài ra, các sự kiện do máy chủ gửi có nhiều tính năng mà WebSockets không có theo thiết kế, bao gồm cả việc tự động kết nối lại, mã sự kiện và khả năng gửi các sự kiện tuỳ ý.
Tạo EventSource bằng JavaScript
Để đăng ký theo dõi một luồng sự kiện, hãy tạo một đối tượng EventSource
và truyền đối tượng đó vào
URL luồng của bạn:
const source = new EventSource('stream.php');
Tiếp theo, hãy thiết lập trình xử lý cho sự kiện message
. Bạn có thể tuỳ ý
nghe open
và 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.
}
});
Khi máy chủ đẩy thông tin cập nhật, trình xử lý onmessage
sẽ kích hoạt
và dữ liệu mới sẽ có sẵn trong thuộc tính e.data
. Điều tuyệt vời là
mỗi khi kết nối bị đóng, trình duyệt sẽ tự động kết nối lại với
nguồn sau khoảng 3 giây. Việc triển khai máy chủ của bạn thậm chí có thể có quyền kiểm soát
hết thời gian chờ kết nối lại này.
Vậy là xong. Khách hàng của bạn hiện có thể xử lý các sự kiện từ stream.php
.
Định dạng luồng sự kiện
Việc gửi luồng sự kiện từ nguồn là vấn đề cần tạo
nội dung phản hồi bằng văn bản thuần tuý, được phân phát với Content-Type text/event-stream
,
theo định dạng SSE.
Ở dạng cơ bản, phản hồi phải chứa dòng data:
, theo sau là
tin nhắn, theo sau là hai "\n" ký tự để kết thúc luồng:
data: My message\n\n
Dữ liệu nhiều dòng
Nếu thông điệp của bạn dài hơn, bạn có thể chia nhỏ bằng cách sử dụng nhiều data:
dòng.
Hai hoặc nhiều dòng liên tiếp bắt đầu bằng data:
được coi là
phần dữ liệu duy nhất, nghĩa là chỉ một sự kiện message
được kích hoạt.
Mỗi dòng phải kết thúc bằng một "\n" (ngoại trừ mục cuối cùng, sẽ kết thúc
có 2). Kết quả được chuyển đến trình xử lý message
là một chuỗi đơn
được nối bởi các ký tự dòng mới. Ví dụ:
data: first line\n
data: second line\n\n</pre>
Thao tác này tạo ra "dòng đầu tiên\ndòng thứ hai" trong e.data
. Sau đó, người dùng có thể sử dụng
e.data.split('\n').join('')
để tạo lại tin nhắn Sans "\n" ký tự.
Gửi dữ liệu JSON
Việc sử dụng nhiều dòng sẽ giúp bạn gửi JSON mà không làm hỏng cú pháp:
data: {\n
data: "msg": "hello world",\n
data: "id": 12345\n
data: }\n\n
Có thể có mã phía máy khách để xử lý luồng đó:
source.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
console.log(data.id, data.msg);
});
Liên kết mã nhận dạng với một sự kiện
Bạn có thể gửi mã nhận dạng duy nhất kèm theo sự kiện phát trực tuyến bằng cách thêm một dòng bắt đầu bằng
id:
:
id: 12345\n
data: GOOG\n
data: 556\n\n
Việc đặt mã nhận dạng cho phép trình duyệt theo dõi sự kiện cuối cùng được kích hoạt để nếu,
kết nối đến máy chủ bị gián đoạn, tiêu đề HTTP đặc biệt (Last-Event-ID
) sẽ
đặt bằng yêu cầu mới. Điều này cho phép trình duyệt xác định sự kiện thích hợp để kích hoạt.
Sự kiện message
chứa một thuộc tính e.lastEventId
.
Kiểm soát thời gian chờ kết nối lại
Trình duyệt sẽ cố gắng kết nối lại với nguồn trong khoảng 3 giây
sau mỗi lần kết nối được đóng. Bạn có thể thay đổi thời gian chờ đó bằng cách thêm
dòng bắt đầu bằng retry:
, theo sau là số mili giây
hãy đợi trước khi cố gắng kết nối lại.
Ví dụ sau đây thử kết nối lại sau 10 giây:
retry: 10000\n
data: hello world\n\n
Chỉ định tên sự kiện
Một nguồn sự kiện có thể tạo nhiều loại sự kiện bằng cách bao gồm một
tên sự kiện. Nếu có một dòng bắt đầu bằng event:
,
tiếp theo là tên riêng biệt cho sự kiện, thì sự kiện đó sẽ được liên kết với tên đó.
Trên máy khách, bạn có thể thiết lập một trình nghe sự kiện để theo dõi sự kiện cụ thể đó.
Ví dụ: dữ liệu đầu ra của máy chủ sau đây sẽ gửi 3 loại sự kiện: "tin nhắn" chung chung event, "userlogon" và "update" sự kiện:
data: {"msg": "First message"}\n\n
event: userlogon\n
data: {"username": "John123"}\n\n
event: update\n
data: {"username": "John123", "emotion": "happy"}\n\n
Khi thiết lập trình nghe sự kiện trên ứng dụng:
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}`);
};
Ví dụ về máy chủ
Dưới đây là cách triển khai máy chủ cơ bản trong 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()));
?>
Dưới đây là cách triển khai tương tự trên Nút JS bằng cách sử dụng Trình xử lý nhanh:
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>
Huỷ luồng sự kiện
Thông thường, trình duyệt sẽ tự động kết nối lại với nguồn sự kiện khi kết nối bị đóng nhưng hành vi đó có thể bị huỷ từ máy khách hoặc máy chủ.
Để huỷ phiên phát trực tiếp từ ứng dụng, hãy gọi:
source.close();
Để huỷ một luồng dữ liệu từ máy chủ, hãy phản hồi bằng một mã không phải text/event-stream
Content-Type
hoặc trả về trạng thái HTTP không phải 200 OK
(chẳng hạn như 404 Not Found
).
Cả hai phương pháp đều ngăn trình duyệt thiết lập lại kết nối.
Lưu ý về bảo mật
Các yêu cầu do EventSource tạo ra phải tuân theo chính sách cùng nguồn gốc như các API mạng khác như tìm nạp. Nếu bạn cần điểm cuối SSE trên máy chủ của mình có thể truy cập từ nhiều nguồn gốc, đọc cách bật bằng Chia sẻ tài nguyên trên nhiều nguồn gốc (CORS).