SeeEmitter和Flux使用Post方式推送和接收消息

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();

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值