服務端
- websocket和event-stream的優缺點
WebSocket和Event-Stream(Server-Sent Events)都是實現實時通訊的技術,但是它們各自有不同的優缺點。
⭐️ WebSocket
- 優點:
- 雙向通訊:WebSocket提供了一個全雙工的通訊通道,客戶端和伺服器可以同時傳送和接收資料。
- 實時性:由於WebSocket是持久連線,所以它具有高實時性。
- 更少的資料傳輸量:WebSocket在建立連線後,資料傳輸時不需要包含HTTP頭,因此資料傳輸量較小。
- websocket可傳輸較為複雜的資料結構,例如json、二進位制位元組等。
- websocket針對java、Python等語言支援較好
- websocket天然支援跨域問題(據說也有跨域問題,目前無法模擬出來)
- 缺點:
- 雖然大部分現代瀏覽器都支援WebSocket,但是一些老版本的瀏覽器可能不支援。
- 協議複雜:WebSocket的協議相對複雜,需要處理連線、斷開連線、心跳等問題。
- 適用場景
- 適合較為複雜的業務場景,需要多次進行通訊,例如:聊天、遊戲等
🌟 Event-Stream (Server-Sent Events):
- 優點:
- 簡單易用:Event-Stream的API相對簡單,易於使用和理解。
- 自動重連:如果連線斷開,Event-Stream會自動嘗試重連。
- 基於HTTP:Event-Stream基於HTTP協議,因此可以利用現有的HTTP基礎設施,如負載均衡和快取。
- 缺點:
- 單向通訊:Event-Stream只支援伺服器向客戶端傳送資料,不支援客戶端向伺服器傳送資料。
- 實時性:由於Event-Stream是基於HTTP的,因此它的實時性可能不如WebSocket。
- 資料傳輸量:Event-Stream在每次傳送資料時都需要包含HTTP頭,因此資料傳輸量可能較大。
- 資料結構:Event-Stream傳輸僅僅支援字元形式,無法適應較複雜的場景
- java流式下發可能出現丟包、站包問題,需要自己實現編解碼來對訊息進一步處理
- 適用場景
- 適合較為簡單的場景,例如:大模型客戶端發一次訊息後,服務端返回結果
event-stream形式
JAVA
- java實現SSE的方式主要有2種,即透過Spring mvc提供的SseEmitter和Flux。目前我用的比較多的是SseEmitter(原因:本人比較懶,此方式較為簡單)
- 只需定義一個
SseEmitter
物件並輸出即可,透過SseEmitter.send()
傳送訊息(傳送訊息需要非同步執行)- SseEmitter建構函式中,超時時間設定為0,否則請求過長,會出現超時情況(預設超時時間和http介面相同,可透過spring配置進行設定)
- SseEmitter種主要的方法
- send:傳送訊息
- complete():表示訊息已結束
- completeWithError(): 傳送錯誤並結束,多用於異常捕獲中
@GetMapping(value = "sseEmitterTest")
public Object sseEmitterTest(@RequestParam(value = "name") String name) {
SseEmitter sseEmitter = new SseEmitter(0L);
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 50, 60L, TimeUnit.SECONDS, queue);
threadPoolExecutor.execute(() -> {
char[] charArray = name.toCharArray();
for (char charStr : charArray) {
try {
sseEmitter.send(new String(new char[]{charStr}));
Thread.sleep(100);
} catch (IOException | InterruptedException e) {
sseEmitter.completeWithError(e);
}
}
sseEmitter.complete();
});
return sseEmitter;
}
客戶端
JAVA
引入依賴
<dependency>
<groupId>org.asynchttpclient</groupId>
<artifactId>async-http-client</artifactId>
<version>2.12.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>3.5.2</version>
</dependency>
呼叫介面
訊息處理類
public class DefaultMessageHandler implements AsyncHandler<Response> {
@Override
public State onStatusReceived(HttpResponseStatus httpResponseStatus) {
return null;
}
@Override
public State onHeadersReceived(HttpHeaders httpHeaders) {
return null;
}
@Override
public State onBodyPartReceived(HttpResponseBodyPart httpResponseBodyPart) {
// 業務邏輯處理
return State.CONTINUE;
}
@Override
public void onThrowable(Throwable throwable) {
}
@Override
public Response onCompleted() {
return null;
}
}
主方法
@NoArgsConstructor
public class HttpStreamClient {
private AsyncHandler<Response> asyncHandler;
public HttpStreamClient(AsyncHandler<Response> asyncHandler) {
this.asyncHandler = asyncHandler;
}
public void sendMessage(String url, String message) {
// 組裝okhttp請求
AsyncHttpClient client = Dsl.asyncHttpClient();
BoundRequestBuilder requestBuilder = client.preparePost(url);
requestBuilder.setHeader("Accept", "text/event-stream");
requestBuilder.setHeader("Content-Type", "application/json");
requestBuilder.setBody(message);
requestBuilder.execute(asyncHandler);
}
public static void main(String[] args) {
DefaultMessageHandler defaultMessageHandler = new DefaultMessageHandler();
HttpStreamClient httpStreamClient = new HttpStreamClient(defaultMessageHandler);
String message = "Hello WOrld!"
httpStreamClient.sendMessage("API", message);
}
}
Python
- 引入依賴
pip install requests
- 編碼部分
import requests
import json
def invokeStreamLlm():
requestBody = {
name: "Hello World!"
}
response = requests.post(
"API",
json=requestBody,
stream=True,
headers={"Content-Type": "application/json"},
)
if response.status_code != 200:
return
for line in response.iter_lines():
if line and line.strip():
content = json.loads(line[5:])
code = content['code']
if code == 200:
print(content['data'])
elif code == -200:
print('結束標識')
if __name__ == "__main__":
content = invokeStreamLlm()
vue
- 本地開發時,透過vue的正向代理,流式介面無法做到流式下發。資料會在最後一塊全部下發
- 本地想要流式下發解決方法:
- 本地部署一個nginx,透過nginx反向代理介面後,可實現流式下發
- 注:實際專案中,url則是vue配置的api,ip和埠採用代理來代替
引入依賴
npm install @microsoft/fetch-event-source -D
編碼部分
- sse介面本身不支援get請求,開源的依賴 fetchEventSource 可支援POST介面
- openWhenHidden:表示是否監聽視窗的視覺化,即
- false:視窗切換後,監聽到視窗的變化後,sse介面會重新建立連線併傳送訊息
- true:視窗切換後,不會監聽視窗變化,sse不會重新連線
- vue開發時,需要配置
compress: false
,否則不會流式下發- nginx部署時,nginx需要特殊定製,否則會出現無法流式下發的問題
location /api { proxy_set_header Host $http_host; proxy_read_timeout 1200; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://127.0.0.1:8089; client_max_body_size 30m; add_header Cache-Control no-cache; proxy_set_header Connection ''; proxy_http_version 1.1; chunked_transfer_encoding off; proxy_buffering off; proxy_cache off; }
<script>
import { fetchEventSource } from '@microsoft/fetch-event-source';
export default {
data() {
return {
ctrl: new AbortController()
};
},
mounted() {
this.invokeSse();
},
methods: {
invokeSse() {
fetchEventSource('API', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: ['text/event-stream', 'application/json'],
},
body: JSON.stringify({
data: ''
}),
signal: this.ctrl.signal,
openWhenHidden: true,
onopen(event) {
console.log(event);
},
onmessage(msg) {
// 訊息接收
console.log(msg);
},
onclose(e) {
console.log(e);
this.ctrl.abort();
},
onerror(err) {
console.log(err);
this.ctrl.abort();
},
});
}
},
};
</script>