写点什么

我是一个请求,我是如何被发送的?

发布于: 4 小时前

​​​​​​​​摘要:本文主要分析使用 cse 提供的 RestTemplate 的场景,其实 cse 提供的 rpc 注解(RpcReference)的方式最后的调用逻辑和 RestTemplate 是殊途同归的。


本文分享自华为云社区《我是一个请求,我该何去何从(下)》,原文作者:向昊 。

 

那么发送请求又是个什么样的流程了?本文主要分析使用 cse 提供的 RestTemplate 的场景,其实 cse 提供的 rpc 注解(RpcReference)的方式最后的调用逻辑和 RestTemplate 是殊途同归的。

使用


使用 cse 提供的 RestTemplate 时候,是这样初始化的:


RestTemplate restTemplate = RestTemplateBuilder.create();
restTemplate.getForObject("cse://appId:serviceName/xxx", Object.class);
复制代码


我们可以注意到 2 个怪异的地方:


  • RestTemplate 是通过 RestTemplateBuilder.create()来获取的,而不是用的 Spring 里提供的。

  • 请求路径开头是 cse 而不是我们常见的 http、https 且需要加上服务所属的应用 ID 和服务名称。

解析


根据 url 匹配 RestTemplate


首先看下 RestTemplateBuilder.create(),它返回的是 org.apache.servicecomb.provider.springmvc.reference.RestTemplateWrapper,是 cse 提供的一个包装类。


// org.apache.servicecomb.provider.springmvc.reference.RestTemplateWrapper// 用于同时支持cse调用和非cse调用class RestTemplateWrapper extends RestTemplate {    private final List<AcceptableRestTemplate> acceptableRestTemplates = new ArrayList<>();     final RestTemplate defaultRestTemplate = new RestTemplate();     RestTemplateWrapper() {        acceptableRestTemplates.add(new CseRestTemplate());    }     RestTemplate getRestTemplate(String url) {        for (AcceptableRestTemplate template : acceptableRestTemplates) {            if (template.isAcceptable(url)) {                return template;            }        }        return defaultRestTemplate;    }}
复制代码


  • AcceptableRestTemplate:这个类是一个抽象类,也是继承 RestTemplate 的,目前其子类就是 CseRestTemplate,我们也可以看到在初始化的时候会默认往 acceptableRestTemplates 中添加一个 CseRestTemplate。


  • 回到使用的地方 restTemplate.getForObject:这个方法会委托给如下方法:


public <T> T getForObject(String url, Class<T> responseType, Object... urlVariables) throws RestClientException {    return getRestTemplate(url).getForObject(url, responseType, urlVariables);}
复制代码


可以看到首先会调用 getRestTemplate(url),即会调用 template.isAcceptable(url),如果匹配到了就返回 CseRestTemplate,否则就返回常规的 RestTemplate。那么再看下 isAcceptable()这个方法:

 

到这里我们就清楚了路径中的 cse://的作用了,就是为了使用 CseRestTemplate 来发起请求,也理解了为啥 RestTemplateWrapper 可以同时支持 cse 调用和非 cse 调用。

委托调用


从上面可知,我们的 cse 调用其实都是委托给 CseRestTemplate 了。在构造 CseRestTemplate 的时候会初始化几个东西:


public CseRestTemplate() {    setMessageConverters(Arrays.asList(new CseHttpMessageConverter()));    setRequestFactory(new CseClientHttpRequestFactory());    setUriTemplateHandler(new CseUriTemplateHandler());}
复制代码


这里需要重点关注 newCseClientHttpRequestFactory():


public class CseClientHttpRequestFactory implements ClientHttpRequestFactory {    @Override    public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {        return new CseClientHttpRequest(uri, httpMethod);    }}
复制代码


最终委托到了 CseClientHttpRequest 这个类,这里就是重头戏了!


我们先把注意力拉回到这句话:restTemplate.getForObject("cse://appId:serviceName/xxx",Object.class),从上面我们知道其逻辑是先根据 url 找到对应的 RestTemplate,然后调用 getForObject 这个方法,最终这个方法会调用到:org.springframework.web.client.RestTemplate#doExecute:


protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,    @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {     ClientHttpResponse response = null;    try {        ClientHttpRequest request = createRequest(url, method);        if (requestCallback != null) {            requestCallback.doWithRequest(request);        }        response = request.execute();        handleResponse(url, method, response);        return (responseExtractor != null ? responseExtractor.extractData(response) : null);    }}
复制代码


  • createRequest(url,method):会调用 getRequestFactory().createRequest(url, method),即最终会调用到我们初始化 CseClientHttpRequest 是塞的 RequestFactory,所以这里会返回 ClientHttpRequest 这个类。


  • request.execute():这个方法会委托到 org.apache.servicecomb.provider.springmvc.reference.CseClientHttpRequest#execute 这个方法上。


