傳統的http1.0請求開發,已經滿足了我們日常的web開發。
一般請求就像下圖這樣子,客服端發起一個請求(觸發),服務端做出一個響應(動作):
有時會有諸如實時重新整理,實時顯示的場景,我們往往是客戶端定時發起請求,不斷的嘗試獲取最新的資料。
但是每次請求都會建立並釋放一個新的連線,這樣對於需要頻繁請求的場景,效能損耗太大,此外對於實時性響應的場景也很難評估輪詢週期。輪詢的週期短,很多查詢結果其實並沒有變化,增加了成本開銷。輪詢週期長,又不能實時的展示資料,週期值變成了一個經驗值,而且不同場景都需要不斷的調整。這屬實不夠友好。
於是http1.1協議對此進行了擴充套件,允許長連線的存在。今天要介紹的SSE協議,就屬於http1.1下的新協議。
SSE全稱為 Sever-Sent Event
指伺服器端事件傳送。當客戶端請求成功後,服務端會依次將事件(其實就是響應資訊),分多次傳送到客戶端。客戶端只要接收事件(響應資訊),做出相應的處理即可。
就像下圖的樣子:
比如K線增長圖,實時熱力圖,各種增長曲線等等,都可以實時的,由後端主動將事件推送到前端,不再需要前端每次建立一個新的連線來請求。這種方式也稱之為長連線。
除了SSE,像websocket 、TCP等都屬於長連線的型別。依次連線可以多次互動。
SSE其實最初並不受重視,甚至很多人都不知道這個協議。如果是簡單一點的話,通常直接多輪詢幾遍就解決問題了,如果是複雜一點的話,直接就使用websocket這樣的重協議來處理了,功能也相對來說比較強大。但是自從互動大模型問世以後,大模型的流式對話往往能更高效的輸出,這種流式輸出的使用者體驗也更好。這種主要是側重大模型響應的互動模式,(防盜連線:本文首發自http://www.cnblogs.com/jilodream/ )反而使得SSE的優勢又體現出來了。
下面我們看下如何在springboot中使用sse來開發:
由於springboot的封裝,我們使用SSE開發變得異常簡單,
核心思路是:
建立一個 SseEmitter 物件,返回給前端
這個SseEmitter類似於一個socket,我們只管向裡邊塞資料即可,
而前端在收到SseEmitter物件後,則只管從sseEmitter中取資料即可。(注意此處一般採用註冊響應方式)
後端程式碼如下:
pom檔案新增依賴:
1 <dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-web</artifactId> 4 </dependency>
controller類:
1 package com.example.demo.learnsse; 2 3 import lombok.extern.slf4j.Slf4j; 4 import org.springframework.http.MediaType; 5 import org.springframework.web.bind.annotation.CrossOrigin; 6 import org.springframework.web.bind.annotation.GetMapping; 7 import org.springframework.web.bind.annotation.RequestParam; 8 import org.springframework.web.bind.annotation.RestController; 9 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; 10 11 import java.io.IOException; 12 import java.util.concurrent.TimeUnit; 13 14 /** 15 * @discription 16 */ 17 @Slf4j 18 @RestController 19 @CrossOrigin(origins = "*") 20 public class SseController { 21 22 23 @GetMapping(value = "/learn/sseChat" , produces = {MediaType.TEXT_EVENT_STREAM_VALUE}) 24 public SseEmitter chat(@RequestParam String name) throws IOException { 25 SseEmitter sseEmitter = new SseEmitter(360000L); 26 sseEmitter.onCompletion(() -> log.warn("sse complete!!!" + Thread.currentThread().getName())); 27 sseEmitter.onError(throwable -> { 28 log.warn("sse error " + Thread.currentThread().getName(), throwable); 29 }); 30 sseEmitter.send("start"); 31 Runnable r = () -> { 32 int i = 1; 33 try { 34 while (i <= 10) { 35 sseEmitter.send(Thread.currentThread().getName()+": the next index:" + i); 36 log.warn(Thread.currentThread().getName() + ":" + i); 37 i++; 38 TimeUnit.SECONDS.sleep(3); 39 } 40 sseEmitter.complete(); 41 } catch (Exception e) { 42 log.warn("catch a ex", e); 43 sseEmitter.completeWithError(e); 44 } 45 }; 46 Thread t = new Thread(r); 47 t.start(); 48 log.warn("start return sse"); 49 return sseEmitter; 50 } 51 }
我們可以不寫前端,直接用瀏覽器或者命令列訪問,
瀏覽器效果如下:
真實效果是一行行輸出的
data:start data:Thread-2: the next index:1 data:Thread-2: the next index:2 data:Thread-2: the next index:3 data:Thread-2: the next index:4 data:Thread-2: the next index:5 data:Thread-2: the next index:6 data:Thread-2: the next index:7 data:Thread-2: the next index:8 data:Thread-2: the next index:9 data:Thread-2: the next index:10
日誌輸出如下:
2024-12-02 11:06:36.267 WARN 2032 --- [nio-8081-exec-4] com.example.demo.learnsse.SseController : sse complete!!!http-nio-8081-exec-4 2024-12-02 11:06:38.440 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:2 2024-12-02 11:06:41.442 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:3 2024-12-02 11:06:44.450 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:4 2024-12-02 11:06:47.458 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:5 2024-12-02 11:06:50.468 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:6 2024-12-02 11:06:53.471 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:7 2024-12-02 11:06:56.475 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:8 2024-12-02 11:06:59.483 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:9 2024-12-02 11:07:02.495 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:10 2024-12-02 11:07:05.508 WARN 2032 --- [nio-8081-exec-5] com.example.demo.learnsse.SseController : sse complete!!!http-nio-8081-exec-5
這樣一個簡單的單次連線,伺服器多次推送的示例就寫完了。
當然你也可以寫一個簡短的前端程式碼,檢視效果,注意此時涉及到跨域了,因此我們的java程式碼要使用註解@CrossOrigin(origins = "*") 來解決跨域,請看controller程式碼中紅色字型
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>SSE Example</title> 5 </head> 6 <body> 7 <div id="events"></div> 8 <script> 9 const eventSource = new EventSource('http://127.0.0.1:8081/learn/sseChat?name=xx'); 10 11 eventSource.onmessage = function(event) { 12 const newElement = document.createElement("div"); 13 newElement.textContent = "New message: " + event.data; 14 document.getElementById("events").appendChild(newElement); 15 }; 16 17 eventSource.onerror = function(error) { 18 console.error("Error:", error); 19 const newElement = document.createElement("div"); 20 newElement.textContent = "error message: " + error; 21 document.getElementById("events").appendChild(newElement); 22 eventSource.close(); 23 }; 24 25 eventSource.onclose = function(event) { 26 const newElement = document.createElement("div"); 27 newElement.textContent = "close message: " + event.data; 28 document.getElementById("events").appendChild(newElement); 29 eventSource.close(); 30 }; 31 </script> 32 </body> 33 </html>
我們在建立好SSE示例時,一般會設定以下幾個回撥方法:
onCompletion(Runnable callback):當非同步請求完成時,我們會呼叫此方法註冊的回撥函式。
onError(Consumer<Throwable> callback) 當非同步處理期間發生錯誤時,會呼叫該方法設定的回撥函式
服務端發現任務結束時,主動知會客戶端關閉連線:
complete():表示已經完成推送,通知客戶端不再有新的事件傳送。
completeWithError(Throwable ex) 表示由於發生了某個異常而結束推送。springmvc將透過異常處理機制傳遞該異常。
一般在對接大模型時,(防盜連線:本文首發自http://www.cnblogs.com/jilodream/ )我們除了完成SSE相關的註冊,還會設定與大模型的連線,
一般的思路是這樣的:
1、當前端傳送請求提問來後端時,
2、我們首先建立一個SseEmitter,作為未來傳送的套接字,
3、接著啟動一個http連線,來請求大模型,
4、此時我們會使用Reactor-Mono之類的響應式程式設計框架,來回撥處理大模型推送回來的資料。(其中Reactor部分的程式碼實現,由於篇幅有限,我會在後邊的文章中講解)
5、在Mono的每次回撥到大模型推送回來的資料時,我們透過SseEmitter傳送給前端
6、將第二步建立好的SseEmitter,返回給前端。
注意3/4/5步都是作為非同步回撥註冊到mono中的。整體的結構圖如下: