Soul網關學習Http請求探險

回顧

在Soul 請求處理概覽概覽這篇文章中,咱們已經知曉了Soul針對於請求的處理入庫在DefaultSoulPluginChain的excute,其中執行了一個插件鏈的模式來完成了請求的處理。前端

咱們大致梳理了注入到plugins的插件,可是即便這樣依然不能縱觀全局,對此特意對soul插件所涉及的類進行了相關梳理,總體梳理結果以下圖。web

在梳理文章中能夠看到核心類是SoulPlugin、PluginEnum、PluginDataHandler、MetaDataSubscriber,在梳理請求的相關文章中咱們目前只須要重點關注SoulPlugin與PluginEnum類。編程

SoulPlugin類咱們已經有了必定的理解,那PluginEnum枚舉類的主要做用是什麼呢?websocket

PluginEnum:插件的枚舉類markdown

屬性 做用
code 插件的執行順序 越小越先執行
role 角色 暫時未發現實際引用地址
name 插件名稱

其實咱們不難發如今DefaultSoulPluginChain的plugins的插件都是有固定的執行順序的,那這個插件的執行順序是在哪定義的呢?cookie

最終能夠追溯到SoulConfiguration類下app

public SoulWebHandler soulWebHandler(final ObjectProvider<List<SoulPlugin>> plugins) {
        //省略
        final List<SoulPlugin> soulPlugins = pluginList.stream()
               .sorted(Comparator.comparingInt(SoulPlugin::getOrder)).collect(Collectors.toList());
        return new SoulWebHandler(soulPlugins);
    }

複製代碼

整理整個PluginEnum類相關引用,整理出以下表格,不難看出插件與插件之間的順序關係 負載均衡

等級 做用
第一等級 只有GlobalPlugin 全局插件
第二等級到第八等級 能夠理解爲在請求發起前的前置處理插件
第九等級到第十一等級 能夠理解爲針對調用方的方式所針對的不一樣調用處理
第十二等級 只有MonitorPlugin 監控插件
第十三等級 是針對於各個調用方返回結果處理的Response相關插件

在剛纔的回顧中咱們已經明白soul處理請求的大致流程 - 1.GloBalPlugin插件 進行全局的初始化 - 2.部分插件根據鑑權、限流、熔斷等規則對請求進行處理 - 3.選擇適合本身的調用方式進行拼裝參數,發起調用。 - 4.進行監控 - 5.對調用的結果進行處理dom

請求流程梳理

如下演示代碼截圖來自於soul-examples下的http demo,調用的接口地址爲http://127.0.0.1:9195/http/test/findByUserId?userId=10socket

DefaultSoulPluginChain的excute方法進行埋點,查看一次http請求調用通過了哪些類?

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) {
                        System.out.println("跳過的插件爲"+plugin.getClass().getName().replace("org.dromara.soul.plugin.",""));
                        return this.execute(exchange);
                    }
                    System.out.println("未跳過的插件爲"+plugin.getClass().getName().replace("org.dromara.soul.plugin.",""));
                    return plugin.execute(exchange, this);
                }
                return Mono.empty();
            });
        }

複製代碼

最終輸出的未跳過的插件以下:

未跳過的插件爲global.GlobalPlugin
未跳過的插件爲sign.SignPlugin
未跳過的插件爲waf.WafPlugin
未跳過的插件爲ratelimiter.RateLimiterPlugin
未跳過的插件爲hystrix.HystrixPlugin
未跳過的插件爲resilience4j.Resilience4JPlugin
未跳過的插件爲divide.DividePlugin
未跳過的插件爲httpclient.WebClientPlugin
未跳過的插件爲alibaba.dubbo.param.BodyParamPlugin
未跳過的插件爲monitor.MonitorPlugin
未跳過的插件爲httpclient.response.WebClientResponsePlugin

這裏有個小疑惑,爲啥這個alibaba.dubbo.param.BodyParamPlugin插件會被執行,暫時忽略,後期跟蹤。

咱們發現一次針對於http請求的網關調用 所執行的插件的大致流程與咱們猜測的處理流程一致。
目前咱們只挑重點來說,即GlobalPlugin、DividePlugin、WebClientPlugin、WebClientResponsePlugin

