流式响应-AI问答

1.WebClient之Spring WebFlux

       Flux是Reactor项目中的一个核心概念,它代表了一个可响应式、可取消的、元素序列。Flux是异步的,这意味着它不会阻塞当前线程来处理数据。相反,它允许数据项在它们准备好时一个接一个地处理,通常是通过一个非阻塞的方式。
在Spring WebFlux中,Flux通常用于处理HTTP请求和响应,特别是在需要处理流式数据时。例如,一个Flux<String>可以表示一个服务器发送事件(SSE)流,或者一个客户端接收到的流式数据流。
      Flux的特点包括:
非阻塞:Flux不会阻塞调用线程,它允许事件在准备好时按顺序处理。
背压:Flux支持背压,这意味着它可以根据下游的处理能力动态调整数据流的大小。
可取消:Flux是可取消的,这意味着可以在任何时候取消它,以释放资源。
元素序列:Flux可以包含0个或多个元素,并且每个元素都可以是一个数据项。
在处理Flux时,通常会使用subscribe方法来注册观察者,观察者可以处理Flux发出的元素。Flux还提供了许多其他操作符,如filter、map、flatMap等,这些操作符可以链式调用,以便在处理数据时应用各种转换。

1.1依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.32</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

1.2配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

    @Autowired
    private WebClient webClient;

    @Bean
    public WebClient webClient() {
        return WebClient.builder()
                .baseUrl("http://third-party-api") // 设置基础URL
                .build();
    }

    Public Flux<String> postStreamingData(RequestVO request) {
        String requestBody = FastJsonUtil.toJson(request);
        return webClient.post()
                   .uri(url)
                   .contentType(MediaType.APPLICATION_JSON)
                   .bodyValue(requestBody)
                   .retrieve()
                   .bodyToFlux(String.class);
}

1.3.Flux数据处理,Flux属性(常用)

 1.map对数据进行格式转化处理等;

 2.filter对数据进行过滤,不符合的数据过滤掉;

 3.doOnNext获取每一个流出的数据,对一个流出的数据进行处理,例子中用于拼接,当然用subscribe也可以;

 4.subscribe方法来监听Flux中的每个字符串。在subscribe方法中,我们提供了三个回调:onNext、onError和onComplete。onNext回调用于处理每个字符串,你可以在这里进行拼接操作。onError回调用于处理错误情况,而onComplete回调用于在所有数据项到达后执行

 5.doOnCancel: 这个操作符会在流被取消时执行,例如当客户端断开连接时。取消通常发生在下游取消了对Flux的订阅,或者使用了cancel()方法。doOnCancel保证只会在取消发生时被调用,并且不会被调用多次。
 6.doFinally: 这个操作符会在流完成、取消或者发生错误时执行。它用于执行资源清理或者任何需要在流生命周期结束时执行的代码。doFinally保证在流的任何终止状态下至少被调用一次,无论是正常完成、被取消还是发生错误。
在大多数情况下,如果你使用了doOnCancel和doFinally,doFinally会在doOnCancel之后被调用,因为doFinally是在流完全终止之后执行的。但是,这并不是一个绝对的保证,因为doFinally可能会在doOnCancel之前被调用,尤其是在某些错误情况下。

Public Flux<String> completion(RequestVo request) {
   StringBuilder sb = new StringBuilder();
   return webClientConfig.postStreamingData(request)
           .map(this::chat)
           .doOnNext(result -> {
                sb.append(result);
           })
           .doFinally(result -> {
               save(sb.toString());
           });
}

