
1. 項目概述為什么SSE在今天依然值得你投入時間如果你正在構建一個需要實時向客戶端推送數據的Web應用比如一個股票行情看板、一個新聞頭條的滾動通知或者一個后臺任務的進度條你可能會立刻想到WebSocket。確實WebSocket是雙向、全雙工的功能強大。但很多時候我們面對的場景其實要簡單得多服務器單向地向客戶端推送數據客戶端只需要接收。在這種“只讀”的實時場景下有一個被嚴重低估的“老將”其實更合適——那就是SSE全稱Server-Sent Events。我第一次深入使用SSE是在一個內部監控系統里。需求很簡單在管理后臺我需要一個實時更新的圖表展示服務器集群的CPU和內存使用率。用WebSocket當然可以但殺雞用牛刀了而且還要處理連接管理、心跳、重連等一系列“重量級”的配套工作。用傳統的輪詢Polling數據更新頻率要求是1秒一次頻繁的HTTP請求對服務器和網絡都是不必要的開銷。就在我糾結時SSE進入了視野。它基于最普通的HTTP/HTTPS協議客戶端通過一個持久的連接監聽服務器發來的事件流服務器可以隨時推送數據。實現那個監控圖表后端只用了不到50行代碼前端更是簡單到只用監聽一個EventSource對象。從那以后但凡遇到服務器向客戶端單向推送數據的場景SSE就成了我的首選方案。SSE協議其實并不新它屬于HTML5規范的一部分。但正因為其簡單、高效且基于HTTP讓它具有了WebSocket所不具備的獨特優勢天然的防火墻友好性使用標準HTTP端口和協議、內置的自動重連機制、以及更簡單的實現成本。在微服務架構和Serverless場景下構建一個輕量的、事件驅動的數據推送服務SSE往往能帶來意想不到的簡潔和穩定。接下來我們就徹底拆解SSE從協議原理到實戰避坑給你一份能直接上手的完全指南。2. SSE協議核心原理與工作機制拆解要用好SSE不能只停留在API調用的層面必須理解其底層的工作機制。這能幫助你在遇到詭異問題時快速定位是網絡問題、服務器問題還是代碼邏輯問題。2.1 基于HTTP長連接的“事件流”SSE的本質是一個長時間保持打開的HTTP連接。與我們熟悉的“請求-響應”模式不同在SSE中客戶端發起一個GET請求后這個連接并不會在服務器返回數據后立即關閉而是會一直保持Keep-Alive。服務器通過這個持久的連接可以持續地、分批次地向客戶端發送數據。這個連接傳輸的內容格式有嚴格規定即text/event-stream格式。這不是JSON也不是XML而是一種簡單的、面向行的文本格式。服務器響應的Content-Type頭部必須是text/event-stream這等于告訴瀏覽器“接下來我發送的是一個事件流請你用SSE的規則來解析它?!闭麄€通信過程可以這樣類比客戶端像打開了一個一直播放的廣播頻道HTTP連接服務器是這個電臺。電臺服務器會不定時地播報一條條消息事件。每條消息都有其特定的格式。客戶端只需要調諧到這個頻道創建EventSource然后靜靜地收聽即可。2.2 事件流的數據格式規范SSE消息的格式極其簡單主要由四種類型的行構成每行以換行符\n結束在協議中兩個換行符\n\n標識一條消息的結束。data:行這是消息的核心數據行。一行data:后面跟著實際要發送的數據。如果數據內容很長可以分成多行每行都以data:開頭。data: {stock: AAPL, price: 175.32}或者多行數據最終會被拼接成一個字符串data: 這是一段很長的消息 data: 它被分成了兩行來發送??蛻舳耸盏胶驟ventSource的onmessage事件會接收到拼接后的完整字符串這是一段很長的消息\n它被分成了兩行來發送。。event:行用于指定事件類型。這是一個可選字段。如果不指定默認事件類型為message。如果指定了例如event: update那么客戶端就需要通過addEventListener(update, ...)來監聽這個特定類型的事件而不是通用的onmessage。event: systemAlert data: 服務器將于凌晨2點進行維護。id:行用于設置當前消息的ID。這個ID有兩個重要作用。第一客戶端在自動重連時會在HTTP請求頭中帶上最后一個收到的消息IDLast-Event-ID服務器可以據此決定從哪條消息開始恢復發送實現斷點續傳。第二在瀏覽器開發者工具的Network標簽中你可以看到這個ID便于調試。id: 1024 data: 任務進度更新retry:行用于建議客戶端在連接斷開后重新連接前的等待時間毫秒。這只是一個建議值瀏覽器不一定會嚴格遵守但大多數實現會尊重它。這對于控制重連頻率、減輕服務器在故障時的壓力非常有用。retry: 10000這告訴客戶端“如果連接斷了等10秒再試?!币粭l完整的SSE消息示例event: priceUpdate id: 12345 data: {symbol:GOOGL,price:2800.50} retry: 5000注意末尾的兩個換行符它標志著這條消息的結束瀏覽器會觸發相應的事件。注意SSE規范要求文本必須是UTF-8編碼。此外雖然數據內容可以是任何字符串但通常我們傳遞JSON字符串因為這樣在前端處理起來最方便。服務器端在發送前需要將對象JSON.stringify()一下。2.3 與WebSocket、長輪詢的對比選型理解了SSE是什么我們更需要知道它適合用在哪兒。通過對比它的定位就非常清晰了。特性Server-Sent Events (SSE)WebSocket長輪詢 (Long Polling)通信方向單向(服務器 - 客戶端)雙向(全雙工)模擬單向 (客戶端拉取)協議HTTP / HTTPS獨立的ws://或wss://協議HTTP / HTTPS連接性質持久HTTP連接持久、獨立的TCP連接短暫的HTTP連接請求掛起數據格式text/event-stream(文本)二進制或文本幀任意 (通常為JSON)自動重連內置支持(通過retry和id)需要手動實現需要手動實現瀏覽器兼容性除IE/Edge Legacy外現代瀏覽器廣泛支持現代瀏覽器廣泛支持所有瀏覽器典型場景實時通知、股票行情、新聞推送、監控日志聊天室、協同編輯、在線游戲、實時交易兼容性要求極高的舊系統選型心法首選SSE當你只需要服務器向客戶端推送數據且客戶端不需要頻繁向服務器發送指令時。例如Dashboard、實時價格更新、新聞訂閱、服務器端事件觸發的前端反饋如“你的報告已生成”。必須用WebSocket當應用需要頻繁的、低延遲的雙向交互時。例如聊天應用你一言我一語、多人在線游戲狀態實時同步、遠程桌面控制。考慮長輪詢只有在需要支持非常古老的瀏覽器如IE9及以下且無法使用Polyfill時才作為備選。它的效率低于SSE因為每次通信都需要建立新的HTTP連接。一個關鍵認知SSE基于HTTP這既是優勢也是限制。優勢是它穿透防火墻和代理服務器毫無壓力因為所有網絡設施都認識HTTP。限制是一個瀏覽器對同一個域名有并發HTTP連接數的限制通常是6個。如果你在一個頁面里創建了多個EventSource連接到同一個域名可能會占滿連接池影響其他資源的加載。而WebSocket連接不受此限制。3. 服務端實現詳解以Spring Boot為例理論說清楚了我們動手實現。服務端是SSE的源頭它的實現質量直接決定了連接的穩定性和數據的正確性。這里我以最常用的Java Spring Boot框架為例因為它在企業級開發中應用極廣。我會分享兩種主流實現方式及其適用場景。3.1 基于SseEmitter的響應式推送Spring Framework 4.2 提供了SseEmitter類它是實現SSE的推薦方式非常直觀。你可以把它想象成一個可以向客戶端發送事件的“管道”。核心實現步驟創建控制器和映射首先定義一個REST控制器并創建一個返回SseEmitter的端點。import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; RestController RequestMapping(/sse) public class SseController { // 用于保存用戶ID與SseEmitter的映射實現定向推送 private static final ConcurrentHashMapString, SseEmitter emitterMap new ConcurrentHashMap(); /** * 客戶端連接入口 * param clientId 客戶端唯一標識 * return SseEmitter */ GetMapping(path /connect/{clientId}, produces text/event-stream) public SseEmitter connect(PathVariable String clientId) { // 設置連接超時時間0表示永不超時但實際受服務器和網絡配置影響 SseEmitter emitter new SseEmitter(0L); emitterMap.put(clientId, emitter); // 連接建立時可以發送一個初始事件 try { emitter.send(SseEmitter.event().name(connect).data(Client [ clientId ] connected.)); } catch (IOException e) { // 發送失敗通常意味著客戶端已斷開 emitterMap.remove(clientId); emitter.completeWithError(e); return emitter; } // 設置連接完成和超時、錯誤時的回調用于資源清理 emitter.onCompletion(() - { System.out.println(Client [ clientId ] completed.); emitterMap.remove(clientId); }); emitter.onTimeout(() - { System.out.println(Client [ clientId ] timed out.); emitter.complete(); emitterMap.remove(clientId); }); emitter.onError((ex) - { System.out.println(Client [ clientId ] error: ex.getMessage()); emitter.completeWithError(ex); emitterMap.remove(clientId); }); return emitter; } /** * 向特定客戶端發送消息 */ PostMapping(/send/{clientId}) public String sendMessage(PathVariable String clientId, RequestBody String message) { SseEmitter emitter emitterMap.get(clientId); if (emitter ! null) { try { // 發送一個名為“message”的事件 emitter.send(SseEmitter.event().name(message).data(message)); return Message sent to client [ clientId ]; } catch (IOException e) { // 發送失敗移除失效的emitter emitterMap.remove(clientId); return Client [ clientId ] connection lost.; } } return Client [ clientId ] not found.; } }關鍵點解析produces text/event-stream這個注解至關重要它確保了HTTP響應頭Content-Type: text/event-stream被正確設置。超時設置new SseEmitter(0L)設置超時為0代表連接理論上永久有效。在實際生產環境中你可能需要設置一個合理的超時時間如30分鐘并配合客戶端自動重連機制。因為網絡代理、負載均衡器也可能有自身的連接超時設置。資源管理onCompletion、onTimeout、onError回調是必須的。這是清理emitterMap防止內存泄漏的關鍵??蛻舳岁P閉頁面、網絡斷開都會觸發這些回調。并發處理上面的例子使用了ConcurrentHashMap來存儲SseEmitter這在單機部署時沒問題。如果是多機部署這個映射需要替換為分布式緩存如Redis否則你無法向連接到其他服務器實例的客戶端推送消息。3.2 使用Spring WebFlux實現響應式流如果你的項目本身就是響應式架構使用Spring WebFlux那么使用Flux和ServerSentEvent對象來實現SSE會更加優雅和強大它能更好地處理背壓等流控問題。import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Flux; import org.springframework.http.codec.ServerSentEvent; import java.time.Duration; import java.time.LocalTime; RestController public class ReactiveSseController { /** * 模擬一個每秒發送一次服務器時間的無限流 */ GetMapping(path /stream-time, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxServerSentEventString streamTime() { return Flux.interval(Duration.ofSeconds(1)) .map(sequence - ServerSentEvent.Stringbuilder() .id(String.valueOf(sequence)) // 設置消息ID .event(timeUpdate) // 設置事件類型 .data(Server Time: LocalTime.now()) .retry(Duration.ofSeconds(5).toMillis()) // 設置重試時間 .build()); } /** * 更復雜的例子從某個消息中間件如Kafka訂閱事件并轉發為SSE */ // GetMapping(/stream-kafka) // public FluxServerSentEventMyEvent streamFromKafka() { // return kafkaReceiver.receive() // 假設有一個Kafka接收器 // .map(record - ServerSentEvent.builder(record.value()).build()) // .doOnError(e - log.error(Stream error, e)) // .onErrorResume(e - Flux.empty()); // 發生錯誤時結束流 // } }WebFlux方式的優勢聲明式編程代碼更簡潔直接返回一個FluxServerSentEvent流即可。內置背壓支持如果客戶端處理速度慢響應式流框架會自動調節數據發送速率避免服務器壓垮客戶端。與響應式生態無縫集成可以輕松地將來自Kafka、RabbitMQ、數據庫變更流Change Data Capture的事件通過SSE實時推送到前端。實操心得在初期我建議先用SseEmitter因為它更直觀與傳統Spring MVC編程模型一致易于調試。當你需要處理高并發、或者數據源本身就是一個流如監控指標流時再考慮遷移到WebFlux方案。另外無論用哪種方式一定要在Nginx或Apache等反向代理服務器配置中禁用對/event-stream這類路徑的緩沖buffering和超時timeout否則代理服務器可能會緩存你的數據流導致客戶端接收延遲或中斷。4. 客戶端瀏覽器接入與事件處理服務端準備好了前端接入卻簡單得令人驚訝。現代瀏覽器通過EventSourceAPI原生支持SSE。4.1 基礎連接與事件監聽最基本的用法如下// 假設服務端端點 http://your-api.com/sse/connect/user123 const eventSource new EventSource(http://your-api.com/sse/stream-time); // 監聽默認的 message 事件服務端未指定event類型時觸發 eventSource.onmessage function(event) { console.log(收到消息默認:, event.data); // event.data 是字符串通常是JSON需要解析 const data JSON.parse(event.data); updateUI(data); }; // 監聽特定類型的事件服務端發送了 event: timeUpdate eventSource.addEventListener(timeUpdate, function(event) { console.log(收到時間更新事件:, event.data); // 處理特定事件邏輯 }); // 監聽連接打開事件 eventSource.onopen function(event) { console.log(SSE連接已建立); }; // 監聽錯誤事件 eventSource.onerror function(event) { console.error(SSE連接錯誤:, event); // EventSource在出錯時會自動嘗試重連你可以根據event.target.readyState判斷狀態 // readyState: 0 (CONNECTING), 1 (OPEN), 2 (CLOSED) if (event.target.readyState EventSource.CLOSED) { console.log(連接已關閉); } };4.2 高級特性自定義頭部、身份認證與跨域基礎的EventSource構造函數功能有限它不支持自定義HTTP請求頭。這在需要傳遞認證信息如JWT Token時是個大問題。解決方法有兩種方法一使用Fetch API模擬EventSource這是目前更推薦的方式它提供了完全的控制權。async function createSSEConnection(url, token) { const response await fetch(url, { method: GET, headers: { Authorization: Bearer ${token}, Accept: text/event-stream, // 重要告訴服務器我需要事件流 // 如果支持可以攜帶上次斷開時的Last-Event-ID // Last-Event-ID: lastEventId }, // 其他fetch配置... }); if (!response.ok || !response.body) { throw new Error(SSE連接失敗: ${response.status}); } const reader response.body.getReader(); const decoder new TextDecoder(); let buffer ; while (true) { const { done, value } await reader.read(); if (done) { console.log(流已結束); break; } buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop(); // 最后一行可能是不完整的放回緩沖區 let eventName message; let data ; let id null; let retry null; for (const line of lines) { if (line.startsWith(event:)) { eventName line.substring(6).trim(); } else if (line.startsWith(data:)) { data (data ? \n : ) line.substring(5).trim(); } else if (line.startsWith(id:)) { id line.substring(3).trim(); } else if (line.startsWith(retry:)) { retry parseInt(line.substring(6).trim(), 10); } // 遇到空行表示一條消息結束 if (line.trim() ) { if (data) { // 觸發自定義事件 const event new MessageEvent(eventName, { data }); // 這里可以模擬EventSource的行為調用對應的回調函數 console.log(事件[${eventName}]:, data, ID: ${id}); // 例如window.dispatchEvent(new CustomEvent(sse-message, { detail: { eventName, data, id } })); } // 重置臨時變量 eventName message; data ; // id 和 retry 通常在下一條消息開始時重置或根據需求保留 } } } } // 使用 createSSEConnection(http://your-api.com/sse/stream, your-jwt-token).catch(console.error);這種方法雖然代碼量多但優勢巨大你可以控制所有HTTP參數處理CORS并且能更精細地處理流數據。缺點是失去了原生EventSource自動重連的便利性需要自己實現。方法二通過URL參數傳遞Token不推薦如果安全要求不高且Token可以短期暴露可以將認證信息放在查詢字符串中。const token your-jwt-token; const eventSource new EventSource(http://your-api.com/sse/stream?token${token});為什么不推薦URL可能被記錄在瀏覽器歷史、服務器日志、代理日志中存在泄露風險。HTTP頭部是更安全的選擇。關于跨域CORSSSE同樣受同源策略限制。服務端必須設置正確的CORS響應頭例如// Spring Boot中可以在配置類或控制器方法上添加 CrossOrigin(origins http://your-frontend-domain.com, allowedHeaders *, exposedHeaders Content-Type)并且對于攜帶認證信息的請求如使用Fetch API自定義頭部服務端還需要設置allowCredentials前端和Access-Control-Allow-Credentials: true后端。5. 生產環境部署的注意事項與性能調優把SSE用在Demo里很簡單但要穩定地運行在生產環境有幾個坑你必須提前知道。5.1 連接管理與資源釋放這是SSE服務端最常見的坑連接泄漏。每個SSE連接都是一個長期持有的資源如SseEmitter對象、線程或反應式流訂閱。如果客戶端斷開連接關閉瀏覽器標簽、網絡中斷而服務端沒有感知并釋放資源很快就會導致服務器內存耗盡或線程池枯竭。解決方案務必設置超時不要使用SseEmitter(0L)。根據業務容忍度設置一個合理的超時時間例如30分鐘30 * 60 * 1000L。超時后onTimeout回調會觸發進行資源清理。完善回調邏輯如前文代碼所示onCompletion、onTimeout、onError三個回調中必須將對應的SseEmitter從你的管理容器如Map中移除??蛻舳税l送心跳對于超時時間設置較長的連接可以讓客戶端定期比如每50秒通過同一個連接或另一個通道向服務器發送一個“心跳”ping。服務器收到后可以重置該連接的計時器或者至少知道客戶端還活著。對于SseEmitter可以定時向客戶端發送一個注釋行以:開頭的行瀏覽器會忽略它來保持連接活躍。// 服務端定時發送心跳 scheduledExecutor.scheduleAtFixedRate(() - { emitterMap.forEach((clientId, emitter) - { try { emitter.send(SseEmitter.event().comment(heartbeat)); } catch (IOException e) { // 發送失敗連接可能已失效移除 emitterMap.remove(clientId); } }); }, 0, 50, TimeUnit.SECONDS);5.2 負載均衡與粘性會話在微服務架構下你的應用可能部署了多個實例前面有一個負載均衡器如Nginx, HAProxy, 云負載均衡。問題來了客戶端A連接到實例1建立了SSE長連接。當客戶端A想通過另一個HTTP請求比如POST一個操作來觸發實例1向自己推送消息時這個請求可能會被負載均衡器路由到實例2。而實例2上并沒有客戶端A的SseEmitter連接導致推送失敗。解決方案會話粘滯Session Affinity/Sticky Session配置負載均衡器使得來自同一客戶端通?;贑ookie或IP的所有請求都轉發到同一個后端實例。這是最簡單的方案但不符合無狀態服務的理念且在實例重啟時會導致連接中斷。集中式連接管理不使用內存中的Map而是使用一個外部的、共享的消息中間件。所有實例都將收到的客戶端連接信息如clientId和實例標識注冊到Redis或數據庫中。當需要推送時先查找到客戶端連接在哪個實例上然后通過內部RPC如gRPC、HTTP或消息隊列如RabbitMQ、Kafka通知該特定實例進行推送。這是更優雅、可擴展性更強的方案但架構復雜度更高。// 偽代碼集中式管理思路 // 1. 連接建立時 redisTemplate.opsForValue().set(sse:client: clientId, currentInstanceId); // 2. 需要推送時 String targetInstanceId redisTemplate.opsForValue().get(sse:client: clientId); if (targetInstanceId ! null targetInstanceId.equals(currentInstanceId)) { // 本地推送 localEmitter.send(...); } else if (targetInstanceId ! null) { // 通過內部消息通知目標實例推送 messageQueue.sendToInstance(targetInstanceId, new PushCommand(clientId, message)); }5.3 監控與調試SSE連接是長連接在服務器上可以用netstat或ss命令查看大量的ESTABLISHED狀態的連接。你需要監控活躍連接數警惕連接數無限制增長可能是資源泄漏。連接持續時間分布了解業務的正常連接時長。網絡流量SSE會持續產生流量需關注其帶寬消耗。瀏覽器調試在Chrome/Firefox的開發者工具中Network標簽頁里SSE連接會顯示為一個類型為eventsource的請求。點擊它在EventStream選項卡中你可以實時看到服務器推送過來的每一條格式化后的事件包括event、data、id非常直觀。6. 常見問題排查與實戰技巧實錄在實際開發和運維中我踩過不少坑這里總結幾個最典型的問題和解決方法。6.1 連接秒斷或無法建立現象前端EventSource的onerror事件立即觸發readyState很快變為CLOSED。瀏覽器Network里看到請求狀態可能是(canceled)或直接失敗。排查步驟檢查響應頭這是最常見的原因。確保服務器響應的Content-Type是text/event-stream而不是application/json或text/plain。瀏覽器對此要求非常嚴格。檢查代理和網關Nginx/Apache等反向代理默認會對響應進行緩沖buffer。對于SSE流必須禁用緩沖否則代理會等到緩沖區滿或連接關閉才發送數據導致客戶端收不到實時數據。# Nginx 配置示例 location /sse/ { proxy_pass http://backend-server; proxy_set_header Connection ; proxy_http_version 1.1; # 必須使用HTTP/1.1 proxy_buffering off; # 關鍵關閉代理緩沖 proxy_cache off; # 關閉緩存 chunked_transfer_encoding off; # 對于SSE通常也關閉分塊編碼視情況而定 proxy_read_timeout 3600s; # 設置一個很長的讀超時 }檢查CORS如果前端和后端域名不同務必正確配置CORS。錯誤信息可能在瀏覽器控制臺的Console中看到。檢查防火墻和安全組確保服務器的相應端口通常是80或443對客戶端開放。6.2 客戶端收不到數據但連接顯示正?,F象Network里連接狀態碼是200一直處于Pending或Loading狀態但前端始終沒有觸發onmessage事件。排查步驟檢查數據格式用curl或Postman直接請求SSE端點查看原始響應。curl -N http://your-server.com/sse/stream檢查輸出是否符合SSE格式規范data:、event:等字段以兩個換行符\n\n分隔消息。一個常見的錯誤是在輸出數據后沒有輸出額外的換行符。每條SSE消息必須以兩個換行符\n\n或\r\n\r\n結尾。檢查編碼和特殊字符確保服務器輸出是UTF-8編碼并且數據內容中沒有意外包含控制字符或非法字符這些可能會破壞流的解析。服務器端流是否被刷新在某些框架或Servlet容器中需要手動刷新輸出流flush()。在Spring的SseEmitter中send()方法通常會處理刷新但如果你在底層直接操作HttpServletResponse的OutputStream記得在寫入數據后調用outputStream.flush()。6.3 自動重連不工作或循環重連現象連接斷開后客戶端沒有自動重連或者不斷重連失敗形成循環。原因與解決服務端未發送retry指令客戶端默認的重連延遲是幾秒鐘。如果網絡環境差這個時間可能太短。服務端可以在消息中發送retry: 10000來建議客戶端10秒后重連。重連時服務端返回非200狀態碼如果連接斷開是因為服務器錯誤如5xx重連時服務器如果還沒恢復會繼續返回錯誤導致循環??蛻舳薊ventSource在收到非200狀態碼或網絡錯誤時會嘗試重連。你需要確保服務端應用健壯并在故障恢復后能正常處理新連接。Last-Event-ID處理不當客戶端重連時會在請求頭中攜帶上次收到的最后一個消息ID。如果服務端沒有正確處理這個ID比如忽略它總是從頭發送數據在消息頻率很高的場景下客戶端可能會丟失斷開期間的大量消息。服務端邏輯應該能根據Last-Event-ID從某個緩存或數據庫中獲取后續消息。6.4 內存增長與性能優化現象隨著連接數增加服務器內存使用量持續上升。優化方向及時釋放資源如前所述完善連接斷開時的回調清理邏輯??刂葡Ⅲw積和頻率避免發送過大的數據包。對于高頻更新考慮使用增量更新只發送變化的部分或降低推送頻率如從每秒一次改為每兩秒一次。使用二進制數據SSE規范只支持文本。但如果要推送圖片等二進制數據可以將其編碼為Base64字符串。注意這會增加約33%的數據量。對于頻繁的二進制數據推送WebSocket是更好的選擇??紤]使用專門的推送服務當連接數達到萬級甚至更高時基于傳統應用服務器線程模型如Tomcat的SSE實現可能會遇到瓶頸。此時可以考慮使用基于Netty等NIO框架的專門推送中間件或者云服務商提供的推送服務。一個實用的調試技巧在開發階段可以在服務端每條推送的消息里加上一個序列號和時間戳。這樣在前端你可以清晰地看到消息接收是否連續、是否有延遲便于定位是網絡問題還是服務端處理瓶頸。// 服務端 emitter.send(SseEmitter.event() .id(String.valueOf(seqId.incrementAndGet())) .data({\value\: data ,\ts\: System.currentTimeMillis() }) );SSE是一個在特定場景下極其高效和簡潔的工具。它可能沒有WebSocket那么“全能”但正是這種“專注”讓它實現起來更輕量運維起來更省心。下次當你需要實現一個實時數據看板、一個進度通知功能時不妨先問問自己我真的需要雙向通信嗎如果答案是否定的那么SSE很可能就是你正在尋找的那個優雅的解決方案。從簡單的EventSourceAPI開始逐步深入到生產級的優化和問題排查這條路徑上的坑我已經替你踩過不少希望這份指南能讓你走得更加順暢。