流式接口
服务端
- 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>
本文来自博客园,作者:柯南小海盗,转载请注明原文链接:https://www.cnblogs.com/knxhd/p/18350402