HttpClient源码解析系列二:请求的主要流程和源码

首先看一下继承实现的关系图:

下面我们就根据上篇的例子来分析源码的处理流程,先说一下本片博客的主要内容及流程

继续跟踪httpclient.execute()方法,发现其内部会调用CloseableHttpClient.doExecute()方法,实际会调到InternalHttpClient类的抽象方法doExecute(),该抽象方法有三种实现,如下:

三种实现分别为基础配置实现类,全量配置实现类和基本配置实现类,我们从全量配置实现类InternalHttpClient开始分析,那我们就先来看一下InternalHttpClient类的字段,然后再看对父类doExecute()的实现。

下面解释一下上面的三个字段:

1.HttpClientConnectionManager:看名字就知道是连接管理器是一个接口,有两个实现BasicHttpClientConnectionManager和PoolingHttpClientConnectionManager分别是单线程管理器的和连接池管理器,来管理连接的生命周期,比如Connection被创建出来后处于闲置状态,由连接池管理,调用时会校验是否是open状态,不是的话会进行connect。connect的过程就是 基于不同schema(http、https、ftp等,就是https://baidu.com这种)创建不同的socket连接(ssl和plain)并且将http请求(连接)绑定到socket。这个不是本文讨论的重点,后面有机会单独开一篇PoolingHttpClientConnectionManager连接池的介绍。

2.ClientExecChain:类似于执行器,执行完整的调用执行过程,它是一个包装类,类似于java io类,每个包装类完成一个特定的功能,多层嵌套完成一个系统性的功能,比如处理协议范畴的例如cookie、报文解析等,又比如处理自动重试、重定向、身份验证等功能。

3.HttpRoutePlanner:从HttpHost, HttpRequest, HttpContext 三个值来获取HttpRoute的过程,可以理解成从参数和环境中获取连接的目标地址。

4.Lookup<CookieSpecProvider>:是 CookieSpec(cookie的管理规范)的工厂,后者定义了Set-Cookie以及Cookie的转换方式

5.Lookup<AuthSchemeProvider>:是AuthScheme的工厂,跟权限验证有关,不太清楚。。。

6.CookieStore:用来保存cookie的,和增删改的操作。

7.CredentialsProvider:跟第五条有关,不清楚。。。

8.RequestConfig:配置request的。

9.List<Closeable>:用来关闭流、释放资源。

下面看看核心方法doExecute()

protected CloseableHttpResponse doExecute(
            final HttpHost target,
            final HttpRequest request,
            final HttpContext context) throws IOException, ClientProtocolException {
        Args.notNull(request, "HTTP request");
        HttpExecutionAware execAware = null;
        if (request instanceof HttpExecutionAware) {
            execAware = (HttpExecutionAware) request;
        }
        try {
            final HttpRequestWrapper wrapper = HttpRequestWrapper.wrap(request, target);
            final HttpClientContext localcontext = HttpClientContext.adapt(
                    context != null ? context : new BasicHttpContext());
            RequestConfig config = null;
            if (request instanceof Configurable) {
                config = ((Configurable) request).getConfig();
            }
            if (config == null) {
                final HttpParams params = request.getParams();
                if (params instanceof HttpParamsNames) {
                    if (!((HttpParamsNames) params).getNames().isEmpty()) {
                        config = HttpClientParamConfig.getRequestConfig(params, this.defaultConfig);
                    }
                } else {
                    config = HttpClientParamConfig.getRequestConfig(params, this.defaultConfig);
                }
            }
            if (config != null) {
                localcontext.setRequestConfig(config);
            }
            setupContext(localcontext);
            final HttpRoute route = determineRoute(target, wrapper, localcontext);
            return this.execChain.execute(route, wrapper, localcontext, execAware);
        } catch (final HttpException httpException) {
            throw new ClientProtocolException(httpException);
        }
    }

其实InternalHttpClient类的主要的字段了解了,上面的代码都不需要仔细看了,主要是通过对请求对象(HttpGet、HttpPost等)进行一番包装后,最后实际由execChain.execute()来真正执行请求,(上面的倒数第三行)继续跟着方法往下招实现,由于我们在上篇的例子中使用的是HttpClients.createDefault()这个默认方法构造了CloseableHttpClient,没有指定ClientExecChain接口的具体实现类,所以系统默认会使用RedirectExec这个实现类,

由于RedirectExec#execute()方法,如下:

public CloseableHttpResponse execute(
            final HttpRoute route,
            final HttpRequestWrapper request,
            final HttpClientContext context,
            final HttpExecutionAware execAware) throws IOException, HttpException {
        // 判断是否为空
        Args.notNull(route, "HTTP route");
        Args.notNull(request, "HTTP request");
        Args.notNull(context, "HTTP context");

        final List<URI> redirectLocations = context.getRedirectLocations();
        if (redirectLocations != null) {
            redirectLocations.clear();
        }
        final RequestConfig config = context.getRequestConfig();
        final int maxRedirects = config.getMaxRedirects() > 0 ? config.getMaxRedirects() : 50;
        HttpRoute currentRoute = route;
        HttpRequestWrapper currentRequest = request;
        // 无限循环是为了除了多重的重定向,其实这个类主要的功能是处理重定向
        // 然后调用MainClientExec类中的#execute()方法,而这个方法才是真正的发送请求
        for (int redirectCount = 0;;) {
            // 每次都调用自己进行处理,知道没有重定向请求,然后才返回值(在下面)
            final CloseableHttpResponse response = requestExecutor.execute(
                    currentRoute, currentRequest, context, execAware);
            try {
                // 满足重定向的要求,就会发生重定向请求,和后面的else相对应
                // 如果远端返回结果标识需要重定向(响应头部是301、302、303、307等重定向标识),则HttpClient默认会自动帮我们做重定向
                if (config.isRedirectsEnabled() &&
                        this.redirectStrategy.isRedirected(currentRequest.getOriginal(), response, context)) {
                    // 重定向的次数默认最大50次
                    if (redirectCount >= maxRedirects) {
                        throw new RedirectException("Maximum redirects ("+ maxRedirects + ") exceeded");
                    }
                    redirectCount++;

                    final HttpRequest redirect = this.redirectStrategy.getRedirect(
                            currentRequest.getOriginal(), response, context);
                    if (!redirect.headerIterator().hasNext()) {
                        final HttpRequest original = request.getOriginal();
                        redirect.setHeaders(original.getAllHeaders());
                    }
                    currentRequest = HttpRequestWrapper.wrap(redirect);

                    if (currentRequest instanceof HttpEntityEnclosingRequest) {
                        RequestEntityProxy.enhance((HttpEntityEnclosingRequest) currentRequest);
                    }

                    final URI uri = currentRequest.getURI();
                    final HttpHost newTarget = URIUtils.extractHost(uri);
                    if (newTarget == null) {
                        throw new ProtocolException("Redirect URI does not specify a valid host name: " +
                                uri);
                    }

                    // Reset virtual host and auth states if redirecting to another host
                    if (!currentRoute.getTargetHost().equals(newTarget)) {
                        final AuthState targetAuthState = context.getTargetAuthState();
                        if (targetAuthState != null) {
                            this.log.debug("Resetting target auth state");
                            targetAuthState.reset();
                        }
                        final AuthState proxyAuthState = context.getProxyAuthState();
                        if (proxyAuthState != null && proxyAuthState.isConnectionBased()) {
                            this.log.debug("Resetting proxy auth state");
                            proxyAuthState.reset();
                        }
                    }

                    currentRoute = this.routePlanner.determineRoute(newTarget, currentRequest, context);
                    if (this.log.isDebugEnabled()) {
                        this.log.debug("Redirecting to '" + uri + "' via " + currentRoute);
                    }
                    EntityUtils.consume(response.getEntity());
                    response.close();
                } else {
                    // 如果没有重定向了,就返回
                    // 拿到最终真正的业务返回结果后,直接把整个response向外返回,这一步没有帮我们关闭流 ,外层的业务代码在使用完response后,需要我们自己关闭流   
                    return response;
                }
            } catch (final RuntimeException ex) {
                // 如果中途发生了异常,也会帮我们把流关闭
                response.close();
                throw ex;
            } catch (final IOException ex) {
                // 如果中途发生了异常,也会帮我们把流关闭
                response.close();
                throw ex;
            } catch (final HttpException ex) {
                // Protocol exception related to a direct.
                // The underlying connection may still be salvaged.
                try {
                    EntityUtils.consume(response.getEntity());
                } catch (final IOException ioex) {
                    this.log.debug("I/O error while releasing connection", ioex);
                } finally {
                    response.close();
                }
                throw ex;
            }
        }
    }

注意上面的注释,我们接着上面的RedirectExec#execute()方法中调用的

继续点进去,往下面看具体的功能实现功能:

接着就进入了MainClientExec#execute()返回CloseableHttpResponse:

public CloseableHttpResponse execute(final HttpRoute route,final HttpRequestWrapper request,final HttpClientContext context,final HttpExecutionAware execAware) throws IOException, HttpException {
        // 验证为空
        Args.notNull(route, "HTTP route");
        Args.notNull(request, "HTTP request");
        Args.notNull(context, "HTTP context");
        // 看到Auth相关的直接跳过,这和认证(HTTPS等)相关,直接往下看;
        // 其实很多权限相关的基本上都是Auth开头的,比如shiro
        AuthState targetAuthState = context.getTargetAuthState();
        if (targetAuthState == null) {
            targetAuthState = new AuthState();
            context.setAttribute(HttpClientContext.TARGET_AUTH_STATE, targetAuthState);
        }
        AuthState proxyAuthState = context.getProxyAuthState();
        if (proxyAuthState == null) {
            proxyAuthState = new AuthState();
            context.setAttribute(HttpClientContext.PROXY_AUTH_STATE, proxyAuthState);
        }
        // 判断请求是不是带有实体,如果是就生成一个代理的Entity,然后set进request
        // 里面的实现就不展开了request.setEntity(new RequestEntityProxy(entity));
        if (request instanceof HttpEntityEnclosingRequest) {
            RequestEntityProxy.enhance((HttpEntityEnclosingRequest) request);
        }
        // 这是HttpClient中一个标识,它可以用来保证某些连接一段时间内只为某个用户所使用
		// 连接池的连接都有state,就是这里的userToken,所有的连接的state默认都是null
		// 当我们像向HttpClient连接池中请求userToken为“my”的连接时,它会返回一个连接,并将连接状态标记为my,这样同一个线程下次再来请求连接的时候,就会优先拿到这个连接
        Object userToken = context.getUserToken();
        // 这里是获取连接,下一篇博客专门来分析他
        final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);
        // execAware其实是原生的HttpRequest的一个拷贝
        // 我估计是防止请求取消中断是,用来返回原生的请求,不然HttpRequest经过很多包装有所变化了,这是我猜的。。。
        if (execAware != null) {
            if (execAware.isAborted()) {
                connRequest.cancel();
                throw new RequestAbortedException("Request aborted");
            }
            execAware.setCancellable(connRequest);
        }
        // 这是前面一些方法设置的环境和参数
        final RequestConfig config = context.getRequestConfig();

        final HttpClientConnection managedConn;
        try {
            final int timeout = config.getConnectionRequestTimeout();
            managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
        } catch(final InterruptedException interrupted) {
            Thread.currentThread().interrupt();
            throw new RequestAbortedException("Request aborted", interrupted);
        } catch(final ExecutionException ex) {
            Throwable cause = ex.getCause();
            if (cause == null) {
                cause = ex;
            }
            throw new RequestAbortedException("Request execution failed", cause);
        }
        // 这个地方将这个连接设置到上下文中
		// 使用者可以方便的在调用处获得本次执行所使用的连接(使用连接池的话就有作用了)
		// 只需要自己创建一个context,然后让HttpClient使用你的上下文即可
        context.setAttribute(HttpCoreContext.HTTP_CONNECTION, managedConn);

        if (config.isStaleConnectionCheckEnabled()) {
            // 连接是否打开
            if (managedConn.isOpen()) {
                this.log.debug("Stale connection check");
                // 跟上面一样,检查连接是否有效,为什么检查2次,我也不清楚
                // 方法比较重要,调用的是BHttpConnectionBase的#isStale(),里面涉及socket知识,有兴趣可以去看一看
                if (managedConn.isStale()) {
                    this.log.debug("Stale connection detected");
                    managedConn.close();
                }
            }
        }
         // 新建一个连接持有类,可能是因为操作连接的N多代码都是重复的,而且操作连接比较频繁
        final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn);
        try {
            if (execAware != null) {
                execAware.setCancellable(connHolder);
            }

            HttpResponse response;
            for (int execCount = 1;; execCount++) {
                // 如果请求过了一次,并且请求是不可重复的,就报错
                // 因为我们用的一般是StringEntity,是可重复的读的资源,所以不会报错
                // 有时候传文件时,用了InputStreamEntity就是不可重复读取的
                if (execCount > 1 && !RequestEntityProxy.isRepeatable(request)) {
                    throw new NonRepeatableRequestException("Cannot retry request " +
                            "with a non-repeatable request entity.");
                }

                if (execAware != null && execAware.isAborted()) {
                    throw new RequestAbortedException("Request aborted");
                }
                // 连接如果没有打开,则要建立网络连接(sockt连接)
                if (!managedConn.isOpen()) {
                    this.log.debug("Opening connection " + route);
                    try {
                        establishRoute(proxyAuthState, managedConn, route, request, context);
                    } catch (final TunnelRefusedException ex) {
                        if (this.log.isDebugEnabled()) {
                            this.log.debug(ex.getMessage());
                        }
                        response = ex.getResponse();
                        break;
                    }
                }
                final int timeout = config.getSocketTimeout();
                if (timeout >= 0) {
                    managedConn.setSocketTimeout(timeout);
                }

                if (execAware != null && execAware.isAborted()) {
                    throw new RequestAbortedException("Request aborted");
                }

                if (this.log.isDebugEnabled()) {
                    this.log.debug("Executing request " + request.getRequestLine());
                }
                // 认证相关的,没什么用,跳过。
                if (!request.containsHeader(AUTH.WWW_AUTH_RESP)) {
                    if (this.log.isDebugEnabled()) {
                        this.log.debug("Target auth state: " + targetAuthState.getState());
                    }
                    this.authenticator.generateAuthResponse(request, targetAuthState, context);
                }
                if (!request.containsHeader(AUTH.PROXY_AUTH_RESP) && !route.isTunnelled()) {
                    if (this.log.isDebugEnabled()) {
                        this.log.debug("Proxy auth state: " + proxyAuthState.getState());
                    }
                    this.authenticator.generateAuthResponse(request, proxyAuthState, context);
                }

                context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
                // 调用执行,意思是将请求,连接,上下文放入response加工工厂
                // 返回一个响应,下一篇博客主要分析这个方法
                response = requestExecutor.execute(request, managedConn, context);

                // 执行完了,判断这个连接是不是要保持长连接的
				// 如果是需要保持长连接的,则将其标记为可重用
                if (reuseStrategy.keepAlive(response, context)) {
                    // Set the idle duration of this connection
                    final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
                    if (this.log.isDebugEnabled()) {
                        final String s;
                        if (duration > 0) {
                            s = "for " + duration + " " + TimeUnit.MILLISECONDS;
                        } else {
                            s = "indefinitely";
                        }
                        this.log.debug("Connection can be kept alive " + s);
                    }
                    connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
                    connHolder.markReusable();
                } else {
                    connHolder.markNonReusable();
                }

              // 下面都没啥用省略了
              // 。。。。。。。
              // 。。。。。。。。
    }

本章就是分析了一下主要的流程和MainClientExec#execute,下章才是重点,怎么获取带连接的。也就是分析上面方法的

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值