流式接口

服务端

  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>
posted @ 2024-08-09 11:29  柯南小海盗  阅读(37)  评论(0编辑  收藏  举报