前言
上次迭代已经结束了,这次的迭代(是关于AI答疑的)也开始了,前几天开了一个技术评审,由于我们这个项目是处于重构阶段(拆分那些并发量很高的微服务), 这次的需求没有太多,这次的话分给我的就是AI答疑满意度功能和一个AI答疑对话流式输出的接口(这种的接口很少做,一些api都不是很熟悉),leader说有个类似的接口,叫我模仿一下尝试尝试新的东西。于是我就去全面的学习了一下服务器推送技术,和大家分享分享。
服务器推送技术和客户端主动请求
客户端主动请求包括:
-
轮询(Polling) :
- 描述:客户端定期向服务器发送请求,检查是否有新数据。如果服务器有新数据,就返回给客户端;如果没有,就返回空或者旧数据。
- 优点:实现简单,不需要特殊的服务器支持。
- 缺点:效率低,因为即使没有新数据,客户端也会频繁发送请求,造成不必要的网络流量和服务器负载。
-
长轮询(Long Polling) :
- 描述:客户端发送请求到服务器,服务器保持请求打开,直到有新数据可发送或者超时。
- 优点:比传统轮询更高效,因为只有在有数据时才响应。
- 缺点:仍然有延迟,因为需要等待服务器超时或有数据时才响应。
-
HTTP/2 Server Push:
- 描述:在HTTP/2协议中,服务器可以在客户端请求一个资源时,主动推送相关的其他资源。
- 优点:减少了额外的请求-响应周期,加快了页面加载速度。
- 缺点:需要客户端和服务器都支持HTTP/2协议。
服务器推送技术:
服务器推送(Server Push)技术允许客户端和服务端在有新内容可用时主动向客户端推送更新,而不需要用户主动去查询。服务器推送的优点有两个:
- 用户体验更流畅。用户不需要一直去刷新页面来获取最新内容,系统会在有新的消息出现时自动推送给客户端。
- 更高效。服务器只在有真正有用的内容时才主动推送,节省了大量不必要的客户端请求。
服务器推送技术:
-
SSE(Server-Sent Events) :
- 描述:一种允许服务器向客户端推送事件的技术。客户端通过创建一个到服务器的单向连接来监听事件。
- 优点:实现相对简单,只需要客户端监听一个事件源,适合单向通信(服务器到客户端)。
- 缺点:不支持双向通信,如果需要客户端向服务器发送数据,还需要额外的轮询或WebSocket连接。
-
WebSocket:
- 描述:一种在单个TCP连接上进行全双工通信的协议。一旦WebSocket连接建立,服务器和客户端就可以通过这个连接发送数据,无需每次建立新的连接。
- 优点:支持全双工通信,延迟低,适用于需要实时双向通信的场景。
- 缺点:实现相对复杂,需要服务器和客户端都支持WebSocket协议。
SSE:
- 单向通信:SSE 是一种单向通信协议,服务器可以向客户端发送数据,但客户端不能向服务器发送数据。
- 简单易用:SSE 的实现相对简单,只需要在服务器端使用 SseEmitter 或类似机制,客户端使用标准的EventSource API 即可。
- 文本数据:SSE 只支持文本数据,不支持二进制数据。
- 自动重连:SSE 支持自动重连,当连接断开时,客户端会自动尝试重新连接。
- 浏览器支持:现代浏览器普遍支持 SSE,无需额外的库或插件。
对比一下我们熟知的Websocket Websocket:
- 双向通信:WebSocket 是一种双向通信协议,服务器和客户端都可以互相发送数据。
- 复杂性:WebSocket 的实现相对复杂,需要处理连接管理、心跳检测、错误处理等。
- 二进制数据:WebSocket 支持二进制数据传输,适用于需要传输大量数据或复杂数据结构的场景。
- 手动重连:WebSocket 需要手动实现重连逻辑,客户端需要在连接断开时主动重新建立连接。
- 广泛支持:WebSocket 在现代浏览器和多种编程语言中都有良好的支持。
下面是通过SSE实现AI流式输出的举例用法
// 1. Controller 接口代码如下,为了防止串流以及后续支持客户端主动停止推流,每次请求携带唯一的用户id。
@PostMapping("/chat")
public SseEmitter chat(@Validated @RequestBody ChatRequest request){
return Facade.chat(request,getUserId());
}
//2.接下来是Facade层,使用CompletableFuture异步调用AI
public SseEmitter chat(ChatRequest request, Long userId){
//创建连接,并设置超时时间
SseEmitter sseEmitter =new SseEmitter(3600000L);
//注册完成回调,调用sseEmitter.complete()时触发
sseEmitter.onCompletion(()->
log.info(request,userId);
);
//注册超时回调,超时后触发
sseEmitter.onTimeOut(()->
log.info(request,userId);
);
// 注册异常回调,调用 emitter.completeWithError() 触发
sseEmitter.onError(()->
log.info(request,userId);
);
// 使用 CompletableFuture 异步调用 AI 获取结果
CompletableFuture.runAsync(() -> manager.chat(request.getAsk(),String.valueOf(userId), sseEmitter), taskExecutor);
return sseEmitter;
}
//3.接下来是Manager层
public void chat(String ask,String UserId,SseEmitter sseEmitter){
//使用okhttpClient创建事件源工厂
EventSource.Factory factory=EventSource.createFactory(okHttpClient);
//构建请求体(根据调用的AI的请求所需的请求体构建请求体)
...
String json=JSONObject.toJSONString(request);
...
//构建HTTP请求(根据你需要调用的AI构建对应的HTTP请求)
RequestBody requestBody=RequestBody.companion.create(json,MediaType.parse("applicationn/json;charset=utf-8"));//创建请求体
//构建HTTP请求
Request requestBuilder=new Request.Builder.url(...).post(requestBody).builder();
//创建自定义事件监听器
ZdyEventSourceListener eventSourceListener=new ZdyEventSourceListener(sseEmitter);
//创建并启动事件源,并用eventSourceListener处理返回的事件
factory.newEventSource(requestBuilder,eventSourceListener);
}
//下面是自定义监听器
@Slf4j
public class ZdyEventSourceListener extends EventSourceListener {
private final SseEmitter sseEmitter;
private final Map<String, String> sessionData = new ConcurrenHashMap<>();
public ZdyEventSourceListener(SseEmitter sseEmitter) {
this.sseEmitter = sseEmitter;
}
//连接打开
@Override
public void onOpen(@NotNull EventSource eventSource, @NotNull Response response) {
log.debug("ZdyEventSourceListener::onOpen");
super.onOpen(eventSource, response);
}
//接收到内容
@Override
public void onEvent(@NotNull EventSource eventSource, @Nullable String id, @Nullable String type, @NotNull String data) {
log.debug("ZdyEventSourceListener::onEvent data = {}", data);
// 解析数据
Map<String, Object> eventData = JSONObject.parseObject(data, Map.class);
String sessionId = (String) eventData.get("sessionId");
String message = (String) eventData.get("message");
// 更新会话数据
if (Objects.nonNull(sessionId) && Objects.nonNull(message)) {
sessionData.compute(sessionId, (key, oldValue) -> {
if (oldValue == null) {
return message;
} else {
return oldValue + message;
}
});
}
// 发送数据
this.send(data);
super.onEvent(eventSource, id, type, data);
}
//连接关闭
@Override
public void onClosed(@NotNull EventSource eventSource) {
log.debug("ZdyEventSourceListener::onClosed");
// 处理会话数据
sessionData.forEach((sessionId, data) -> {
log.debug("Session {} closed with data: {}", sessionId, data);
// 可以在这里进行进一步处理,例如保存到数据库
});
// 完成 sseEmitter
sseEmitter.complete();
super.onClosed(eventSource);
}
// 接收失败
@Override
public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable t, @Nullable Response response) {
log.error("ZdyEventSourceListener::onFailure eventSource = {}, response = {}", eventSource, response, t);
// 发送错误消息
this.send(JSONObject.toJSONString(Map.of("error", "SSE connection failed")));
sseEmitter.complete();
super.onFailure(eventSource, t, response);
}
//发送消息
private void send(String data) {
try {
sseEmitter.send(data, org.springframework.http.MediaType.APPLICATION_JSON);
} catch (IOException e) {
log.error("ZdyEventSourceListener::send error, errorMsg = {}", e.getMessage(), e);
sseEmitter.complete();
}
}
}
水平有限以上文章如果有小错误还请见谅
最近在公司代码中看到了一些线程池的使用,让我想起了TL、ITL、TTL的用法,后续我会在总结总结分享给大家,多谢大家捧场!!!