public String chat(String responseLine) {

  if (!Object.equals(responseLine, "[DONE]") {
      returnresponseLine;
  }
}

public void save(String result) {
  //保存到数据库
}

1.4.关闭资源

当你完成对WebClient的使用后,你可以简单地让它被垃圾回收器回收。由于WebClient是基于响应式编程的,它不会保持任何持久的资源,除非你在代码中显式地持有资源(例如,通过订阅了一个Flux或Mono)。如果你需要关闭WebClient来释放资源,你可以调用它的close方法。这通常不是必需的,因为WebClient实例本身并不持有

1.5Mono和Flux

Mono和Flux都是Reactor项目中的核心概念,用于表示响应式编程中的数据流,但它们在表示数据流的能力和用法上有所不同。
Mono
Mono代表了一个可能包含0个或1个元素的序列。
Mono是不可变的,一旦创建,就不能改变它包含的元素。
Mono通常用于表示一个单独的数据项,例如一个HTTP响应体。
Mono可以通过just、empty、error等方法创建。
Mono不支持背压(backpressure),它总是假设有足够的处理能力来处理数据。
Flux
Flux代表了一个可能包含0个、1个或多个元素的序列。
Flux是可变的,可以在其生命周期内添加、删除或修改元素。
Flux通常用于表示一个数据流,例如一系列HTTP响应体或事件。
Flux可以通过just、fromIterable、range等方法创建。
Flux支持背压,这意味着它可以根据下游的处理能力动态调整数据流的大小。

1.6.Flux转SseEmitter输出

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
@RequestMapping("/api/events")
public class EventController {

    private final WebClient webClient = WebClient.builder().build();
    private final ExecutorService nonBlockingService = Executors.newCachedThreadPool();

    @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter handleEvents() {
        SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);

        nonBlockingService.execute(() -> {
            // 调用第三方流式接口
            Flux<String> flux = webClient.get()
                    .uri("http://third-party-api/stream")
                    .retrieve()
                    .bodyToFlux(String.class);

            // 订阅流式数据并使用SseEmitter发送
            flux.subscribe(
                    data -> {
                        try {
                            emitter.send(data);
                        } catch (IOException e) {
                            emitter.completeWithError(e);
                        }
                    },
                    error -> {
                        emitter.completeWithError(error);
                    },
                    () -> {
                        emitter.complete();
                    }
            );
        });

        return emitter;
    }
}

2.SseEmitter

2.1.对外提供流式接口

2.1.1.Controller

@RestController
@RequestMapping("/sse/websocket")
public class SSEController {


