【Soul源码阅读】5.GET 请求在 Soul 网关中流转的简单分析

前几天一直在外围转悠,试用了一下默认的 divide 插件,今天来研究下一个请求是如何通过 Soul 网关分发到真实服务节点的。

通过浏览器或 postman 发送一个 GET 请求。

http://localhost:9195/http/order/findById?id=95

selector 和 rule 默认都打开日志了,可以在 SoulBootstrapApplication 的 console 控制台看到如下日志如下:

2021-01-18 18:35:05.357  INFO 20836 --- [work-threads-17] o.d.soul.plugin.base.AbstractSoulPlugin  : divide selector success match , selector name :/http
2021-01-18 18:35:05.357  INFO 20836 --- [work-threads-17] o.d.soul.plugin.base.AbstractSoulPlugin  : divide selector success match , selector name :/http/order/findById
2021-01-18 18:35:05.358  INFO 20836 --- [work-threads-17] o.d.s.plugin.httpclient.WebClientPlugin  : The request urlPath is http://10.0.0.12:8188/order/findById?id=95, retryTimes is 0

可以看到1~2行日志,显示匹配上了 divide selector,日志是在 AbstractSoulPlugin 这个类,Ctrl + Shift + N 找到这个类,再 Ctrl + F 搜索 “selector success match”,可以搜索到2个方法,1个 selectorLog() 1个 ruleLog(),这两个方法都在同一个 execute() 方法中调用了,在 execute() 方法第1行把断点打上,我们再请求一次。

// AbstractSoulPlugin.java
/**
 * Process the Web request and (optionally) delegate to the next
 * {@code SoulPlugin} through the given {@link SoulPluginChain}.
 *
 * @param exchange the current server exchange
 * @param chain    provides a way to delegate to the next plugin
 * @return {@code Mono<Void>} to indicate when request processing is complete
 */
@Override
public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
	// 拿到插件 plugin 的名字
    String pluginName = named();
    final PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName);
    // 如果插件的 enable 为 true 才进,否则调用责任链的下一个节点
    if (pluginData != null && pluginData.getEnabled()) {
        final Collection<SelectorData> selectors = BaseDataCache.getInstance().obtainSelectorData(pluginName);
        if (CollectionUtils.isEmpty(selectors)) {
            return handleSelectorIsNull(pluginName, exchange, chain);
        }
        final SelectorData selectorData = matchSelector(exchange, selectors);
        if (Objects.isNull(selectorData)) {
            return handleSelectorIsNull(pluginName, exchange, chain);
        }
        selectorLog(selectorData, pluginName);
        final List<RuleData> rules = BaseDataCache.getInstance().obtainRuleData(selectorData.getId());
        if (CollectionUtils.isEmpty(rules)) {
            return handleRuleIsNull(pluginName, exchange, chain);
        }
        RuleData rule;
        if (selectorData.getType() == SelectorTypeEnum.FULL_FLOW.getCode()) {
            //get last
            rule = rules.get(rules.size() - 1);
        } else {
            rule = matchRule(exchange, rules);
        }
        if (Objects.isNull(rule)) {
            return handleRuleIsNull(pluginName, exchange, chain);
        }
        ruleLog(rule, pluginName);
        return doExecute(exchange, chain, selectorData, rule);
    }
    return chain.execute(exchange);
}

根据 pluginName 获取 PluginData,debug 时,进去看下,里面有一个 Map(PLUGIN_MAP) 保存了默认14种插件数据,如下图所示,默认情况下只有 divide 插件是打开的(enabled = true),这个类是 BaseDataCache,可以从名字上看出这是一个缓存类。
在这里插入图片描述
所以在判断插件不为 null 并且 enabled 为 true 时,能进入的只有 divide 插件。
下面就是匹配 selector 和 rule 的逻辑,这里迎来了1个重点,doExecute 方法,它的签名是:

protected abstract Mono<Void> doExecute(ServerWebExchange exchange, SoulPluginChain chain, SelectorData selector, RuleData rule);

这是一个抽象方法,所在的类 AbstractSoulPlugin 是一个抽象类,这就是一个抽象模板方法,需要子类自己去实现,Ctrl + H 可以看到这么多子类:
在这里插入图片描述
此时可以看到 this 就是 DividePlugin 对象:
在这里插入图片描述
debug 进入方法,会进到 DividePlugin 子类的覆写方法里:

