簡介 SSE 的全稱是 Server Sent Events,即伺服器推送事件。它是一種基於 HTTP 的伺服器到客戶端的單向(半雙工)通信機制,使伺服器能夠主動將實時數據推送給客戶端,而不需要客戶端多次發起請求。 官方文檔:https://developer.mozilla.org/en-US/d ...
簡介
SSE 的全稱是 Server Sent Events,即伺服器推送事件。它是一種基於 HTTP 的伺服器到客戶端的單向(半雙工)通信機制,使伺服器能夠主動將實時數據推送給客戶端,而不需要客戶端多次發起請求。
官方文檔:https://developer.mozilla.org/en-US/docs/Web/API/EventSource
解決了什麼問題
常規的HTTP請求響應流程無法做到伺服器主動推送數據到客戶端,SSE可以解決此問題。
適用場景
實時更新訂閱數據、實時通知、實時日誌監控、實時數據統計、簡單的文本數據傳輸。
示例代碼
服務端
// 這行代碼用於關閉輸出緩衝。關閉後,腳本的輸出將立即發送到瀏覽器,而不是等待緩衝區填滿或腳本執行完畢。
ini_set('output_buffering', 'off');
// 這行代碼禁用了 zlib 壓縮。通常情況下,啟用 zlib 壓縮可以減小發送到瀏覽器的數據量,但對於伺服器發送事件來說,實時性更重要,因此需要禁用壓縮。
ini_set('zlib.output_compression', false);
// 這行代碼使用迴圈來清空所有當前激活的輸出緩衝區。ob_end_flush() 函數會刷新並關閉最內層的輸出緩衝區,@ 符號用於抑制可能出現的錯誤或警告。
while (@ob_end_flush()) {}
// 這行代碼設置 HTTP 響應的 Content-Type 為 text/event-stream,這是伺服器發送事件(SSE)的 MIME 類型。
header('Content-Type: text/event-stream');
// 這行代碼設置 HTTP 響應的 Cache-Control 為 no-cache,告訴瀏覽器不要緩存此響應。
header('Cache-Control: no-cache');
// 這行代碼設置 HTTP 響應的 Connection 為 keep-alive,保持長連接,以便伺服器可以持續發送事件到客戶端。
header('Connection: keep-alive');
// 這行代碼設置 HTTP 響應的自定義頭部 X-Accel-Buffering 為 no,用於禁用某些代理或 Web 伺服器(如 Nginx)的緩衝。這有助於確保伺服器發送事件在傳輸過程中不會受到緩衝影響
header('X-Accel-Buffering: no');
/**
* @function 封裝sse格式的數據
* @param $data string
* @return string
*/
function sse($data) {
//data:\n\n不能少,sse固定格式
return "data:{$data}\n\n";
}
// 開啟輸出緩衝
ob_start();
while (true) {
$json = json_encode(['data' => ['time' => date('Y-m-d H:i:s')]], JSON_UNESCAPED_UNICODE);
echo sse($json);
//刷新緩衝區
ob_flush();
//將輸出緩衝區的內容立即發送到客戶端
flush();
sleep(1);
}
客戶端
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
</body>
<script>
if(!! window.EventSource) {
var sse = new EventSource('http://127.0.0.1/test/sse.php');
//通信事件
sse.onmessage = function (event) {
var response = JSON.parse(event.data);
console.log(response.data.time);
};
// 打開事件
sse.onopen = function (event) { console.log('連接成功'); };
//關閉事件
sse.onclose = function(event) {console.log('連接關閉');};
//錯誤事件
sse.onerror = function (event) {console.error('連接失敗');};
} else {
alert('您的瀏覽器不支持SSE');
}
</script>
</html>
服務端對客戶端單向通信是實時了,可服務端數據發生變化時,怎麼及時同步到SSE模塊呢?依客戶端顯示通知數量為需求做個簡單示例
方案1:純粹輪詢模式
做法:不停對資料庫做查詢。
優點:實現簡單。
缺點:很不優雅的方案,性能消耗大。
場景:數據量不大且趕工時,可作為臨時方案。
示例:
ob_start();
$user_id = 1; //假設用戶id為1,實際可傳參獲取。
while (true) {
$notice_count = DB::table('notice')->where('user_id', $user_id)->count();
echo sse(json_encode(['notice_num' => notice_count]));
ob_flush();
flush();
sleep(1);
}
方案2:基於事件觸發,用消息隊列做訂閱發佈模式
做法:對要實時獲取的數據,先賦一個初始值的實際值,傳遞給客戶端,當數據發生變化時,觸發生產消息的通知,SSE模塊不停的消費消息。
優點:避免了輪詢模式瘋狂查詢。
缺點:仍舊需要消耗一些資源,實現稍微繁瑣。
場景:方法優雅,適用於訂閱端根據消息做更複雜的業務邏輯操作時使用。
示例:
暫時用redis隊列簡單實現:技術選型可根據實際情況做高可用或更複雜的設計。
//例如要實現一個通知數量實時變更的功能:
//發佈端:
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
//假設用戶id為1
$user_id = 1;
//執行完其它的針對notice表寫操作的業務邏輯代碼...,然後向隊列丟一個任務
$redis->lPush('add_one_notice_task:'. $user_id, 1);
//------------------------------------------------------------------------------------------------------------------
//訂閱端
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
//先查詢資料庫通知數量
$user_id = 1; //假設用戶id為1,實際可傳參獲取。
$notice_count = DB::table('notice')->where('user_id', $user_id)->count();
$redis->set('user_notice_num:'. $user_id, $notice_count);
while (true) {
//若檢測到有自增一個通知數量的任務,則消費時觸發一個增加數量的動作。
$add_one_notice_task = $redis->rPop('add_one_notice_task:'. $user_id);
if($add_one_notice_task) {
$redis->incr('user_notice_num:'. $user_id);
}
echo sse(json_encode(['notice_num' => $redis->get('user_notice_num:' . $user_id) ?? 0]));
ob_flush();
flush();
sleep(1);
}
方案3:基於事件觸發的輪詢
做法:觸髮端直接一步到位,修改好數據後緩存,監聽端不停的監聽緩存的值。
優點:實現起來比訂閱發佈簡單,又避免輪詢頻繁查庫,通過緩存解耦,避免了方案1的性能問題,又能保證緩存一致性。
缺點:終究還是輪詢,仍舊需要消耗一些資源。
場景:相對簡單的,不需要在監聽端做業務處理,只做純粹返回數據的場景。
示例:
//觸髮端
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
//假設用戶id為1,實際可傳參獲取。
$user_id = 1;
//執行完其它的針對notice表寫操作的業務邏輯代碼...
$notice_count = DB::table('notice')->where('user_id', $user_id)->count();
$redis->set('user_notice_num:'. $user_id, $notice_count);
//------------------------------------------------------------------------------------------------------------------
//監聽端
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$user_id = 1; //假設用戶id為1,實際可傳參獲取。
while (true) {
echo sse(json_encode(['notice_num' => $redis->get('user_notice_num:' . $user_id) ?? 0]));
ob_flush();
flush();
sleep(1);
}
在實戰項目中的封裝
/**
* @function 與客戶端server send event通信方式
* @param $callback callable 回調,若返回數組代表要輸出json,返回null代表本次迴圈不進行輸出
* @param $millisecond int 數據分發間隔,單位:毫秒
* @return string
* @other void
*/
function sse($callback, $millisecond = 1000) {
set_time_limit(0);
ini_set('output_buffering', 'off');
ini_set('zlib.output_compression', false);
while (@ob_end_flush()) {}
header('Content-Type: text/event-stream; Charset=UTF-8');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Credentials: true");
header('Access-Control-Allow-Methods: *');
header('Access-Control-Allow-Headers: *');
ob_start();
while (true) {
$callback_res = $callback();
if($callback_res !== null) {
$data = json_encode($callback_res, JSON_UNESCAPED_UNICODE);
echo "data:{$data}\n\n";
}
ob_flush();
flush();
usleep($millisecond * 1000);
}
}
//調用
sse(function() {
if('業務邏輯數據存在') {
return ['k' => 'v'];
}
return null;
}, 1000);
SSE優點
- 實現簡單易用。
- 有斷線重連的能力,即使網路中斷,SSE仍舊會嘗試每隔幾秒自動重試的機制。
- 避免了客戶端使用短輪詢造成請求量過大的問題,避免在項目中因需要一個實時的通信小模塊就需要另外搭建WebSocket的問題,得不償失。
SSE缺點
- 完全不相容IE瀏覽器。
- SSE是一種半雙工通信,因為數據只能在一個方向上流動,即從伺服器到客戶端。與之相比,全雙工通信(例如WebSocket)允許數據在兩個方向上同時流動,允許雙向的數據傳輸。
- 為了避免濫用和資源占用,一些瀏覽器可能會限制單個功能變數名稱下的SSE連接數,例如同時最多打開6個連接。而另一些瀏覽器可能會限制整個瀏覽器實例中的SSE連接總數,這個限制不是由JavaScript語言本身所設定的,而是由瀏覽器實現所定義的。
SSE對比WebSocket
協議區別
協議:SSE是基於HTTP協議,而WebSocket則是獨立的協議,它們都可以在瀏覽器和伺服器之間建立持久的連接。
數據格式
SSE通過HTTP協議傳輸的數據格式是文本(通常是JSON格式),因此它適合用於傳輸簡單的文本數據或者事件。而WebSocket可以傳輸文本和二進位數據,在處理音頻、視頻等大型數據時更有優勢。
通信方式
SSE基於半雙工模式,伺服器可以通過發送事件流(event stream)來主動推送數據給客戶端。客戶端通過監聽這些事件來接收數據。而WebSocket是全雙工通信協議,客戶端和伺服器可以隨時發送和接收數據。
相容性
IE10及以上支持 WebSocket。但IE都不相容SSE,並且不同瀏覽器對SSE相容性不一樣,可通過Polyfill解決,官網:https://developer.mozilla.org/en-US/docs/Glossary/Polyfill。