發起Debug調用依次追蹤上述四個插件的做用。

GlobalPlugin SoulContext對象封裝插件

GlobalPlugin的插件的excute方法以下所示

public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
        final ServerHttpRequest request = exchange.getRequest();
        final HttpHeaders headers = request.getHeaders();
        final String upgrade = headers.getFirst("Upgrade");
        SoulContext soulContext;
        if (StringUtils.isBlank(upgrade) || !"websocket".equals(upgrade)) {
            soulContext = builder.build(exchange);
        } else {
            final MultiValueMap<String, String> queryParams = request.getQueryParams();
            soulContext = transformMap(queryParams);
        }
        exchange.getAttributes().put(Constants.CONTEXT, soulContext);
        return chain.execute(exchange);
    }

複製代碼

不難看出 在GlobalPlugin的excute方法中主要目的就是封裝一個SoulContext對象,放入exchange中(exchange對象是整個插件鏈上的共享對象,有一個插件執行完成後傳遞給下一個插件,本人理解的就是一個相似於ThreadLocal對象)。

那SoulContext對象中又包含哪些屬性呢?

屬性 含義
module 每種RPCType針對的值不一樣http調用時指代網關調用的前置地址
method 切割後的方法名(在RpcType爲http時)
rpcType RPC調用類型有Http、dubbo、sofa等
httpMethod Http調用的方式目前只支持get、post
sign 鑑權的相關屬性目前不知道具體做用,可能與SignPlugin插件有關
timestamp 時間戳
appKey 鑑權的相關屬性目前不知道具體做用,可能與SignPlugin插件有關
path 路徑指代調用到soul網關的全路徑(在RpcType爲http時)
contextPath 與module取值一致(在RPCType爲http時)
realUrl 與method的值一致(在RpcType爲http時)
dubboParams dubbo的參數?
startDateTime 開始時間懷疑與監控插件和統計指標模塊有聯用

在執行完GlobalPlugin插件後,最終封裝完成的SoulContext對象以下所示。

其餘RPCType的SoulContext的參數封裝能夠查看DefaultSoulContextBuilder的build方法進行追蹤,因爲本編文章主要追溯http調用,故在這裏不在多餘討論。

DividePlugin 路由選擇插件

在執行完成GlobalPlugin插件後,最終封裝成了一個SoulContext對象,並將其放在了ServerWebExchange中,供下游的調用鏈使用。

接下來讓咱們看一下DividePlugin插件在整個鏈式調用過程當中到底起了一個什麼樣的做用?

AbstractSoulPlugin

經過追溯源碼得知DividePlugin插件繼承於AbstractSoulPlugin類,而AbstractSoulPlugin類實現了SoulPlugin接口

那麼AbstractSoulPlugin又作了哪些擴展呢?讓咱們梳理一下該類的方法。

方法名 做用
excute 實現於SoulPlugin接口,在AbstractSoulPlugin中起到一個模板方法的做用
doexcute 抽象方法 交由各個子類實現
matchSelector 匹配選擇器
filterSelector 篩選選擇器
matchRule 匹配規則
filterRule 篩選規則
handleSelectorIsNull 處理選擇器爲空狀況
handleRuleIsNull 處理規則爲空狀況
selectorLog 選擇器日誌打印
ruleLog 規則日誌打印

看一下excute方法的具體做用

public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
        String pluginName = named();
        //獲取對應插件
        final PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName);
        //判斷插件是否啓用
        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()) {
                rule = rules.get(rules.size() - 1);
            } else {
                //匹配規則
                rule = matchRule(exchange, rules);
            }
            if (Objects.isNull(rule)) {
                return handleRu![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f523f655f0014d288b7a4502cc6a08d1~tplv-k3u1fbpfcp-watermark.image)leIsNull(pluginName, exchange, chain);
            }
            //打印規則日誌
            ruleLog(rule, pluginName);
            //執行子類具體實現
            return doExecute(exchange, chain, selectorData, rule);
        }
        return chain.execute(exchange);
    }

複製代碼

最終整理的流程圖以下所示:

ps:在上述的流程圖中並無細化到具體的方法級別的處理。

