SeeEmitter和Flux使用Post方式推送和接收消息
背景
项目需求中,需要向公司的Chat服务发送提问请求,然后接收Chat服务返回的数据,把数据时时推送给前端。
在项目中,请求Chat服务和响应前端Context-type都是:text/event-stream。 在请求Chat服务时,使用Flux。推送数据给前端使用SseEmitter,前端使用@microsoft/fetch-event-source组件。
@microsoft/fetch-event-source:
JS原生的EventSourceAPI 不支持发送 POST 请求,它仅支持通过 GET 请求从服务器接收连续的消息流。需求是前端要求使用POST请求,还需携带数据。
Flux使用
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2、Java
Flux.from()是异步操作
public void receiveSSE() {
// SSE服务器端地址
WebClient webClient = WebClient.create("你的SSE服务器地址");
Flux<ServerSentEvent> sseStream = webClient.post()
// 请求头信息
.header(HttpHeaders.AUTHORIZATION, "你的SSE服务器TOKEN")
.bodyValue("你的请求头体")
.retrieve()
.bodyToFlux(ServerSentEvent.class);
// 接收event-stream数据(***这里异步的***)
Flux.from(sseStream)
.doOnNext(sseEvent -> {
// 接收到数据(数据是什么类型取决于SSE服务端,我这里是LinkedHashMap)
LinkedHashMap<String, Object> data = (LinkedHashMap<String, Object>) sseEvent.data();
// TODO ...对数据处理
})
.doOnError(error -> {
log.error("连接断开: {}", error.getMessage());
})
.doOnComplete(() -> {
log.info("完成");
}).subscribe();
}
SseEmitter使用
如果只需要使用SseEmitter,必须保证每个请求返回的SeeEmitter对象都是唯一的。可以通过Map<id, SseEmitter>来维护每一个对象,前端发送请求的时候携带id,带区分SseEmitter对象。
同时还需要再新增一个接口,给前端主动关闭SseEmitter(sseEmitter.complete()),通过前端携带的id到Map中拿到对应的对象将其关闭。
1、Java
// 维护SseEmitter对象
Map<String, SseEmitter> map = new HashMap<String, SseEmitter>();
@PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter sendSSE() {
// 超时时间300s
SseEmitter sseEmitter = new SseEmitter(300_000L);
// 必须使用新线程去做推送的功能,需要先返回SseEmitter对象给前端
new Thread(() -> {
try {
sseEmitter.send(SseEmitter.event()
.reconnectTime(5000) // 与前端断开连接后5s后尝试重连
.id(UUID.randomUUID().toString())
.name("name")
.data("你的数据"));
} catch (IOException e) {
log.error("SEE推送消息失败, 失败消息: {}", data, e);
sseEmitter.completeWithError(e);
}
}).start();
// id可以通过前段生成传递过来,也可以在后端生成
String sseId = requestBody.getSseId();
// 将当前请求SseEmitter对象缓存起来
map.put(sseId, sseEmitter);
// 先返回SseEmitter对象给前端
return sseEmitter;
}
@GetMapping("/close/{id}")
public String closeSseEmitter(@PathVariable String id) {
SseEmitter sseEmitter = map.get(id);
sseEmitter.complete();
return null;
}
2、前端处理
前端使用@microsoft/fetch-event-source组件,这里不提供例子。
Flux和SseEmitter综合案例
结合背景的需求,使用Flux接收流数据,同时,将不断接收到的数据时时推送给前端。
前端使用@microsoft/fetch-event-source组件,这里不提供例子。
1、Controller接口
注意:
1、这里获取请求体内容不能使用@RequestBody。
2、Controller中必须先返回SseEmitter对象,因为Flux.from()是异步的,所以会先将SseEmitter对象返回给Controller,Controller再将返回给前端,
@Autowired
private SSEHelper sseHelper;
@PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter codeByTsl(HttpServletResponse response, HttpServletRequest request) throws IOException {
// 获得请求体(这里requestBody占时没用,如果需要使用就用这种方式获取)
String requestBody = getRequestBody(request);
// 返回是SseEmitter对象
return sseHelper.receiveSSE();
}
/**
* 获得请求体信息
*/
private static String getRequestBody(HttpServletRequest request) throws IOException {
StringBuilder requestBody = new StringBuilder();
BufferedReader reader = request.getReader();
String line;
while ((line = reader.readLine()) != null) {
requestBody.append(line);
}
return requestBody.toString();
}
2、接收Chat服务端数据同时推送给前端
@Service
@Slf4j
public class SSEHelper {
// 发送数据
public void sendSSE(SseEmitter sseEmitter, LinkedHashMap<String, Object> data) {
try {
log.info("推送消息: {}", data);
sseEmitter.send(SseEmitter.event()
.reconnectTime(5000)
.id(data.get("sseId").toString())
.name(data.get("event").toString())
.data(data));
} catch (IOException e) {
log.error("SEE推送消息失败, 失败消息: {}", data, e);
sseEmitter.completeWithError(e);
}
}
// 接收数据
public SseEmitter receiveSSE() {
SseEmitter sseEmitter = new SseEmitter(300_000L);
// SSE服务器端地址
WebClient webClient = WebClient.create("SSE服务器地址");
Flux<ServerSentEvent> sseStream = webClient.post()
// 请求头信息
.header(HttpHeaders.AUTHORIZATION, "SSE服务器TOKEN")
.bodyValue("请求头体")
.retrieve()
.bodyToFlux(ServerSentEvent.class);
// 这里Flux.from()是异步的,该方法会先把SseEmitter对象返回出去
Flux.from(sseStream)
.doOnNext(sseEvent -> {
// 具体什么类型需要看你SSE服务返回什么
LinkedHashMap<String, Object> data = (LinkedHashMap<String, Object>) sseEvent.data();
// 推送消息给客户端
sendSSE(sseEmitter, data);
})
.doOnError(error -> {
log.error("连接断开: {}", error.getMessage());
sseEmitter.completeWithError(error);
})
.doOnComplete(() -> {
log.info("完成");
sseEmitter.complete();
}).subscribe();
// Flux.form()是异步的,SseEmitter对象是先返回给前端
return sseEmitter;
}
}
3、使用Postman测试
使用Postman发送请求,就会一直接收到我们推送的消息。
消息左边的end和new_message 是 sseEmitter.send()中的name();