至此我们知道前面的调用最终会委托到 CseClientHttpRequest#execute 这个方法上

cse 调用


接着上文分析:


public ClientHttpResponse execute() {    path = findUriPath(uri);    requestMeta = createRequestMeta(method.name(), uri);     QueryStringDecoder queryStringDecoder = new QueryStringDecoder(uri.getRawSchemeSpecificPart());    queryParams = queryStringDecoder.parameters();     Object[] args = this.collectArguments();     // 异常流程,直接抛异常出去    return this.invoke(args);}
复制代码


  • createRequestMeta(method.name(), uri):这里主要是根据 microserviceName 去获取调用服务的信息,并会将获取的信息放入到 Map 中。服务信息如下:



  • 可以看到里面的信息很丰富,例如应用名、服务名、还有接口对应的 yaml 信息等。


  • this.collectArguments():这里隐藏了一个校验点,就是会校验传入的参数是否符合对方接口的定义。主要是通过这个方法:org.apache.servicecomb.common.rest.codec.RestCodec#restToArgs,如果不符合真个流程就结束了。

准备 invocation


从上面分析可知,获取到接口所需的参数后就会调用这个方法:

org.apache.servicecomb.provider.springmvc.reference.CseClientHttpRequest#invoke:


private CseClientHttpResponse invoke(Object[] args) {    Invocation invocation = prepareInvocation(args);    Response response = doInvoke(invocation);     if (response.isSuccessed()) {        return new CseClientHttpResponse(response);    }     throw ExceptionFactory.convertConsumerException(response.getResult());}
复制代码


  • prepareInvocation(args):这个方法会准备好 Invocation,这个 Invocation 在上集已经分析过了,不过上集中的它是为服务端服务的,那么咱们这块当然就得为消费端工作了


protected Invocation prepareInvocation(Object[] args) {    Invocation invocation =        InvocationFactory.forConsumer(requestMeta.getReferenceConfig(),            requestMeta.getOperationMeta(),            args);        return invocation;}
复制代码


从名字也可以看出它是为消费端服务的,其实无论是 forProvider 还是 forConsumer,它们最主要的区别就是加载的 Handler 不同,这次加载的 Handler 如下:

class org.apache.servicecomb.qps.ConsumerQpsFlowControlHandler

(流控)

classorg.apache.servicecomb.loadbalance.LoadbalanceHandler

(负载)

classorg.apache.servicecomb.bizkeeper.ConsumerBizkeeperHandler

(容错)

classorg.apache.servicecomb.core.handler.impl.TransportClientHandler

(调用,默认加载的)


  • doInvoke(invocation):初始化好了 invocation 后就开始调用了。最终会调用到这个方法上:org.apache.servicecomb.core.provider.consumer.InvokerUtils#innerSyncInvoke


至此,这些动作就是 cse 中 RestTemplate 和 rpc 调用的不同之处。不过可以清楚的看到 RestTemplate

的方式是只支持同步的,即 innerSyncInvoke,但是 rpc 是可以支持异步的,即 reactiveInvoke


public static Response innerSyncInvoke(Invocation invocation) {        invocation.next(respExecutor::setResponse);}
复制代码


到这里我们知道了,消费端发起请求还是得靠 invocation 的责任链驱动

启动 invocation 责任链


好了,咱们的老朋友又出现了:invocation.next,这个方法是个典型的责任链模式,其链条就是上面说的那 4 个 Handler。前面 3 个就不分析了,直接跳到 TransportClientHandler。


// org.apache.servicecomb.core.handler.impl.TransportClientHandlerpublic void handle(Invocation invocation, AsyncResponse asyncResp) throws Exception {    Transport transport = invocation.getTransport();    transport.send(invocation, asyncResp);}
复制代码


  • invocation.getTransport():获取请求地址,即最终发送请求的时候还是以 ip:port 的形式。


  • transport.send(invocation, asyncResp):调用链为 org.apache.servicecomb.transport.rest.vertx.VertxRestTransport#send->org.apache.servicecomb.transport.rest.client.RestTransportClient#send(这里会初始化 HttpClientWithContext,下面会分析)->org.apache.servicecomb.transport.rest.client.http.RestClientInvocation#invoke(真正发送请求的地方)


public void invoke(Invocation invocation, AsyncResponse asyncResp) throws Exception {     createRequest(ipPort, path);    clientRequest.putHeader(org.apache.servicecomb.core.Const.TARGET_MICROSERVICE, invocation.getMicroserviceName());    RestClientRequestImpl restClientRequest =        new RestClientRequestImpl(clientRequest, httpClientWithContext.context(), asyncResp, throwableHandler);    invocation.getHandlerContext().put(RestConst.INVOCATION_HANDLER_REQUESTCLIENT, restClientRequest);     Buffer requestBodyBuffer = restClientRequest.getBodyBuffer();    HttpServletRequestEx requestEx = new VertxClientRequestToHttpServletRequest(clientRequest, requestBodyBuffer);    invocation.getInvocationStageTrace().startClientFiltersRequest();    // 触发filter.beforeSendRequest方法    for (HttpClientFilter filter : httpClientFilters) {        if (filter.enabled()) {            filter.beforeSendRequest(invocation, requestEx);        }    }        // 从业务线程转移到网络线程中去发送    // httpClientWithContext.runOnContext}
复制代码


  • createRequest(ipPort,path):根据参数初始化 HttpClientRequestclientRequest,初始化的时候会传入一个创建一个 responseHandler,即对响应的处理。注意 org.apache.servicecomb.common.rest.filter.HttpClientFilter#afterReceiveResponse 的调用就是在这里埋下伏笔的,是通过回调 org.apache.servicecomb.transport.rest.client.http.RestClientInvocation#processResponseBody 这个方法触发的(在创建 responseHandler 时候创建的)且 org.apache.servicecomb.common.rest.filter.HttpClientFilter#beforeSendRequest:这个方法的触发我们也可以很清楚的看到在发送请求执行的。


  • requestEx:注意它的类型是 HttpServletRequestEx,虽然名字里面带有 Servlet,但是打开它的方法可以发现有很多我们在 tomcat 中那些常用的方法都直接抛出异常了,这也是一个易错点!


  • httpClientWithContext.runOnContext:用来发送请求的逻辑,不过这里还是有点绕的,下面重点分析下


httpClientWithContext.runOnContext


首先看下 HttpClientWithContext 的定义:


public class HttpClientWithContext {    public interface RunHandler {        void run(HttpClient httpClient);    }     private HttpClient httpClient;     private Context context;     public HttpClientWithContext(HttpClient httpClient, Context context) {        this.httpClient = httpClient;        this.context = context;    }     public void runOnContext(RunHandler handler) {        context.runOnContext((v) -> {            handler.run(httpClient);        });    }}
复制代码


从上面可知发送请求调用的是这个方法:runOnContext,参数为 RunHandler 接口,然后是以 lambda 的方式传入的,lambda 的参数为 httpClient,这个 httpClient 又是在 HttpClientWithContext 的构造函数中初始化的。这个构造函数是在 org.apache.servicecomb.transport.rest.client.RestTransportClient#send 这个方法中初始化的(调用

org.apache.servicecomb.transport.rest.client.RestTransportClient#findHttpClientPool 这个方法)。


但是我们观察调用的地方:


// 从业务线程转移到网络线程中去发送httpClientWithContext.runOnContext(httpClient -> {    clientRequest.setTimeout(operationMeta.getConfig().getMsRequestTimeout());    processServiceCombHeaders(invocation, operationMeta);    try {        restClientRequest.end();    } catch (Throwable e) {        LOGGER.error(invocation.getMarker(),            "send http request failed, local:{}, remote: {}.", getLocalAddress(), ipPort, e);        fail((ConnectionBase) clientRequest.connection(), e);    }});
复制代码


其实在这块逻辑中 HttpClient 是没有被用到的,实际上发送请求的动作是 restClientRequest.end()

触发的,restClientRequest 是 cse 中的类 RestClientRequestImpl,然后它包装了 HttpClientRequest

(vertx 中提供的),即 restClientRequest.end()最终还是委托到了 HttpClientRequest.end()

上了。


那么这个 HttpClientRequest 是怎么被初始化的了?它是在 createRequest(ipPort,path)这个方法中初始化的,即在调用 org.apache.servicecomb.transport.rest.client.http.RestClientInvocation#invoke 方法入口处。


初始化的逻辑如下:


clientRequest = httpClientWithContext.getHttpClient().request(method, requestOptions, this::handleResponse)
复制代码


httpClientWithContext.getHttpClient():这个方法返回的是 HttpClient,上面说的 HttpClient 作用就体现出来了,用来初始化了我们发送请求的关键先生:HttpClientRequest。那么至此我们发送请求的整体逻辑大概就清晰了。

总结


无论是采用 RestTemplate 的方式还是采用 rpc 注解的方式来发送请求,其底层逻辑其实是一样的。即首先根据请求信息匹配到对方的服务信息,然后经过一些列的 Handler 处理,如限流、负载、容错等等(这也是一个很好的扩展机制),最终会走到 TransportClientHandler 这个 Handler,然后根据条件去初始化发送的 request,经过 HttpClientFilter 的处理后就会委托给 vertx 的 HttpClientRequest 来真正的发出请求。


点击关注,第一时间了解华为云新鲜技术~

发布于: 4 小时前阅读数: 5
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
我是一个请求,我是如何被发送的?