但仍有幾個點須要着重解釋一下: - 1.插件數據、選擇器數據、規則數據的獲取所有來自於BaseDataCache,該類是數據同步過程當中最終會影響的類。 - 2.選擇器的類型,在使用SpringMvc項目進行接口註冊時,會有一個isFull的選項爲true表明全局代理,在全局代理模式下只會註冊一個選擇器\規則(指代代理全部的接口),因此這裏的對應處理爲rule.size()-1. - 3.選擇器和規則的選擇,實際的處理要複雜的多,考慮到是介紹一次請求流程的大致邏輯,因此這裏不展開闡述,有興趣的能夠查看MatchStrategy、AbstractMatchStrategy及其相關實現類(後期會單獨開一篇具體講解),此處對應頁面的以下:

梳理一下AbstractSoulPlugin的exeute方法做用,通過上述流程圖的引導,咱們已經知曉該方法的做用是爲了選取插件—>選取選擇器—>選取規則,最後交由子類的doexcute方法。

接下來讓咱們看一下DividePlugin的doexcute方法具體作了哪些事。

DividePlugin

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);
        //獲取該選擇器下的注入的地址
        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);
        //拼裝真實調用地址
        String realURL = buildRealURL(domain, soulContext, exchange);
        exchange.getAttributes().put(Constants.HTTP_URL, realURL);
        //設置超時時間 及重試次數
        exchange.getAttributes().put(Constants.HTTP_TIME_OUT, ruleHandle.getTimeout());
        exchange.getAttributes().put(Constants.HTTP_RETRY, ruleHandle.getRetry());
        return chain.execute(exchange);
    }

複製代碼

經過上述代碼梳理完成後大致邏輯以下: - 1.獲取選擇器對應的註冊地址,對應頁面數據以下 - 2.根據規則的handle字段獲取負載均衡策略,並選擇真實的調用地址(LoadBalanceUtils),重試次數和超時時間,對應頁面數據以下。 - 3.將真實調用地址,超時時間,重試次數傳遞到ServerWebExchange中,供下游調用鏈使用。 debug演示: ps:在上述的主題邏輯中咱們沒有看到參數在哪裏?那這個參數在哪封裝的呢?答案在buildRealURL方法中,是從exchange上下文中獲取到的。

WebClientPlugin Http請求調用插件

接下來讓咱們看看Soul如何發起的請求調用

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

複製代碼

在webClient的excute方法中,主要作了三個事 - 1.將從Divide插件中放入exchange的屬性取出來,調用的真實地址、超時時間、重試次數。 - 2.封裝了一個RequestBodySpec對象(不認識這個響應式編程的東西) - 3.調用了一個handleRequestBody方法

先認識handleRequestBody方法

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

    }

複製代碼

在這個方法裏,大致能夠理解爲 - exchange中的請求頭放到本次調用的請求頭中 - 設置contentType - 設置超時時間 - 設置失敗響應 - 設置重試的場景及重試次數 - 最終結果的處理。 在流程中須要還須要看一個doNext方法

大致邏輯就是判斷請求是否成功,將請求結果放入exchange中交給下游插件處理。

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

複製代碼

ps: 雖然並不懂響應式編程,但並不影響咱們閱讀代碼。

WebClientResponsePlugin Http結果處理插件

該實現的excute方法沒有什麼核心邏輯,就是判斷請求狀態碼,根據狀態碼返回給前端不一樣的數據格式。

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

複製代碼

總結

到此爲止,一個基於Soul網關發起的Http請求調用流程大致已經結束。

梳理http請求調用流程 - Global插件封裝SoulContext對象 - 前置插件處理熔斷限流鑑權等操做。 - Divide插件選擇對應調用的真實地址,重試次數,超時時間。 - WebClient插件發起真實的Http調用 - WebClientResponse插件處理對應結果,返回前臺。

基於Http調用的大致流程,咱們能夠大致猜想出基於別RPC調用的流程,就是替換髮起請求的插件和返回結果處理的插件。

在上文中咱們還提到了路由規則的選擇LoadBalanceUtils,選擇器和規則的處理MatchStrategy

以後將會開啓新篇章一步步揭開RPC泛化調用,路由選擇,選擇器、規則匹配的神祕面紗。

相關文章
相關標籤/搜索