Soul网关源码阅读(七)—— http请求转发流程分析

16 篇文章 1 订阅
本文详细剖析了Soul网关如何通过SoulConfiguration加载插件到Spring WebFlux,追踪了从请求到WebClientPlugin处理再到WebClientResponsePlugin响应的全过程,揭示了插件调用顺序和关键组件的工作原理。
摘要由CSDN通过智能技术生成

概要

上一篇我们介绍了如何使用WebFlux快速的搭建一个请求转发的demo,了解一些基本的概念。这一篇我们来分析soul网关的设计思路,了解soul网关是如何做到将多个插件加载到webflux并进行请求转发的。我们以最简单的divide插件来进行http请求的分析。

我们从以下流程分析整个调用链。

  • 一切的开始——SoulConfiguration

  • 请求的核心处理入口——SoulWebHandler

  • Http请求插件——WebClientPlugin

  • Http响应处理插件——WebClientResponsePlugin

基础知识

开始之前需要了解,SpringWebFlux的工作原理,不然会很懵逼。

参考:

Soul网关源码阅读(六)—— Soul网关之WebFlux

Spring WebFlux 的设计及工作原理剖析

Soul网关http转发流程分析

SoulConfiguration

以上我们分析了如何将divide插件配置到我们的spring IOC容器,那么我们的soul网关又是如何加载他们到我们的请求处理链中的呢?

我们启动bootstrap服务,控制台打印了以下日志:
在这里插入图片描述

可以看到通过SoulConfiguration配置了类加载了很多不同的插件到Spring服务。
我们的分析就从SoulConfiguration开始,查看以下代码。

 /**
     * Init SoulWebHandler.
     *
     * @param plugins this plugins is All impl SoulPlugin.
     * @return {@linkplain SoulWebHandler}
     */
    @Bean("webHandler")
    public SoulWebHandler soulWebHandler(final ObjectProvider<List<SoulPlugin>> plugins) {
        List<SoulPlugin> pluginList = plugins.getIfAvailable(Collections::emptyList);
        final List<SoulPlugin> soulPlugins = pluginList.stream()
                .sorted(Comparator.comparingInt(SoulPlugin::getOrder)).collect(Collectors.toList());
        soulPlugins.forEach(soulPlugin -> log.info("load plugin:[{}] [{}]", soulPlugin.named(), soulPlugin.getClass().getName()));
        return new SoulWebHandler(soulPlugins);
    }

可以看到这里从上下文中将所有插件对象的集合plugins作为参数参入了SoulWebHandler,使SoulWebHandler具有处理所有插件的能力。

上面分析到SoulConfiguration配置文件将插件集合plugins配置到了SoulWebHandler,那么可以断定SoulWebHandler具备了我们网关所有插件的业务处理能力。

SoulWebHandler

查看SoulWebHandler的构造方法:

public final class SoulWebHandler implements WebHandler {

    private final List<SoulPlugin> plugins;

    private final Scheduler scheduler;

    /**
     * Instantiates a new Soul web handler.
     *
     * @param plugins the plugins
     */
    public SoulWebHandler(final List<SoulPlugin> plugins) {
        this.plugins = plugins;
        String schedulerType = System.getProperty("soul.scheduler.type", "fixed");
        if (Objects.equals(schedulerType, "fixed")) {
            int threads = Integer.parseInt(System.getProperty(
                    "soul.work.threads", "" + Math.max((Runtime.getRuntime().availableProcessors() << 1) + 1, 16)));
            scheduler = Schedulers.newParallel("soul-work-threads", threads);
        } else {
            scheduler = Schedulers.elastic();
        }
    }
  //...
}

可以看到SoulWebHandler集成了WebHandler,该类的作用就是处理WebFlux的请求的(回想以下我们上一篇中的GreetingWebHandler,这里两者是同一个东西)。从构造函数式中我们可以看到这个handler持有了一个SoulPlugin类型的集合,并且初始化了一个Scheduler类型的成员变量。

Scheduler

Reactor的Scheduler调度器相当于Java中的ExecutorService,不同的调度器定义不同的线程执行环境。Schedulers工具类提供的静态方法可搭建不同的线程执行环境。

Schedulers类已经预先创建了几种常用的不同线程池模型的调度器:使用single()elastic()parallel()方法创建的调度器可以分别使用内置的单线程、弹性线程池和固定大小线程池。如果想创建新的调度器,可以使用newSingle()newElastic()newParallel()方法。这些方法都是返回一个Scheduler的具体实现。