    /** 正式使用注意并发量控制 */
    @RequestMapping(value = "/sub", method = RequestMethod.GET, produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
    public SseEmitter subscribe(@RequestParam("questionId") String questionId) {
        // 简单异步发消息 ====
        new Thread(() -> {
            try {
                Thread.sleep(1000);
                for (int i = 0; i < 10; i++) {
                    Thread.sleep(500);
                    SSEUtils.pubMsg(questionId, questionId + " - kingtao come " + i);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 消息发送完关闭订阅
                SSEUtils.closeSub(questionId);
            }
        }).start();
        // =================

        return SSEUtils.addSub(questionId);
    }
}

2.1.2.SSEUtils

package com.example.demo.util.sse;

import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class SSEUtils {
    // timeout
    private static Long DEFAULT_TIME_OUT = 2*60*1000L;
    // 订阅表
    private static Map<String, SseEmitter> subscribeMap = new ConcurrentHashMap<>();

    /** 添加订阅 */
    public static SseEmitter addSub(String questionId) {
        if (null == questionId || "".equals(questionId)) {
            return null;
        }

        SseEmitter emitter = subscribeMap.get(questionId);
        if (null == emitter) {
            emitter = new SseEmitter(DEFAULT_TIME_OUT);
            subscribeMap.put(questionId, emitter);
        }

        return emitter;
    }

    /** 发消息 */
    public static void pubMsg(String questionId, String msg) {
        SseEmitter emitter = subscribeMap.get(questionId);
        if (null != emitter) {
            try {
                // 更规范的消息结构看源码
                emitter.send(SseEmitter.event().data(msg));
            } catch (Exception e) {
                // e.printStackTrace();
            }
        }
    }

    // 关闭订阅
    public static void closeSub(String questionId) {
        SseEmitter emitter = subscribeMap.get(questionId);
        if (null != emitter) {
            try {
                emitter.complete();
                subscribeMap.remove(questionId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

2.1.3.前端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>sse</title>
</head>
<body>
<div>
    <label>问题id</label>
    <input type="text" id="questionId">
    <button onclick="subscribe()">订阅</button>
    <hr>
    <label>F12-console控制台查看消息</label>
</div>
 
<script>
    function subscribe() {
        let questionId = document.getElementById('questionId').value;
        let url = 'http://localhost:8089/sse/websocket/sub?questionId=' + questionId;
        
        let eventSource = new EventSource(url);
        eventSource.onmessage = function (e) {
            console.log(e.data);
        };

        eventSource.onopen = function (e) {
            // todo
        };

        eventSource.onerror = function (e) {
            // todo
            eventSource.close()
        };
    }
</script>
</body>
</html>

2.2.调用第三方流式接口

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
public class SseController {

    private final RestTemplate restTemplate = new RestTemplate();
    private final ExecutorService executorService = Executors.newSingleThreadExecutor();

    @GetMapping(value = "/sse/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter stream() {
        SseEmitter emitter = new SseEmitter();

        executorService.execute(() -> {
            try {
                String thirdPartyUrl = "http://third-party-api/stream"; // 第三方流式接口URL

                // 调用第三方流式接口
                InputStream inputStream = restTemplate.getForObject(thirdPartyUrl, InputStream.class);
                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

                String line;
                while ((line = reader.readLine()) != null && !emitter.isComplete()) {
                    emitter.send(line); // 发送每行数据作为SSE事件
                }

                emitter.complete(); // 完成SSE事件流
            } catch (IOException e) {
                emitter.completeWithError(e); // 发生错误时完成SSE事件流
            }
        });

        return emitter;
    }
}
package com.example.demo.util.sse;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class SSEClient {
    // timeout
    private static Integer DEFAULT_TIME_OUT = 2*60*1000;

    /** 获取SSE输入流 */
    public static InputStream getSseInputStream(String urlPath, int timeoutMill) throws IOException {
        HttpURLConnection urlConnection = getHttpURLConnection(urlPath, timeoutMill);
        InputStream inputStream = urlConnection.getInputStream();
        return new BufferedInputStream(inputStream);
    }

    /** 读流数据 */
    public static void readStream(InputStream is, MsgHandler msgHandler) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        try {
            String line = "";
            while ((line = reader.readLine()) != null) {
                msgHandler.handleMsg(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 服务器端主动关闭时,客户端手动关闭
            reader.close();
            is.close();
        }
    }

    private static HttpURLConnection getHttpURLConnection(String urlPath, int timeoutMill) throws IOException {
        URL url = new URL(urlPath);
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
        urlConnection.setDoOutput(true);
        urlConnection.setDoInput(true);
        urlConnection.setUseCaches(false);
        urlConnection.setRequestMethod("GET");
        urlConnection.setRequestProperty("Connection", "Keep-Alive");
        urlConnection.setRequestProperty("Charset", "UTF-8");
        // text/plain模式
        urlConnection.setRequestProperty("Content-Type", "text/plain; charset=UTF-8");
        // 读过期时间
        urlConnection.setReadTimeout(timeoutMill);
        return urlConnection;
    }

    /** 消息处理接口 */
    interface MsgHandler {
        void handleMsg(String line);
    }

    public static void main(String[] args) throws Exception {
        // 单订阅
        String urlPath = "http://localhost:8089/sse/websocket/sub?questionId=kingtao3";
        InputStream inputStream = getSseInputStream(urlPath, DEFAULT_TIME_OUT);
        readStream(inputStream, new MsgHandler() {
            @Override
            public void handleMsg(String line) {
                if (line != null && line.contains("data:")) {
                    // 注意按约定的消息协议解析消息
                    String msg = line.split(":")[1];
                    System.out.println(msg);
                }
            }
        });

        // 并发订阅
        // ExecutorService executorService = Executors.newFixedThreadPool(100);
        // for (int i = 0; i < 100; i++) {
        //     int finalI = i;
        //     executorService.submit(() -> {
        //         try {
        //             String urlPath = "http://localhost:8089/sse/websocket/subscribe?questionId=kingtao" + finalI;
        //             InputStream inputStream = getSseInputStream(urlPath, DEFAULT_TIME_OUT);
        //             readStream(inputStream, new MsgHandler() {
        //                 @Override
        //                 public void handleMsg(String line) {
        //                     if (line != null && line.contains("data:")) {
        //                         // 注意按约定的消息协议解析消息
        //                         String msg = line.split(":")[1];
        //                         System.out.println(msg);
        //                     }
        //                 }
        //             });
        //         } catch (Exception e) {
        //             e.printStackTrace();
        //         }
        //     });
        // }
        // executorService.shutdown();
    }
}

  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值