// DividePlugin.java
@Override
protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
    final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
    assert soulContext != null;
    final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class);
    // 这里又看到了 Cache 字样,可以想见,里面缓存了真实服务器节点的相关信息。
    final List<DivideUpstream> upstreamList = UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selector.getId());
    if (CollectionUtils.isEmpty(upstreamList)) {
        log.error("divide upstream configuration error: {}", rule.toString());
        Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
        return WebFluxResultUtils.result(exchange, error);
    }
    final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
    DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
    if (Objects.isNull(divideUpstream)) {
        log.error("divide has no upstream");
        Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
        return WebFluxResultUtils.result(exchange, error);
    }
    // set the http url
    String domain = buildDomain(divideUpstream);
    // 获取真实服务器接口地址,此时为 http://10.0.0.12:8188/order/findById?id=95
    String realURL = buildRealURL(domain, soulContext, exchange);
    exchange.getAttributes().put(Constants.HTTP_URL, realURL);
    // set the http timeout 设置超时时间
    exchange.getAttributes().put(Constants.HTTP_TIME_OUT, ruleHandle.getTimeout());
    // 设置重试次数
    exchange.getAttributes().put(Constants.HTTP_RETRY, ruleHandle.getRetry());
    return chain.execute(exchange);
}

这里首先拿到了 soulContext:(Context 模式)
在这里插入图片描述
根据选择器 ID 从 UpstreamCacheManager 实例里面获取真实服务器节点的相关信息:
在这里插入图片描述
从以上信息中,获取到真实服务器接口地址,设置到 ServerWebExchange 对象的 attributes 属性里。
最后,这里的 chain.execute(exchange) 与前面插件没有打开走的逻辑是一样的,都是调用责任链的下一个节点。

// 接口定义
public interface SoulPluginChain {
    /**
     * Delegate to the next {@code WebFilter} in the chain.
     *
     * @param exchange the current server exchange
     * @return {@code Mono<Void>} to indicate when request handling is complete
     */
    Mono<Void> execute(ServerWebExchange exchange);
}

// 默认实现是 SoulWebHandler 的私有内部静态类
// SoulWebHandler.java
private static class DefaultSoulPluginChain implements SoulPluginChain {
    private int index;
    private final List<SoulPlugin> plugins;
    /**
     * Instantiates a new Default soul plugin chain.
     *
     * @param plugins the plugins
     */
    DefaultSoulPluginChain(final List<SoulPlugin> plugins) {
        this.plugins = plugins;
    }
    /**
     * Delegate to the next {@code WebFilter} in the chain.
     *
     * @param exchange the current server exchange
     * @return {@code Mono<Void>} to indicate when request handling is complete
     */
    @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();
        });
    }
}

代码进到 execute 方法里时,可以看到 index = 7,plugins 里有14个插件对象:
在这里插入图片描述
对于响应式编程,我也是才接触,先忽略掉,后面需要恶补一下。
Supplier 这个 lambda 表达式里的逻辑,根据 index 依次获取对应的 plugin 对象,如果 skip 为 true,直接跳过当前这个插件,再次执行当前这个 execute 方法,进入到下一个插件;如果 skip 为 false,执行这个插件的 execute 方法。
此时拿到的是 WebClientPlugin,skip 为 false,进入到 WebClientPlugin 的 execute 方法。

// WebClientPlugin.java
@Override
public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
    final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
    assert soulContext != null;
    // 把刚才封装进去真实服务器接口地址拿出来 http://10.0.0.12:8188/order/findById?id=95
    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))
            .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));

}

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

在 doNext 方法中,把请求的响应及响应状态封装到了 ServerWebExchange 对象中。
最后,继续调用责任链的 execute 方法,进行下一个插件的逻辑,直到所有的插件都执行了一遍。

经过上面的调试,可以肯定的是2点:
1.通过 divide plugin 获取到真实服务器接口信息。
2.通过 WebClientPlugin 真正向服务器调用接口,获取返回信息。

在这个过程中,使用 ServerWebExchange 对象作为 Session 一样使用,至于为什么这样使用,还需要进一步研究。

整个调用链没有整个搞清楚,只了解了其中一部分,需要我们一步步深入,一点点攻破各个难点,各位,我们一起加油。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值