流式介面

柯南小海盗發表於2024-08-09

服務端

  1. websocket和event-stream的優缺點

WebSocket和Event-Stream(Server-Sent Events)都是實現實時通訊的技術,但是它們各自有不同的優缺點。

⭐️ WebSocket

  • 優點:
  1. 雙向通訊:WebSocket提供了一個全雙工的通訊通道,客戶端和伺服器可以同時傳送和接收資料。
  2. 實時性:由於WebSocket是持久連線,所以它具有高實時性。
  3. 更少的資料傳輸量:WebSocket在建立連線後,資料傳輸時不需要包含HTTP頭,因此資料傳輸量較小。
  4. websocket可傳輸較為複雜的資料結構,例如json、二進位制位元組等。
  5. websocket針對java、Python等語言支援較好
  6. websocket天然支援跨域問題(據說也有跨域問題,目前無法模擬出來)
  • 缺點:
  1. 雖然大部分現代瀏覽器都支援WebSocket,但是一些老版本的瀏覽器可能不支援。
  2. 協議複雜:WebSocket的協議相對複雜,需要處理連線、斷開連線、心跳等問題。
  • 適用場景
  1. 適合較為複雜的業務場景,需要多次進行通訊,例如:聊天、遊戲等

🌟 Event-Stream (Server-Sent Events):

  • 優點:
  1. 簡單易用:Event-Stream的API相對簡單,易於使用和理解。
  2. 自動重連:如果連線斷開,Event-Stream會自動嘗試重連。
  3. 基於HTTP:Event-Stream基於HTTP協議,因此可以利用現有的HTTP基礎設施,如負載均衡和快取。
  • 缺點:
  1. 單向通訊:Event-Stream只支援伺服器向客戶端傳送資料,不支援客戶端向伺服器傳送資料。
  2. 實時性:由於Event-Stream是基於HTTP的,因此它的實時性可能不如WebSocket。
  3. 資料傳輸量:Event-Stream在每次傳送資料時都需要包含HTTP頭,因此資料傳輸量可能較大。
  4. 資料結構:Event-Stream傳輸僅僅支援字元形式,無法適應較複雜的場景
  5. java流式下發可能出現丟包、站包問題,需要自己實現編解碼來對訊息進一步處理
  • 適用場景
  1. 適合較為簡單的場景,例如:大模型客戶端發一次訊息後,服務端返回結果

event-stream形式

JAVA

  • java實現SSE的方式主要有2種,即透過Spring mvc提供的SseEmitter和Flux。目前我用的比較多的是SseEmitter(原因:本人比較懶,此方式較為簡單)
  • 只需定義一個 SseEmitter物件並輸出即可,透過 SseEmitter.send()傳送訊息(傳送訊息需要非同步執行)
  • SseEmitter建構函式中,超時時間設定為0,否則請求過長,會出現超時情況(預設超時時間和http介面相同,可透過spring配置進行設定)
  • SseEmitter種主要的方法
  1. send:傳送訊息
  2. complete():表示訊息已結束
  3. 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

  1. 引入依賴
pip install requests
  1. 編碼部分
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

編碼部分

  1. sse介面本身不支援get請求,開源的依賴 fetchEventSource 可支援POST介面
  2. openWhenHidden:表示是否監聽視窗的視覺化,即
    • false:視窗切換後,監聽到視窗的變化後,sse介面會重新建立連線併傳送訊息
    • true:視窗切換後,不會監聽視窗變化,sse不會重新連線
  3. vue開發時,需要配置 compress: false,否則不會流式下發
  4. 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>

相關文章