这里根据配置属性"fix",选择创建了一个固定大小的调度器,线程数默认为Cpu个数*2+1,最小为16个;或者是弹性调度器。

handle方法
    @Override
    public Mono<Void> handle(@NonNull final ServerWebExchange exchange) {
        MetricsTrackerFacade.getInstance().counterInc(MetricsLabelEnum.REQUEST_TOTAL.getName());
        Optional<HistogramMetricsTrackerDelegate> startTimer = MetricsTrackerFacade.getInstance().histogramStartTimer(MetricsLabelEnum.REQUEST_LATENCY.getName());
        return new DefaultSoulPluginChain(plugins).execute(exchange).subscribeOn(scheduler)
                .doOnSuccess(t -> startTimer.ifPresent(time -> MetricsTrackerFacade.getInstance().histogramObserveDuration(time)));
    }

从构造方法我们知道了,SoulWebHandler具备了处理所有插件逻辑的能力,整个Soul网关只配置了这么一个WebHandler,也就是说这个handler对应的handle方法是soul网关处理所有请求的地方。

这么做的好处很明显,我们只需要配置插件到Spring上下文,然后WebFlux去上下文获取就行了,扩展起来比较方便,而不是我们为每一个插件都配置一个WebHandler。

查看具体的handle方法,我们可以看到核心逻辑是有两个:

  1. 创建了一个HistogramMetricsTrackerDelegate类型的startTimer对象
  2. 执行DefaultSoulPluginChain的execute方法,加入scheduler线程模型,以及成功回调的处理逻辑(看起来和监控相关,后面分析)

详细说一下DefaultSoulPluginChain的execute方法,它是负责处理具体插件逻辑的。

@Override
        public Mono<Void> execute(final ServerWebExchange exchange) {
            return Mono.defer(() -> {
                if (this.index < plugins.size()) {
                    SoulPlugin plugin = plugins.get(this.index++);
                    Boolean skip = plugin.skip(exchange);
                    if (skip) {
                        return this.execute(exchange);
                    }
                    return plugin.execute(exchange, this);
                }
                return Mono.empty();
            });
        }

整体结构就是遍历所有插件类型,通过skip方法判断exchange中带有插件类型的参数是否跟plugin匹配,如果匹配则执行对应plugin的execute方法,如果不匹配则往下迭代。

skip方法

从exchange请求中获取插件参数判断,以此判断是否执行当前plugin的execute方法。

    @Override
    public Boolean skip(final ServerWebExchange exchange) {
        final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
        return !Objects.equals(Objects.requireNonNull(soulContext).getRpcType(), RpcTypeEnum.HTTP.getName());
    }

WebClientPlugin

