本文由45歲老架構師尼恩分享,感謝作者,有修訂和重新排版。
1、引言
你有沒有想過,為什麼 ChatGPT 的回答能逐字逐句地“流”出來?這一切的背後,都離不開一項關鍵技術——SSE(Server-Sent Events)!
本文從SSE(Server-Sent Events)技術的原理到示例代碼,為你通俗易懂的講解SSE技術的方方面面。
2、AI大模型實時通信技術專題
技術專題系列文章目錄如下,本文是第 4 篇:
- 《全民AI時代,大模型客户端和服務端的實時通信到底用什麼協議?》
- 《大模型時代多模型AI網關的架構設計與實現》
- 《通俗易懂:AI大模型基於SSE的實時流式響應技術原理和實踐示例》
- 《ChatGPT如何實現聊天一樣的實時交互?快速讀懂SSE實時“推”技術 》
- 《AI大模型爆火的SSE技術到底是什麼?萬字長文,一篇讀懂SSE! 》(☜ 本文)
3、初識SSE
SSE(Server-Sent Events)是一種基於 HTTP 協議的服務器推送技術,允許服務端主動向客户端發送數據流。
SSE 可以被理解為 HTTP 的一個擴展或一種特定用法。它不是一個全新的、獨立的協議,而是構建在標準 HTTP/1.1 協議之上的技術。
SSE 就像是服務器打開了一個“單向數據管道”,服務器通過HTTP 擴展 可以持續不斷地流向瀏覽器,無需客户端反覆發起請求。其實很簡單的: SSE = HTTP 擴展字段 + Keepalive 長連接。
SSE 提供了一種簡單、可靠的方式來實現服務器向客户端的實時數據推送。它非常適合通知、實時數據更新、日誌流和類似 ChatGPT 的逐字輸出場景。如果你只需要單向通信,SSE 往往是比 WebSocket 更簡單、更輕量的選擇。
SSE 適用於服務器主動向客户端推送數據的場景,如實時通知、動態更新等。
所以,目前 幾乎所有主流瀏覽器都原生支持SSE。
PS:更詳細的SSE技術資料,可以進一步閲讀以下幾篇:
- Web端即時通訊技術盤點:短輪詢、Comet、Websocket、SSE
- SSE技術詳解:一種全新的HTML5服務器推送事件技術
- 詳解Web端通信方式的演進:從Ajax、JSONP 到 SSE、Websocket
- 一文讀懂前端技術演進:盤點Web前端20年的技術變遷史
- 網頁端IM通信技術快速入門:短輪詢、長輪詢、SSE、WebSocket
- 搞懂現代Web端即時通訊技術一文就夠:WebSocket、socket.io、SSE
4、SSE的誕生背景
4.1 短輪詢、長輪詢、Flash 、 WebSocket
在 SSE 技術出現之前,Web 應用要實現服務器向客户端的實時數據推送,主要依賴以下幾種技術,但它們都存在明顯的缺陷。
4.1.1)短輪詢 (Polling):
原理:用短連接請求數據。客户端以固定的時間間隔(例如每秒一次)頻繁地向服務器發送請求,詢問是否有新數據。
缺點:大量請求可能是無效的(無新數據),浪費服務器和帶寬資源,實時性差。
短輪詢的技術流程圖:
4.1.2)長輪詢 (Long Polling):
原理:使用長連接請求數據。 客户端發送一個請求,服務器會保持這個連接打開(長連接),直到有新數據可用或超時。一旦客户端收到響應,會立即發起下一個請求。
缺點:雖然減少了無效請求,但每個連接仍然需要客户端發起,服務器需要維護大量掛起的連接,實現複雜。
長輪詢 (Long Polling) 的技術突破:減少無效請求,但服務器需維護掛起連接
4.1.3)基於 Flash 的解決方案:
原理:利用 Adobe Flash 插件提供的 Socket 功能實現全雙工通信。
缺點:依賴瀏覽器插件,在移動端(如 iPhone)不受支持,且隨着技術的發展(Flash 被淘汰)已走向消亡。基於 Flash 方法都非原生支持,效率低下或依賴外部插件。
4.1.4)基於 WebSocket的解決方案:
原理:在客户端與服務器之間建立一條全雙工的 TCP 長連接,雙方可隨時互相推送數據。
缺點:
- 1)需要一次額外的協議升級握手(Upgrade: websocket),對 CDN、防火牆、代理服務器的兼容性不如普通 HTTP;
- 2)雙向通信能力在“服務器→客户端單向推送”場景下顯得過度設計,增加心跳、重連、幀解析等複雜度;
- 3)早期瀏覽器支持不一(IE ≤ 9 無原生實現),需要 Polyfill 或 Flash 降級方案。
WebSocket全雙工通道的革命性:擺脱HTTP束縛,實現真正的實時交互(PS: WebSocket 並不僅是 Web 領域的通訊協議,它屬於複雜度較高的二進制通訊協議)。
4.2 SSE 誕生的核心背景
因此,Web 領域迫切需要一種標準化的、高效的、由瀏覽器原生支持的服務器到客户端的單向通信機制。這就是 SSE 誕生的核心背景。
核心需求:
- 1)簡單:易於服務器和客户端實現;
- 2)高效:基於 HTTP/HTTPS,避免不必要的請求開銷;
- 3)標準:成為 W3C 標準,得到瀏覽器原生支持;
- 4)自動重連:內置連接失敗後自動重試的機制。
SSE——真正的服務器推送:
5、SSE的前世今生
SSE 的發展是 Web 標準化進程和實時通信需求共同推動的結果。
下圖概述了其關鍵發展節點:
讓我們對圖中的關鍵階段進行詳細解讀。
1)誕生背景(2006 年以前):
Web 早期只有“請求-響應”範式,實時需求(股票、IM、行情)只能靠輪詢或長輪詢,延遲高、浪費資源。Comet(長連接 iframe、jsonp、xhr-streaming 等 Hack 方案)出現,但實現複雜、瀏覽器兼容性差、佔用連接數高。
業界急需一種“瀏覽器原生、基於 HTTP、單向服務器推送”的輕量機制。
2)概念提出與標準化 (約 2006-2009年):
SSE 的概念最初作為 HTML5 標準的一部分被提出,由 WHATWG (Web Hypertext Application Technology Working Group) 和 W3C (World Wide Web Consortium) 共同推動。
其設計思想是定義一個簡單的、基於 HTTP 的協議,允許服務器通過一個長連接持續地向客户端發送文本流。
2006 年,Opera 9 在瀏覽器裏率先實現名為 Server-Sent Events 的實驗 API,用 DOM 事件把服務器推送的文本塊餵給頁面。
同期 WHATWG HTML5 草案開始收錄相關章節,定義了 text/event-stream MIME 類型及“event: / data:”行協議。
後來,它從龐大的 HTML5 規範中分離出來,成為了一個獨立的 W3C 標準文檔。
2008 年,SSE 被正式寫入 HTML5 草案,隨後進入 W3C 標準流程。
3)瀏覽器支持與推廣 (約 2010-2015年):
2011年左右,主流瀏覽器(如 Firefox、Chrome、Safari、Opera)開始陸續支持 SSE API。 Firefox 6、Chrome 6、Safari 5、Opera 11.5 陸續完成原生實現;IE 系列缺席(直到 Edge 79 才補票)。
關鍵的障礙:Internet Explorer (包括 IE 11) 始終沒有支持 SSE API。這在一定程度上限制了其早期的廣泛應用,開發者通常需要為此準備降級方案(如回落到長輪詢)。
隨着 Chrome、Firefox 等現代瀏覽器的市場份額不斷上升,以及移動端瀏覽器對 SSE 的良好支持,SSE 逐漸成為開發實時 Web 應用的可信選擇。
2014 年 10 月:HTML5 成為 W3C Recommendation,SSE 作為官方子模塊鎖定最終語法,瀏覽器陣營格局定型。
4)正式推薦與成熟 (2015年至2022 ):
2015-2020 年,WebSocket 與 WebRTC 佔據實時通信話題中心,SSE 主要在企業內部儀表盤、日誌 tail 等低頻場景默默使用。
SSE 由於有 “單向文本流 + 自動重連 + 輕量” 特性,所以沒有被WebSocket 與 WebRTC 踩死, 使其在 IoT 設備、移動端 WebView 中仍保有一席之地。
2015年,W3C 發佈了 Server-Sent Events 的正式推薦標準,標誌着該技術的成熟和穩定。在此期間,前端生態框架(如 React、Vue.js)和後端語言(如 Node.js、Python、Java)都提供了對 SSE 的良好支持,出現了大量易用的庫和示例。
5) 大模型時代的爆發(2022 至今):
雖然 WebSocket 提供了全雙工通信能力,但 SSE 因其簡單的 API、基於 HTTP 帶來的良好兼容性(如無需擔心代理或防火牆問題)、以及自動重連等特性,在只需要服務器向客户端推送數據的場景中(如新聞推送、實時行情、狀態更新、AI 處理進度流式輸出等)成為了更簡單、更合適的選擇。
ChatGPT、Claude 等生成式 AI 需要“打字機”式逐 token 輸出,SSE 天然契合:
- 1)基於 HTTP/1.1 無需升級協議,CDN 緩存友好;
- 2)瀏覽器 EventSource API 一行代碼即可接入;
- 3)文本流可直接承載 JSON Lines 或 markdown 片段。
2022 年底起:OpenAI、Anthropic、Google Bard 均把 text/event-stream 作為官方流式回答協議,社區庫(FastAPI SSE-Star、Spring WebFlux、Node sse.js、Go gin-sse)迎來二次繁榮。
6、SSE的技術特徵
SSE和WebSocket 都能建立瀏覽器與服務器的長期通信,但區別很明顯:
- 1)SSE 是單向推送 不是雙向推送, 而且是http協議的一個擴展協議, 使用簡單、自動重連,適合文本類實時推送;
- 2)WebSocket 是雙向通信,不是 http協議的一個擴展協議,WebSocket 更靈活,但實現相對複雜。
流程解讀:
- 1)連接初始化:客户端使用特定的 Content-Type: text/event-stream 向服務器發起一個普通的 HTTP GET 請求。服務器確認並保持連接開放。
- 2)數據推送:服務器通過保持打開的連接,以純文本格式(遵循 data: ...、event: ... 等規範)持續發送數據塊。每個消息以兩個換行符 \n\n 結束。
- 3)連接容錯:如果連接因網絡問題中斷,SSE 客户端內置的機制會自動嘗試重新建立連接,極大地提高了應用的魯棒性。
- 4)客户端處理:瀏覽器端的 EventSource API 會解析收到的數據流,觸發相應的事件(如 onmessage 或自定義事件),讓開發者能夠處理推送來的數據。
SSE 的誕生是 Web 開發對簡單、高效、標準化的服務器推送技術需求的直接結果。它有效地替代了笨拙的輪詢技術,在與 WebSocket 的競爭中,找到了自身在單向數據流場景下的獨特定位。
其發展歷程經歷了從概念提出、瀏覽器支持到成為正式標準的完整路徑。儘管曾受限於 IE,但在現代瀏覽器中已成為一項穩定、可靠且被廣泛採用的技術。如今,在實時通知、金融儀表盤、實時日誌跟蹤和大型語言模型(LLM)的流式響應輸出等場景中,SSE 都是首選的解決方案。
7、默默無聞的SSE為何在AI大模型時代一夜爆火?
SSE 最近站到聚光燈下,幾乎可以説最大的推手就是當前 AI 應用(尤其是 ChatGPT 等大型語言模型)的爆發式增長。SSE 之所以成為 AI 應用的“標配”,是因為 SSE 與 AI 所需的“打字機” 輸出模式 是 天作之合。
7.1 什麼是AI大模型“打字機” 式的逐token輸出?
“打字機”式 逐 token 輸出是一種流式傳輸方式,它模擬了人類打字或思考的過程。
服務器不是等待 LLM 生成整個答案 後一次性發送給 用户,而是 流式輸出, 每生成一個“詞元”(token,可以粗略理解為一個詞或一個字),就立刻發送這個“詞元”。
下面舉一個例子,對比 一下 傳統方式(非流式)和 “打字機” (流式)式 的過程。
傳統方式(非流式)過程如下:
- 1)你提問:“請寫一首關於春天的詩”。
- 2)服務器端的 AI 開始思考、生成,整個過程你需要等待(可能好幾秒甚至更久)。
- 3)AI 生成完整的詩歌:“春風拂面綠意濃,百花爭豔映晴空...”。
- 4)服務器將整首詩作為一個完整的 JSON 對象 { "content": "春風拂面綠意濃,百花爭豔映晴空..." } 發送給客户端。
- 5)客户端一次性收到全部內容並渲染出來。
“打字機”(流式)過程如下:
- 1)你提問:“請寫一首關於春天的詩”。
- 2)服務器端的 AI 生成第一個 token “春”,立刻通過 SSE 發送 data: “春”。
- 3)客户端收到“春”並顯示出來。
- 4)AI 生成第二個 token “風”,立刻發送 data: “風”。
- 5)客户端在“春”後面追加“風”,形成“春風”。
- 6)後續 token “拂”、“面”、“綠”、“意”、“濃”... 依次迅速發送和追加。
- 7)你看到的效果就是文字一個接一個地“打”在屏幕上,就像有人在遠端為你實時打字一樣。
“打字機”(流式) 模式的巨大優勢:
- 1)極低的感知延遲:用户幾乎在提問後瞬間就能看到第一個字開始輸出,無需經歷漫長的等待白屏期,體驗流暢自然。
- 2)提供了“正在進行”的反饋:看着文字逐個出現,給人一種模型正在為你“思考”和“創作”的生動感,而不是在“沉默中宕機”。
- 3)更高效地利用時間:用户可以在前半句還在輸出時,就開始閲讀和理解,節省了總體的認知時間。
7.2 為什麼SSE跟AI大模型是“天作之合”?
這正是 SSE 的設計初衷和核心優勢所在,它與 AI 流式輸出的需求完美匹配。
1)單向通信的完美匹配:
AI 的文本生成過程本質上是服務器到客户端的單向數據推送。客户端只需要接收,不需要在生成過程中頻繁地發送請求。SSE 的“服務器推送”模型正是為此而生,而 WebSocket 的雙向能力在這裏是多餘的。
2)基於 HTTP/HTTPS,簡單且兼容:
SSE 使用標準的 HTTP 協議,這意味着 SSE 易於實現和調試:任何後端框架和前端語言都能輕鬆處理。在瀏覽器中調試時,你可以在“網絡”選項卡中直接看到以文本流形式傳輸的事件,非常直觀。
SSE 使用標準的 HTTP 協議,這還意味着 容易繞過網絡障礙:公司防火牆和代理通常對 HTTP/HTTPS 放行,而可能會阻攔陌生的 WebSocket 協議。這使得 SSE 的部署兼容性極好。
3)內置的自動重連機制:
網絡連接並不完全可靠。如果用户在接收很長的回答時網絡波動,連接中斷,SSE 客户端會自動嘗試重新連接。這對於長時間流的應用至關重要,提供了天然的魯棒性。
4)輕量級的文本協議:
AI 流式輸出傳輸的就是文本(UTF-8編碼)。SSE 的協議 data: ...\n\n 就是為傳輸文本片段而設計的,極其高效和簡單。WebSocket 雖然也能傳文本,但其協議設計還考慮了二進制幀、掩碼等更復雜的情況,對於純文本流來説顯得有些“重”。
5)原生瀏覽器 API:
現代瀏覽器都原生支持 EventSource API,開發者無需引入額外的第三方庫,即可輕鬆實現接收流式數據,減少了依賴和打包體積。
所以,SSE 站到聚光燈下的原因正是:
AI 應用需要“打字機”式的逐 token 輸出體驗,而 SSE 作為一種基於 HTTP 的、簡單的、單向的服務器推送技術,是實現這種體驗最自然、最高效、最可靠的技術選擇。
它就像是為這個場景量身定做的工具,沒有多餘的功能,只有恰到好處的設計。因此,當 ChatGPT 等應用席捲全球時,其背後默默無聞的 SSE 技術也終於從幕後走到了台前,被廣大開發者所重新認識和重視。
8、SSE的技術原理詳解
8.1 工作機制的流程圖
SSE 通過一個持久的 HTTP 連接實現服務器到客户端的單向數據流。
以下是其工作機制的流程圖:
以下是關鍵步驟解析。
1)瀏覽器發起一個 HTTP 請求,Header 中包含:
1Accept: text/event-stream
2)服務器響應類型必須為:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
3)服務器發送事件格式(每個事件以兩個換行符結束):
event: message
data: {"time": "2023-10-05T12:00:00", "value": "New update!"}
id: 12345
retry: 5000
\n\n
4)瀏覽器通過 EventSourceAPI 接收並處理事件。
5)服務器發送 一個特殊“結束”事件,可以結束傳輸。比如,服務器發送一個如 event: end 的消息,可以結束傳輸。客户端預先監聽這個自定義的 end 事件,一旦收到,就知道傳輸結束,並可以選擇主動關閉 EventSource 連接。
6)若連接中斷,瀏覽器會根據 retry字段自動重連。如果沒有收到 特殊“結束”事件, 瀏覽器 可以自動重連。
8.2 SSE與其他通信方式對比
不同通信技術各有適用場景,我們用表格清晰對比:
8.3 SSE的適用場景
1)ChatGPT 式逐字輸出( “打字機” 式逐 詞元 token輸出):
2)實時通知系統:
- a. 新訂單提醒;
- b. 用户消息推送;
- c. 審核狀態更新。
3)實時數據看板:
- a. 股票行情;
- b. 設備監控數據;
- c. 實時日誌流。
9、SSE客户端API詳解
SSE的客户端實現非常簡單,瀏覽器原生提供了EventSource對象來處理與服務器的SSE連接。下面我們詳細介紹它的使用方法和核心特性。
9.1 認識瀏覽器端EventSource對象
瀏覽器兼容性檢測:
在使用SSE前,首先需要確認當前瀏覽器是否支持EventSource(除IE/Edge外,幾乎所有現代瀏覽器都支持)。
檢測方法如下:
// 檢查瀏覽器是否支持SSE
if ('EventSource' in window) {
// 支持SSE,可正常使用
console.log('瀏覽器支持SSE');
} else {
// 不支持SSE,需降級處理
console.log('瀏覽器不支持SSE');
}
創建連接:
使用EventSource創建與服務器的連接非常簡單,只需傳入服務器的SSE接口地址:
// 建立與服務器的SSE連接
// url為服務器提供的SSE接口地址(可同域或跨域)
var source = new EventSource(url);
如果需要跨域請求並攜帶Cookie,可通過第二個參數配置:
// 跨域請求時,允許攜帶Cookie
var source = new EventSource(url, {
withCredentials: true // 默認為false,設為true表示跨域請求攜帶Cookie
});
連接狀態(readyState):
EventSource實例的readyState屬性用於表示當前連接狀態,只讀且有三個可能值:
可以通過該屬性判斷當前連接狀態,例如:
if (source.readyState === EventSource.OPEN) {
console.log('SSE連接已正常建立');
}
9.2 基本使用方法
EventSource通過事件機制處理連接過程中的各種狀態和接收的數據,核心事件包括open、message、error。
下面用流程圖展示SSE客户端的完整使用流程:
連接建立:open事件
當客户端與服務器成功建立SSE連接時,會觸發open事件:
// 方式1:使用onopen屬性
source.onopen = function (event) {
console.log('SSE連接已建立');
// 可在此處做連接成功後的初始化操作,如更新UI狀態
};
// 方式2:使用addEventListener(推薦,可添加多個回調)
source.addEventListener('open', function (event) {
console.log('SSE連接已建立(監聽方式)');
}, false);
接收數據:message事件
當客户端收到服務器推送的數據時,會觸發message事件(默認事件,處理未指定類型的消息):
// 方式1:使用onmessage屬性
source.onmessage = function (event) {
// event.data為服務器推送的文本數據
var data = event.data;
console.log('收到數據:', data);
// 可在此處處理數據,如更新頁面內容
};
// 方式2:使用addEventListener
source.addEventListener('message', function (event) {
var data = event.data;
console.log('收到數據(監聽方式):', data);
}, false);
注意:event.data始終是字符串類型,如果服務器發送的是JSON數據,需要用JSON.parse(data)轉換。
連接錯誤:error事件
當連接發生錯誤(如網絡中斷、服務器出錯)時,會觸發error事件:
// 方式1:使用onerror屬性
source.onerror = function (event) {
// 可根據readyState判斷錯誤類型
if (source.readyState === EventSource.CONNECTING) {
console.log('連接出錯,正在嘗試重連...');
} else {
console.log('連接已關閉,無法重連');
}
};
// 方式2:使用addEventListener
source.addEventListener('error', function (event) {
// 錯誤處理邏輯
}, false);
關閉連接:close()方法
如果需要主動關閉SSE連接(關閉後不會自動重連),可調用close()方法:
// 主動關閉SSE連接
source.close();
console.log('SSE連接已手動關閉');
9.3 自定義事件
默認情況下,服務器推送的消息會觸發message事件。但實際開發中,我們可能需要區分不同類型的消息(如"新訂單通知"和"系統公告"),這時就可以使用自定義事件。
客户端通過addEventListener監聽自定義事件名,例如監聽order事件:
// 監聽名為"order"的自定義事件
source.addEventListener('order', function (event) {
var orderData = event.data;
console.log('收到新訂單:', orderData);
// 處理訂單相關邏輯
}, false);
// 再監聽一個名為"notice"的自定義事件
source.addEventListener('notice', function (event) {
var noticeData = event.data;
console.log('收到系統公告:', noticeData);
// 處理公告相關邏輯
}, false);
注意:自定義事件不會觸發message事件,只會被對應的addEventListener捕獲。
上面代碼中,瀏覽器對 SSE 的foo``notice事件進行監聽。如何實現服務器發送foo``notice事件,請看下文。
10、SSE服務器端技術詳解
服務器要實現SSE,核心是按照特定格式向客户端發送數據。下面詳細介紹服務器端的實現規範。
10.1 HTTP 頭信息要求
服務器向客户端發送SSE數據時,必須設置以下HTTP響應頭,否則客户端無法正確識別為事件流:
Content-Type: text/event-stream // 必須,指定為事件流類型
Cache-Control: no-cache // 必須,禁止緩存,確保數據實時性
Connection: keep-alive // 必須,保持長連接
這三個頭信息是SSE的基礎,缺少任何一個都可能導致連接失敗或數據異常。
10.2 數據傳輸格式
服務器發送的每條消息(message)由多行組成,每行格式為[字段]: 值\n(字段名後必須跟冒號和空格,結尾用換行符\n)。多條消息之間用\n\n(兩個換行符)分隔。
此外,以 : 開頭的行是註釋(服務器可定期發送註釋保持連接)。
基本格式示例:
: 這是一條註釋(客户端會忽略)\n
data: 這是第一條消息\n\n
data: 這是第二條消息的第一行\n
data: 這是第二條消息的第二行\n\n
注意:換行符必須是\n(Unix格式),\r\n可能導致客户端解析錯誤。
10.3 核心字段説明
SSE消息支持四個核心字段,分別用於不同場景。
1)data字段:消息內容:
data字段用於攜帶實際的消息內容,是最常用的字段。
單行數據:
data: Hello, SSE!\n\n // 單行數據,以\n\n結束
多行數據(適合JSON等複雜結構):
data: {\n // 第一行以\n結束
data: "name": "張三",\n // 第二行以\n結束
data: "age": 20\n // 第三行以\n結束
data: }\n\n // 最後一行以\n\n結束
客户端接收後,event.data會自動拼接為完整字符串:{"name": "張三","age": 20}
2)event字段:指定事件類型:
event字段用於指定消息的事件類型,客户端可通過對應事件名監聽(即9.3節的自定義事件)。
服務器發送:
event: order\n // 指定事件類型為order
data: 新訂單ID:12345\n // 消息內容
\n // 消息結束(\n\n簡化為單獨一行)
客户端監聽:
source.addEventListener('order', function(event) {
console.log(event.data); // 輸出:新訂單ID:12345
});
3)id字段:消息標識:
id字段用於給消息設置唯一標識,客户端會自動記錄最後一條消息的id(存於source.lastEventId)。
核心作用:當連接斷線重連時,客户端會在請求頭中攜帶Last-Event-ID: [最後收到的id],服務器可根據該ID恢復數據傳輸(避免重複或丟失)。
服務器發送:
id: msg1001\n // 消息標識
data: 這是第1001條消息\n
\n
客户端重連時的請求頭:
Last-Event-ID: msg1001 // 自動攜帶最後收到的id
4)retry字段:重連間隔:
retry字段用於指定客户端斷線後的重連間隔(單位:毫秒),默認重連間隔約為3秒。
服務器發送:
retry: 5000\n // 告訴客户端,斷線後5秒再重連
data: 重連間隔已設置為5秒\n
\n
5)服務器保持連接示例:
服務器可以定期發送註釋行,保持連接活躍:
: 這是保持連接活動的註釋行\n
: 服務器時間 2023-10-05T12:00:00\n
10.4 服務器發送流程
服務器發送SSE數據的完整流程如下:
下面是一個包含多種字段的服務器發送示例,模擬一個實時通知系統:
: 服務器開始發送消息(註釋)\n
id: 1001\n
event: notice\n
data: 系統將在10分鐘後維護\n\n
id: 1002\n
event: order\n
data: {"orderId": "20230501", "status": "paid"}\n\n
retry: 10000\n
id: 1003\n
data: 重連間隔已調整為10秒\n\n
: 這是保持連接活動的註釋行\n
: 服務器時間 2023-10-05T12:00:00\n
客户端接收後:
- 1)notice事件會捕獲到"系統將在10分鐘後維護"
- 2)order事件會捕獲到訂單JSON數據
- 3)重連間隔被設置為10秒
- 4)最後收到的消息ID是1003(斷線重連時會攜帶)
通過以上規範,服務器就能輕鬆實現SSE功能,向客户端實時推送數據。相比WebSocket,SSE的服務器實現更簡單,無需處理複雜的協議握手,只需按格式發送文本數據即可。
11、SSE實戰代碼示例(基於Spring Boot的實時通信)
接下來, 通過一個完整案例 手把手教你用Spring Boot實現SSE功能。這個案例包含服務端(後端)和客户端(前端)代碼, 可以直接運行體驗服務器主動推送數據的效果。
11.1 案例整體架構
我們要實現的系統包含三個核心部分:
- 1)後端服務:基於Spring Boot,提供SSE連接接口、消息廣播接口和任務進度推送接口;
- 2)前端頁面:一個簡單的HTML頁面,通過EventSource與後端建立SSE連接;
- 3)交互流程:客户端連接後,可接收服務器主動推送的連接狀態、廣播消息和任務進度。
整體架構流程圖:
11.2 服務端實現
11.2.1)準備依賴:
首先創建Spring Boot項目,在pom.xml中添加以下依賴(用於web開發和頁面渲染):
<dependencies>
<!-- Spring Web:提供SSE相關類和HTTP服務 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf:用於渲染前端頁面 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
這些依賴是基礎:spring-boot-starter-web提供了SSE核心類SseEmitter,spring-boot-starter-thymeleaf用於將HTML頁面返回給瀏覽器。
11.2.2)編寫SSE核心控制器:
創建SseController,這是服務端處理SSE連接和消息推送的核心類:
package com.example.sse.controller;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
public class SseController {
// 存儲所有活躍的SSE連接(線程安全的列表)
// CopyOnWriteArrayList適合讀多寫少場景,避免併發問題
private final CopyOnWriteArrayList emitters = new CopyOnWriteArrayList<>();
// 線程池:用於異步發送事件,避免阻塞主線程
private final ExecutorService executor = Executors.newCachedThreadPool();
/
* 客户端訂閲SSE的接口
* 客户端通過訪問該接口建立長連接,接收服務器推送的事件
*/
@GetMapping(value = "/sse/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe() {
// 創建SseEmitter實例,設置超時時間為無限(默認30秒會超時,這裏設為Long.MAX_VALUE避免自動斷開)
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
// 將新連接加入活躍列表(後續推送消息時會遍歷這個列表)
emitters.add(emitter);
// 設置連接完成/超時的回調:從活躍列表中移除該連接,釋放資源
emitter.onCompletion(() -> emitters.remove(emitter)); // 連接正常關閉時
emitter.onTimeout(() -> emitters.remove(emitter)); // 連接超時關閉時
// 發送初始連接成功消息(給客户端的"歡迎消息")
try {
emitter.send(SseEmitter.event()
.name("CONNECTED") // 事件名稱:客户端可通過"CONNECTED"事件監聽
.data("You are successfully connected to SSE server!") // 消息內容
.reconnectTime(5000)); // 告訴客户端:如果斷開連接,5秒後重連
} catch (IOException e) {
// 發送失敗時,標記連接異常結束
emitter.completeWithError(e);
}
return emitter; // 將emitter返回給客户端,保持連接
}
/
* 廣播消息接口:向所有已連接的客户端推送消息
* 可通過瀏覽器訪問 http://localhost:8080/sse/broadcast?message=xxx 觸發
/
@GetMapping("/sse/broadcast")
public String broadcastMessage(@RequestParam String message) {
// 用線程池異步執行廣播,避免阻塞當前請求
executor.execute(() -> {
// 遍歷所有活躍連接,逐個發送消息
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name("BROADCAST") // 事件名稱:客户端監聽"BROADCAST"事件
.data(message) // 廣播的消息內容
.id(String.valueOf(System.currentTimeMillis()))); // 消息ID(用於重連時定位)
} catch (IOException e) {
// 發送失敗(可能客户端已斷開),從列表中移除並標記連接結束
emitters.remove(emitter);
emitter.completeWithError(e);
}
}
});
return "Broadcast message: " + message; // 給調用者的響應
}
/
* 模擬長時間任務:向客户端推送實時進度
* 適合文件上傳、數據處理等需要實時反饋進度的場景
/
@GetMapping("/sse/start-task")
public String startTask() {
// 異步執行任務,避免阻塞當前請求
executor.execute(() -> {
try {
// 模擬任務進度:從0%到100%,每次增加10%
for (int i = 0; i <= 100; i += 10) {
Thread.sleep(1000); // 休眠1秒,模擬處理耗時
// 向所有客户端推送當前進度
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name("PROGRESS") // 事件名稱:客户端監聽"PROGRESS"事件
.data(i + "% completed") // 進度數據
.id("task-progress")); // 固定ID,標識這是任務進度消息
} catch (IOException e) {
// 發送失敗,移除連接
emitters.remove(emitter);
}
}
// 任務完成時,發送結束消息
if (i == 100) {
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name("COMPLETE") // 事件名稱:客户端監聽"COMPLETE"事件
.data("Task completed successfully!"));
} catch (IOException e) {
emitters.remove(emitter);
}
}
}
}
} catch (InterruptedException e) {
// 任務被中斷時,恢復線程中斷狀態並退出
Thread.currentThread().interrupt();
break;
}
});
return "Task started!"; // 告訴調用者任務已啓動
}
}
核心代碼説明:
- 1)SseEmitter:Spring提供的SSE核心類,每個實例對應一個客户端連接;
- 2)emitters列表:管理所有活躍連接,方便廣播消息(類似"客户端註冊表");
- 3)executor線程池:異步處理消息發送,避免阻塞主線程(如果同步發送,一個客户端卡住會影響所有用户)。
事件發送:通過 emitter.send(SseEmitter.event()) 構建消息,可指定事件名、數據、ID和重連時間。
11.2.3)編寫頁面控制器:
創建PageController,用於將前端頁面返回給瀏覽器:
package com.example.sse.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller // 注意這裏用@Controller而非@RestController,用於返回頁面
public class PageController {
/*
* 訪問根路徑時,返回SSE客户端頁面
/
@GetMapping("/")
public String index() {
// 返回src/main/resources/templates目錄下的sse-client.html
return "sse-client";
}
}
11.3 客户端實現(HTML頁面)
在 src/main/resources/templates 目錄下創建 sse-client.html,這是用户交互的前端頁面:
Server-Sent Events (SSE) Client Connect to SSE Disconnect Send Broadcast Start Task Messages:
客户端核心邏輯:
- 1)eventSource:EventSource實例,是客户端與服務端SSE連接的"橋樑";
- 2)事件監聽:通過addEventListener監聽服務端定義的事件(CONNECTED/BROADCAST等);
- 3)自動重連:當連接斷開時,EventSource會自動重試(無需手動寫重連邏輯);
- 4)交互函數:connectSSE/disconnectSSE等函數對應頁面按鈕,實現用户操作。
11.4 運行與測試
啓動步驟:
- 1)確保Spring Boot項目配置正確(默認端口8080,無需額外配置);
- 2)啓動Spring Boot應用(運行帶有main方法的啓動類);
- 3)打開瀏覽器,訪問 http://localhost:8080/,看到客户端頁面。
功能測試:
- 1)建立連接:點擊"Connect to SSE"按鈕,頁面會顯示"連接成功"的消息(服務端通過CONNECTED事件推送)
- 2)發送廣播:點擊"Send Broadcast"按鈕,輸入任意消息(如"Hello SSE"),頁面會顯示廣播消息(服務端向所有連接的客户端推送)
- 3)啓動任務:點擊"Start Task"按鈕,頁面會每秒收到一條進度消息(從0%到100%),最後顯示"任務完成"
- 4)斷開連接:點擊"Disconnect"按鈕,連接關閉,不再接收消息。
測試流程圖:
11.5 服務端的關鍵技術點
1)SseEmitter的作用:Spring封裝的SSE工具類,簡化了"保持連接+發送事件"的實現,無需手動處理HTTP流格式。
2)連接管理:用CopyOnWriteArrayList存儲活躍連接,確保線程安全;通過onCompletion/onTimeout回調清理無效連接,避免內存泄漏。
3)異步處理:必須用線程池(ExecutorService)異步發送消息,否則會阻塞主線程,導致新請求無法處理。
4)事件設計:通過name區分不同類型的事件(如PROGRESS/BROADCAST),客户端按需監聽,邏輯更清晰。
5)自動重連:SSE客户端(EventSource)內置重連機制,網絡恢復後會自動重新連接,無需額外代碼。
通過這個案例,你可以清晰看到SSE的優勢:實現簡單(幾行代碼就能建立實時連接)、無需額外協議(基於HTTP)、自帶重連機制。如果你的場景只需要服務器單向推送數據(如實時通知、進度更新),SSE會是比WebSocket更輕量的選擇。
12、選SSE還是選WebSocket?
12.1 SSE與WebSocket全面對比
來對 Server-Sent Events (SSE) 和 WebSocket 進行一場全面、深入的對比。
為了更直觀地理解兩者的工作模式差異,請看下面的序列圖:
1)協議與連接建立:
SSE 協議與連接建立:
- a. 基於純粹的 HTTP。客户端發起一個普通的 HTTP GET 請求,並攜帶特殊的頭 Accept: text/event-stream。
- b. 服務器響應後,保持這個 TCP 連接打開,並開始發送數據流 ,直到遇到結束標記。
- c. 這是一種長連接的 HTTP 用法。
WebSocket 協議與連接建立:
- a. 基於獨立的 WebSocket 協議。連接始於一個特殊的 HTTP 請求,即 “協議升級”請求(Connection: Upgrade, Upgrade: websocket)。
- b. 服務器響應 HTTP 101 Switching Protocols 後,最初的 HTTP 連接被替換為 WebSocket 連接,此後通信不再遵循 HTTP 協議,而是在其之上建立一個全雙工的通道。
2)數據流與通信模式:
SSE:單向通信。設計初衷就是讓服務器能夠主動、高效地向客户端推送數據。•數據是文本流,格式簡單且可讀性強。每條消息可以附帶一個事件類型(event:)和一個ID(id:)。•客户端使用 EventSource API 監聽來自服務器的事件。
WebSocket:全雙工通信。在連接建立後,客户端和服務器處於完全平等的地位,可以隨時、任意地相互發送消息。 另外,WS協議 支持文本和二進制數據,靈活性極高,非常適合需要頻繁雙向交互的場景(如遊戲、協作編輯)。
3)能力與特性:
SSE:內置自動重連機制。如果連接斷開,EventSource 對象會自動嘗試重新連接,並在重連後自動發送上一個收到的事件ID,服務器據此可判斷錯過了哪些消息,實現數據恢復。SSE有出色的瀏覽器支持。所有現代瀏覽器(Chrome, Firefox, Safari, Edge)都原生支持,Internet Explorer (古代瀏覽器)完全不支持(通常需要 polyfill 或降級方案)。
WebSocket:無自動重連。連接斷開後,需要開發者手動編寫重連邏輯和狀態恢復邏輯。WS協議比SSE協議有更廣泛的瀏覽器支持,包括 Internet Explorer 10+。
4)開發與集成:
SSE:開發與集成 非常簡單。服務器端幾乎不需要特殊的庫,任何能輸出 HTTP 流的後端語言都可以實現。客户端 API 也非常直觀。與現有 HTTP 認證、CORS 機制完全兼容,處理方式與普通 HTTP 請求一致。
WebSocket:開發與集成 相對複雜。服務器端需要支持 WebSocket 協議的庫(如 ws for Node.js, Socket.IO 等)。客户端需要處理連接狀態、心跳包等。 雖然升級握手是 HTTP,但後續通信是獨立協議,因此一些複雜的網絡環境(如某些代理服務器)可能會帶來問題。
12.2 SSE與WebSocket到底該如何選擇?
選擇的關鍵在於應用場景和核心需求。
* 選擇 SSE 的場景:
選擇 SSE 的場景包括:
- 1)服務器到客户端的單向數據流。
- 2)簡單和快速實現是關鍵因素。
- 3)需要自動錯誤恢復(重連)。
- 4)數據傳輸格式是文本(如 JSON),且不需要二進制。
SSE 典型的應用場景包括:
- 1)實時新聞推送、體育比分更新。
- 2)金融報價行情(如股票價格變動)。
- 3)社交媒體動態更新(如 Twitter 時間線)。
- 4)服務器日誌流監控。
- 5)AI 處理進度或結果的流式輸出。
* 選擇 WebSocket 場景:
選擇 WebSocket 的場景包括:
- 1)真正的實時雙向通信,客户端和服務器都需要頻繁地發送消息。
- 2)需要傳輸二進制數據(如視頻、音頻、圖像碎片)。
- 3)構建交互性極強的應用,其中低延遲至關重要。
WebSocket 典型的應用場景包括:
- 1)實時在線聊天應用(如 Slack、Discord、RainbowChat-Web)。
- 2)多人在線遊戲。
- 3)協同編輯工具(如 Google Docs)。
- 4)實時儀表盤和控制面板(需要雙向控制)。
12.3 WebSocket+SSE 混合架構
一般來説大型應用場景,強網用 WebSocket、弱網適合使用SSE ,這就是WebSocket+SSE 混合架構。強網用 WebSocket、弱網自動降級到 SSE 的混合架構, 核心在於網絡質量動態評估和雙通道無縫切換。
WebSocket+SSE 混合架構 具體實現方案如下:
核心模塊:網絡質量探針(客户端) 實現
class NetworkProbe {
// 關鍵指標
static RTT_THRESHOLD = 300 // RTT超過300ms視為弱網
static PACKET_LOSS_THRESHOLD = 0.2 // 丟包率>20%觸發降級
// 網絡狀態檢測
async check() {
const { rtt, packetLoss } = await this._measure()
return {
isWeak: rtt > NetworkProbe.RTT_THRESHOLD ||
packetLoss > NetworkProbe.PACKET_LOSS_THRESHOLD
}
}
// 實際測量方法
_measure() {
return new Promise(resolve => {
const start = Date.now()
fetch('/ping', { cache: 'no-store' })
.then(() => {
const rtt = Date.now() - start
resolve({ rtt, packetLoss: 0 })
})
.catch(() => resolve({ rtt: Infinity, packetLoss: 1 }))
})
}
}
核心模塊:雙協議連接管理器(客户端)
class HybridConnection {
constructor() {
this.currentProtocol = null
this.ws = null
this.sse = null
this.messageQueue = [] // 消息緩衝隊列
}
// 智能連接初始化
async connect() {
const { isWeak } = await new NetworkProbe().check()
this.currentProtocol = isWeak ? 'sse' : 'ws'
if (this.currentProtocol === 'ws') {
this._initWebSocket()
} else {
this._initSSE()
}
}
// WebSocket初始化
_initWebSocket() {
this.ws = new WebSocket('wss://api.example.com')
this.ws.onmessage = this._handleMessage
// 發送緩衝隊列消息
this.messageQueue.forEach(msg => this.ws.send(msg))
this.messageQueue = []
}
// SSE初始化
_initSSE() {
this.sse = new EventSource('https://api.example.com/sse')
this.sse.onmessage = this._handleMessage
}
// 統一消息處理
_handleMessage = (event) => {
const data = event.data || event
// 業務邏輯處理...
}
// 發送消息(自動選擇協議)
send(data) {
if (this.currentProtocol === 'ws' && this.ws?.readyState === 1) {
this.ws.send(JSON.stringify(data))
} else if (this.currentProtocol === 'sse') {
// SSE需通過獨立HTTP請求發送
fetch('/send', { method: 'POST', body: JSON.stringify(data) })
} else {
// 協議切換中暫存消息
this.messageQueue.push(JSON.stringify(data))
}
}
// 協議切換(核心!)
async switchProtocol() {
const { isWeak } = await new NetworkProbe().check()
// 無需切換
if (isWeak && this.currentProtocol === 'sse') return
if (!isWeak && this.currentProtocol === 'ws') return
// 執行切換
if (isWeak) {
this.ws?.close()
this._initSSE()
this.currentProtocol = 'sse'
} else {
this.sse?.close()
this._initWebSocket()
this.currentProtocol = 'ws'
}
}
}
12.4 AI大模型中該選擇SSE協議還是WebSocket?
直接答案:對於絕大多數 chat2ai 應用 優先選擇 SSE (Server-Sent Events)。複雜的 chat2ai 應用 優先選擇WebSocket。
但這並非絕對,我們需要根據具體的功能需求來決定。下面我將為你進行詳細的分析和推理。
12.4.1)核心決策分析:
AI聊天應用的核心交互是:
- 1)客户端發送一條消息(一個問題)。
- 2)服務器接收後,調用大語言模型(LLM)API。
- 3)服務器將模型流式返回的答案(逐詞或逐句)實時推送給客户端。
- 4)客户端實時渲染這個流式的答案,營造出“打字機”效果。
這個過程的關鍵在於第3步,即服務器向客户端的單向數據推送。這正是 SSE 的絕對主場。
12.4.2)為什麼 SSE 是更優的選擇?
以下流程圖清晰地展示了基於不同技術方案的聊天交互過程,其中突出了SSE方案的巨大優勢:
正如上圖所示:SSE 方案在實現上更加直接和高效,因為它基於 HTTP,並且專門為服務器到客户端的單向數據流設計。
此外,SSE 還帶來了以下巨大優勢:
1)開發複雜度極低:
- a. 後端:你不需要引入任何複雜的 WebSocket 庫(如 ws, Socket.IO)。你只需要建立一個普通的 HTTP 路由(如 POST /chat 用於發送消息,GET /chat/stream 用於接收流),並在控制器中輸出 text/event-stream 格式的響應流。
- b. 前端:使用瀏覽器原生的 EventSource API 即可輕鬆監聽數據流,幾行代碼就能實現。無需實例化和管理 WebSocket 連接對象。
2)出色的兼容性與可維護性:
- a. SSE 基於 HTTP,這意味着它更容易通過公司防火牆、代理,與現有的認證系統(如 Cookie、JWT)、CORS 策略協同工作,幾乎不會遇到奇怪的網絡問題。
- b. 在瀏覽器“網絡”選項卡中,SSE 的流清晰可見,易於調試。每個消息都是可讀的文本,調試體驗非常好。
3)內置的自動重連與斷點續傳機制:
- a. 這是 SSE 的“殺手級特性”。網絡連接不穩定是移動端的常見問題。如果用户在接收一個很長答案的過程中網絡中斷,SSE 會在網絡恢復後自動重新連接。
- b. 更強大的是,SSE 協議支持發送最後一個消息的 ID。服務器可以識別出這個 ID,並判斷客户端錯過了哪些數據,從而從斷點處繼續發送,而不是重新開始生成整個回答。這既節省了昂貴的 API 調用費用,也提升了用户體驗。這在 WebSocket 中需要手動實現所有邏輯,非常複雜。
12.4.3)WebSocket 的適用場景:
雖然 SSE 是主流選擇,但在 chat2ai 應用變得非常複雜時,WebSocket 可能會成為更好的選擇。
在以下情況下, 應該考慮使用 WebSocket:
1)需要極高頻的雙向通信:不僅僅是用户提問->AI回答。例如:
- a. 實時協作編輯:多個用户同時編輯一份由 AI 生成的文檔,每個人的輸入都需要實時同步給其他所有人。
- b. AI多人遊戲:基於 AI 生成劇情和環境的實時互動遊戲,玩家的每一個動作都需要實時影響虛擬世界。
2)當需要傳輸二進制數據的時候:有的聊天應用不僅支持文本,還支持實時語音對話(客户端錄音發送二進制音頻流,服務器返回 AI 語音二進制流)。WebSocket 對二進制數據的支持是天生的。
3)你需要非常精確的控制心跳和連接狀態: - WebSocket 允許 手動發送 Ping/Pong 幀來檢測連接活性,雖然複雜,但給了開發人員最大的控制權。
12.4.4)傳輸協議選型 結論與建議:
1)起步和絕大多數情況:從 SSE 開始。這是最直接、最高效、最能給你帶來穩定體驗的選擇。使用sse 遇到的技術挑戰會更少,開發速度更快。ChatGPT、Claude 等絕大多數頂級應用都使用 SSE 不是沒有道理的。
2)未來如果需要擴展:採用混合架構。如果應用未來需要加入上述 WebSocket 的適用功能(如實時語音),完全可以同時使用兩種協議:使用 SSE 專門處理 AI 文本答案的流式推送。使用 WebSocket 專門處理 實時語音、實時協作等真正的雙向通信功能。或者 強弱結合,自動切換。
因此,對於 chat2ai 的傳輸協議選型答案是:優先選擇 SSE。
13、參考資料
[0] EventSource API Docs
[1] Web端即時通訊技術盤點:短輪詢、Comet、Websocket、SSE
[2] SSE技術詳解:一種全新的HTML5服務器推送事件技術
[3] 使用WebSocket和SSE技術實現Web端消息推送
[4] 詳解Web端通信方式的演進:從Ajax、JSONP 到 SSE、Websocket
[5] 使用WebSocket和SSE技術實現Web端消息推送
[6] 一文讀懂前端技術演進:盤點Web前端20年的技術變遷史
[7] WebSocket從入門到精通,半小時就夠!
[8] 網頁端IM通信技術快速入門:短輪詢、長輪詢、SSE、WebSocket
[9] 搞懂現代Web端即時通訊技術一文就夠:WebSocket、socket.io、SSE
[10] 大模型時代多模型AI網關的架構設計與實現
[11] 全民AI時代,大模型客户端和服務端的實時通信到底用什麼協議?
[12] 通俗易懂:AI大模型基於SSE的實時流式響應技術原理和實踐示例
[13] Web端實時通信技術SSE在攜程機票業務中的實踐應用
[14] ChatGPT如何實現聊天一樣的實時交互?快速讀懂SSE實時“推”技術
即時通訊技術學習:
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點此)
(本文已同步發佈於:http://www.52im.net/thread-4885-1-1.html)