上面我们了解到了SoulWebHandler将请求委托给具体的插件进行执行,这里我们的http请求会委托给WebClientPlugin。查看WebClientPlugin的execute实现,其核心逻辑则是从请求的exchange中获取http请求的参数,然后设置一些请求属性,最后重点来了——使用WebClient将http请求转发到我们的后端服务!(和我们上一篇的demo逻辑是一样的!)

    @Override
    public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
        final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
        assert soulContext != null;
        String urlPath = exchange.getAttribute(Constants.HTTP_URL);
        if (StringUtils.isEmpty(urlPath)) {
            Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
            return WebFluxResultUtils.result(exchange, error);
        }
        long timeout = (long) Optional.ofNullable(exchange.getAttribute(Constants.HTTP_TIME_OUT)).orElse(3000L);
        int retryTimes = (int) Optional.ofNullable(exchange.getAttribute(Constants.HTTP_RETRY)).orElse(0);
        log.info("The request urlPath is {}, retryTimes is {}", urlPath, retryTimes);
        HttpMethod method = HttpMethod.valueOf(exchange.getRequest().getMethodValue());
        WebClient.RequestBodySpec requestBodySpec = webClient.method(method).uri(urlPath);
        return handleRequestBody(requestBodySpec, exchange, timeout, retryTimes, chain);
    }
		//...
		private Mono<Void> handleRequestBody(final WebClient.RequestBodySpec requestBodySpec,
                                         final ServerWebExchange exchange,
                                         final long timeout,
                                         final int retryTimes,
                                         final SoulPluginChain chain) {
        return requestBodySpec.headers(httpHeaders -> {
            httpHeaders.addAll(exchange.getRequest().getHeaders());
            httpHeaders.remove(HttpHeaders.HOST);
        })
                .contentType(buildMediaType(exchange)) //设置contentType
                .body(BodyInserters.fromDataBuffers(exchange.getRequest().getBody())) //设置请求体
                .exchange() //发送请求
                .doOnError(e -> log.error(e.getMessage())) //异常处理
                .timeout(Duration.ofMillis(timeout)) //超时设置
                .retryWhen(Retry.onlyIf(x -> x.exception() instanceof ConnectTimeoutException) //重试处理
                    .retryMax(retryTimes)
                    .backoff(Backoff.exponential(Duration.ofMillis(200), Duration.ofSeconds(20), 2, true)))
                .flatMap(e -> doNext(e, exchange, chain));

    }
		//将请求响应传入设置参数,传入WebClientResponsePlugin插件进行处理
    private Mono<Void> doNext(final ClientResponse res, final ServerWebExchange exchange, final SoulPluginChain chain) {
        if (res.statusCode().is2xxSuccessful()) {
            exchange.getAttributes().put(Constants.CLIENT_RESPONSE_RESULT_TYPE, ResultEnum.SUCCESS.getName());
        } else {
            exchange.getAttributes().put(Constants.CLIENT_RESPONSE_RESULT_TYPE, ResultEnum.ERROR.getName());
        }
        exchange.getAttributes().put(Constants.CLIENT_RESPONSE_ATTR, res);
        return chain.execute(exchange);
    }

这里面需要注意的是handleRequestBody方法,利用链式调用许多逻辑 ,包括:

设置请求头参数->设置contentType->设置请求体->发送请求->异常处理->超时设置->重试处理->请求响应转换处理->返回Mono

这里需要注意几个WebClient的对象方法:

  • 方法exchange 的作用是发送请求并得到以 Mono表示的 HTTP 响应。
  • 方法flatMap是作用是处理响应并返回Mono对象。

WebClientResponsePlugin

最终处理后的响应落到WebClientResponsePlugin插件上,由该插件将返回值写入ServerHttpResponse, 返回给前端,由此整个http请求转发调用结束。

public class WebClientResponsePlugin implements SoulPlugin {
		@Override
    public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
        return chain.execute(exchange).then(Mono.defer(() -> {
            ServerHttpResponse response = exchange.getResponse();
            ClientResponse clientResponse = exchange.getAttribute(Constants.CLIENT_RESPONSE_ATTR);
            if (Objects.isNull(clientResponse)
                    || response.getStatusCode() == HttpStatus.BAD_GATEWAY
                    || response.getStatusCode() == HttpStatus.INTERNAL_SERVER_ERROR) {
                Object error = SoulResultWrap.error(SoulResultEnum.SERVICE_RESULT_ERROR.getCode(), SoulResultEnum.SERVICE_RESULT_ERROR.getMsg(), null);
                return WebFluxResultUtils.result(exchange, error);
            }
            if (response.getStatusCode() == HttpStatus.GATEWAY_TIMEOUT) {
                Object error = SoulResultWrap.error(SoulResultEnum.SERVICE_TIMEOUT.getCode(), SoulResultEnum.SERVICE_TIMEOUT.getMsg(), null);
                return WebFluxResultUtils.result(exchange, error);
            }
            response.setStatusCode(clientResponse.statusCode());
            response.getCookies().putAll(clientResponse.cookies());
            response.getHeaders().putAll(clientResponse.headers().asHttpHeaders());
            return response.writeWith(clientResponse.body(BodyExtractors.toDataBuffers()));
        }));
    }
 	//...
}

思考总结

今天我们详细分析了一个从前端http请求到soul网关的调用流程,大概逻辑如下:

  1. SoulConfiguration将我们所有装配的SoulPlugin加载到WebFlux上下文。
  2. 配置我们的WebHandler,并让它持有所有SoulPlugin对象。
  3. 通过请求url的contextPath参数来匹配对应的的rpc协议,并委托给响应的插件进行处理。
  4. 通过对应的rpc协议插件与后端进行请求调用。
  5. 将结果返回值转换成对应的Response写回客户端。

明天,我们具体分析一下整个Soul网关插件的调用顺序是怎么设计的。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值