Java

Java HttpComponents源码阅读1

2020-07-14  本文已影响0人  Mhhhhhhy

Java HttpComponents源码阅读1
Java HttpComponents源码阅读2

HttpComponents一直是Java中HTTP请求的常用库,经常用来和OkHttp和Spring RestTemplate拿来比较,今天就来看下该库请求一次http请求会发生什么,由于代码过于庞大和精力时间有限,所以只会翻阅部分代码;

版本:
httpcomponents-client-4.5.x
httpcore-4.4.13

示例

public class Test {
    private static CloseableHttpClient DEFAULT_CLIENT;
    private static void method3() throws Exception {

        DEFAULT_CLIENT = HttpClients.custom().build();
        HttpGet get = new HttpGet("http://www.baidu.com");

        RequestConfig requestConfig = RequestConfig.custom()
                .setSocketTimeout(5000)
                .setConnectTimeout(5000)
                .setConnectionRequestTimeout(5000)
                .build();
        get.setConfig(requestConfig);

        HttpResponse response = DEFAULT_CLIENT.execute(get);
        if (response.getStatusLine().getStatusCode() == 200) {
            HttpEntity resEntity = response.getEntity();
            String message = EntityUtils.toString(resEntity, "utf-8");
            System.out.println(message);
        } else {
            System.out.println("http code != 200");
        }
    }
}

最简单的用法,用一个静态单例持有HTTPClient对象;

HttpClientBuilder

当我们调用HttpClientBuilder#build时,会走到一个长300多行的方法,里面将处理请求和响应所用到的类,根据用户的配置进行组装,大概用到了下面一些关键的类;

拦截器 作用
RequestTargetHost 负责处理HTTP1.1中HOST请求头
RequestUserAgent 负责处理HTTP中User-Agent请求头
RequestDefaultHeaders 负责处理HTTP用户自定义请求头
RequestContent 负责处理HTTP中和Content相关的请求头如Content-Type、Content-Length和Content-Encoding等
RequestClientConnControl 负责处理HTTP中和Keep-Alive请求头
RequestExpectContinue 负责处理HTTP中和Expect请求头
RequestAcceptEncoding 负责处理HTTP中和Accept-Encoding请求头
ResponseProcessCookies 负责处理HTTP中和Set-Cookie请求头

到这里整个build过程遇到的一些重要的类都有了个印象,这个方法的大概流程就是:创建构建InternalHttpClient所需要的对象,根据用户自定义来决定相关配置,并返回InternalHttpClient

内部其中一部分组成部分,其中任务链的起点是从RedirectExec开始;

执行HTTP请求的流程

InternalHttpClient#doExecute

HttpGet get = new HttpGet("http://api8.iwown.com");

首先是创建请求,HttpGet的结构如下

主要是下面几个组成

HttpResponse response = DEFAULT_CLIENT.execute(get);

当我们执行这行代码时,首先进入

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; // 默认的HttpGet、HttpPost等都是支持HttpExecutionAware操作的
    }
    try {
        // 包装一下request和target
        final HttpRequestWrapper wrapper = HttpRequestWrapper.wrap(request, target);
        // 设置httpclient context
        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); 
    }
}

首先是配置HttpClientContext,该类实际上是一个线程安全的map,主要负责将Http请求的一些header或者对象映射的属性储存,因为一个http请求所需要的来源于众多接口和类,放在一起方便存取;

然后是配置路由,将目标地址、端口号、是否使用代理、是否使用SSL等信息生成新的路由;

最后开始从任务链的第一个执行者执行任务,在这里是RedirectExec

RedirectExec#execute

public CloseableHttpResponse execute(
        final HttpRoute route,
        final HttpRequestWrapper request,
        final HttpClientContext context,
        final HttpExecutionAware execAware) throws IOException, HttpException {

    final List<URI> redirectLocations = context.getRedirectLocations();
    if (redirectLocations != null) {
        redirectLocations.clear();
    }

    final RequestConfig config = context.getRequestConfig();
    // 最大重定向次数,默认为50
    final int maxRedirects = config.getMaxRedirects() > 0 ? config.getMaxRedirects() : 50;
    HttpRoute currentRoute = route;
    HttpRequestWrapper currentRequest = request;
    for (int redirectCount = 0;;) {
        // 唤起下一个执行器执行任务
        final CloseableHttpResponse response = requestExecutor.execute(
                currentRoute, currentRequest, context, execAware);
        try {
            // 判断用户设置是否支持重定向 和
            // response返回的结果是否是重定向的状态码 如302
            if (config.isRedirectsEnabled() &&
                    this.redirectStrategy.isRedirected(currentRequest.getOriginal(), response, context)) {
                // 允许重定向且http响应为302或其他重定向标志
                if (!RequestEntityProxy.isRepeatable(currentRequest)) {
                    // 请求不支持 不可重复请求 重定向,直接返回response
                    return response;
                }
                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()) {
                    // 如果header被重置了,补充所有之前请求的headers
                    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);
                }

                // 如果重定向到另一个主机则重置虚拟主机和认证状态
                if (!currentRoute.getTargetHost().equals(newTarget)) {
                    final AuthState targetAuthState = context.getTargetAuthState();
                    if (targetAuthState != null) {
                        targetAuthState.reset();
                    }
                    final AuthState proxyAuthState = context.getProxyAuthState();
                    if (proxyAuthState != null && proxyAuthState.isConnectionBased()) {
                        proxyAuthState.reset();
                    }
                }
                // 重新选择路由
                currentRoute = this.routePlanner.determineRoute(newTarget, currentRequest, context);
                // 确保entity被读取,同时关闭inputstream
                EntityUtils.consume(response.getEntity());
                response.close();
            } else {
                return response;
            }
        } catch (final RuntimeException ex) {
            response.close();
            throw ex;
        } catch (final IOException ex) {
            response.close();
            throw ex;
        } catch (final HttpException ex) {
            // 出现http协议异常,底层的连接可能被挽救复用;
            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就是负责http请求重定向的,主要做了以下几件事

  1. 开启一个循环,次数是用户配置的最大重定向数maxRedirects,在每次循环中执行下一个执行器的任务,在这里也就是RetryExec,等待执行完拿到response;
  2. 如果配置允许重定向,则进入下面步骤,否则直接返回response;
  3. 解析response拿到http status code,如果状态码是302或者301等重定向的状态码,则进行重定向操作,重新生成一个新的请求和路由,在maxRedirects内重新执行RetryExec#execute
  4. 如果遇到异常需要把请求关闭,关闭请求的过程下面在看,同时把异常向上抛出;

RetryExec#execute

public CloseableHttpResponse execute(
        final HttpRoute route,
        final HttpRequestWrapper request,
        final HttpClientContext context,
        final HttpExecutionAware execAware) throws IOException, HttpException {
    final Header[] origheaders = request.getAllHeaders();
    for (int execCount = 1;; execCount++) {
        try {
            // 先执行下一个执行器任务
            return this.requestExecutor.execute(route, request, context, execAware);
        } catch (final IOException ex) {
            if (execAware != null && execAware.isAborted()) {
                // isAborted 意味着请求已经被关闭
                this.log.debug("Request has been aborted");
                throw ex;
            }
            if (retryHandler.retryRequest(ex, execCount, context)) {
                if (!RequestEntityProxy.isRepeatable(request)) {
                    throw new NonRepeatableRequestException("Cannot retry request " +
                            "with a non-repeatable request entity", ex);
                }
                request.setHeaders(origheaders);
            } else {
                if (ex instanceof NoHttpResponseException) {
                    final NoHttpResponseException updatedex = new NoHttpResponseException(
                            route.getTargetHost().toHostString() + " failed to respond");
                    updatedex.setStackTrace(ex.getStackTrace());
                    throw updatedex;
                }
                throw ex;
            }
        }
    }
}

RetryExec负责决定是否应该重新执行由于IOException导致的请求失败,比如再流被关闭后继续读取从流中读取数据而抛出的异常;

在执行后续的任务链上面的任务时遇到了异常后,如果请求被关闭则直接抛出异常,否则交由HttpRequestRetryHandler来判断是否允许重试请求,默认实现为DefaultHttpRequestRetryHandler,主要的判断逻辑是依据retryCountrequestSentRetryEnabledretryCount默认为3次,requestSentRetryEnabled表示请求发生成功后是否允许再被重试,默认为false;

DEFAUTL_HTTP_CLIENT = HttpClients.custom()
         .setRetryHandler(new DefaultHttpRequestRetryHandler(0, false))//forbidden retry
         .build();

可以通过上面方式修改默认配置;

ProtocolExec#exec

public CloseableHttpResponse execute(
        final HttpRoute route,
        final HttpRequestWrapper request,
        final HttpClientContext context,
        final HttpExecutionAware execAware) throws IOException,
        HttpException {

    final HttpRequest original = request.getOriginal();
    URI uri = null;
    if (original instanceof HttpUriRequest) {
        uri = ((HttpUriRequest) original).getURI();
    } else {
        final String uriString = original.getRequestLine().getUri();
        try {
            uri = URI.create(uriString);
        } catch (final IllegalArgumentException ex) {
        }
    }
    request.setURI(uri);

    // 更新request中的uri属性,即是访问的资源路径
    rewriteRequestURI(request, route, context.getRequestConfig().isNormalizeUri());

    final HttpParams params = request.getParams();
    HttpHost virtualHost = (HttpHost) params.getParameter(ClientPNames.VIRTUAL_HOST);
    // 处理virtualhost
    if (virtualHost != null && virtualHost.getPort() == -1) {
        final int port = route.getTargetHost().getPort();
        if (port != -1) {
            virtualHost = new HttpHost(virtualHost.getHostName(), port,
                    virtualHost.getSchemeName());
        }
    }

    HttpHost target = null;
    if (virtualHost != null) {
        target = virtualHost;
    } else {
        if (uri != null && uri.isAbsolute() && uri.getHost() != null) {
            target = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
        }
    }
    if (target == null) {
        target = request.getTarget();
    }
    if (target == null) {
        target = route.getTargetHost();
    }

    if (uri != null) {
        final String userinfo = uri.getUserInfo();
        if (userinfo != null) {
            // 处理和用户验证的信息
            CredentialsProvider credsProvider = context.getCredentialsProvider();
            if (credsProvider == null) {
                credsProvider = new BasicCredentialsProvider();
                context.setCredentialsProvider(credsProvider);
            }
            credsProvider.setCredentials(
                    new AuthScope(target),
                    new UsernamePasswordCredentials(userinfo));
        }
    }

    // 将http请求相关的属性储存到这个请求上下文中
    context.setAttribute(HttpCoreContext.HTTP_TARGET_HOST, target);
    context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
    context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
    // 处理http请求拦截器
    this.httpProcessor.process(request, context);
    // MainClientExec发送该请求,注意这里并不会捕捉异常
    final CloseableHttpResponse response = this.requestExecutor.execute(route, request,
            context, execAware);
    try {
        // Run response protocol interceptors
        context.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
        // 处理http响应拦截器
        this.httpProcessor.process(response, context);
        return response;
    } catch (final RuntimeException ex) {
        response.close();
        throw ex;
    } catch (final IOException ex) {
        response.close();
        throw ex;
    } catch (final HttpException ex) {
        response.close();
        throw ex;
    }
}

ProtocolExec是负责实现HTTP规范要求,来看下这个方法的执行流程;

  1. 解析出请求的uri,比如http://www.baidu.com/index.html?a=1,uri是/index.html?a=1
  2. 将本次请求所需要的信息储存到请求上下文中,一个http请求对应一个context;
  3. 处理http请求拦截器,拦截器种类在上面,最主要就是处理http请求的header;
  4. 执行下一个任务链也就是MainClientExec的任务,等待返回请求,注意这里并不会捕捉MainClientExec#exec的任何异常,通常来说这里只会返回IOExecption,如果发生异常直接抛给上一个执行器;
  5. 处理http响应的拦截器,默认有两个
拦截器 作用
ResponseProcessCookies 在HTTP响应中接收到的响应cookie中包含的数据填充当前的CookieStore
ResponseContentEncoding 负责处理响应内容编码,通常是自定义的解析,使用方法是在builder中设置setContentDecoderRegistry

组件

在研究MainClientExec之前需要先了解一下该执行器遇到的各种组件

SessionInputBufferImpl

似于InputStream类,用于阻塞连接的会话输入缓冲区,这个类在一个内部字节数组中缓冲输入数据,以获得最佳的输入性能。

使用该类时需要和一个InputStream进行绑定后才能从流中读取数据,本质上还是通过SocketInputStream#read来读取,第一次读取的时候会读取最大长度的数据并缓存起来,最大长度设置是在构造器中指定的,追溯到源头的话是通过ConnectionConfig设置的,默认大小为8*1024;

SessionInputBufferImpl提供的最核心的功能就是将数据转换成http协议的格式,比如readLine可以按照HTTP标准分隔符来区别出HTTP每一行;

SessionOutputBufferImpl

负责输出缓冲区,将HTTP数据写入内部缓存的buffer,直到所有数据写完或者缓冲区满了,就会手动SocketOutputStream#write将数据写入流中;

HttpMessageWriter

负责HTTP数据的写入,默认实现是DefaultHttpRequestWriter,内部持有SessionOutputBufferImpl的HTTP请求写入器;

HttpMessageParser

HTTP消息解析器,默认实现为DefaultHttpResponseParser,内部持有SessionInputBuffer,本质上就是解析从流中读取的数据并解析成一个对象代表HTTP响应;

BHttpConnectionBase

这个类充当所有HttpConnection的基础类,实现并提供客户端和服务器HTTP连接通用的功能,基本上是对Socket的操作的封装,外部所有对HTTP连接的打开、关闭最终的反应也就是这里的open()close()

HttpClientConnection

代表了一个客户端的HTTP连接过程,可用于发送请求和接收响应;默认实现为DefaultBHttpClientConnectionDefaultBHttpClientConnection是在BHttpConnectionBase的基础上扩展实现的,除了继承BHttpConnectionBase的操作外还实现HttpClientConnection接口,提供了直接发送和获取请求头和请求体的封装,还提供了发送请求、收到响应等钩子函数;

ManagedHttpClientConnection

表示其状态和生命周期由连接管理器管理的托管连接,同时整合了HttpClientConnectionHttpInetConnection

DefaultManagedHttpClientConnection和LoggingManagedHttpClientConnection

ManagedHttpClientConnectionHttpContext的默认实现,同时也继承了DefaultBHttpClientConnection类,该类最重要的功能是提供了一个线程安全map来缓存HTTP请求上下文,也就是上面代码看到的context#setAttribute

LoggingManagedHttpClientConnectionDefaultManagedHttpClientConnection的基础上提供了日志功能;

ManagedHttpClientConnection接口通常最终实现的内部结构如图所示,一个ManagedHttpClientConnection对象对应着一个HTTP连接;

PoolEntry和CPoolEntry

PoolEntry是一个包含了一个HTTP连接对象和其路由的数据,同时在构建时提供参数可以设置过期时间,并提供外界获取当前连接的方法;

CPoolEntry扩展了PoolEntry,在这基础上增加了对路由是否完成的判断,默认是创建CPoolEntry时捆绑了一个LoggingManagedHttpClientConnection对象和HttpRoute对象;

RouteSpecificPool

RouteSpecificPool由路由与队列组成一个特定路由的连接池,结构如下

当我们创建一个连接对象时(LoggingManagedHttpClientConnection),会将其与请求的路由一起组成CPoolEntry对象,然后放到RouteSpecificPool池中的租用集合(leased),代表着该连接被租用了,当连接使用完毕后会将其从租用池中移除;

available列表是储存的是可重用的连接;
pending是将因获取连接或者创建连接失败的请求任务缓存到这里,等到有空闲连接或者可创建连接时再取出来执行请求;

ConnFactory和HttpConnectionFactory

ConnFactory用于阻塞连接池创建的工厂,默认实现是PoolingHttpClientConnectionManager的内部类InternalConnectionFactory

HttpConnectionFactory是用于创建HttpConnection的工厂,默认实现是ManagedHttpClientConnectionFactory

这两个类的关系如下

InternalConnectionFactory#create本质上调用的还是ManagedHttpClientConnectionFactory#create,创建出来的连接是LoggingManagedHttpClientConnection对象;

ConnPool和ConnPoolControl

ConnPool接口表示一个共享池连接,可以从其租借并释放回其;
ConnPoolControl接口来控制连接池的运行时属性,例如最大连接总数或每个路由允许的最大连接数;这两个接口的默认实现类为AbstractConnPool

AbstractConnPool

该抽象类提供方法让外部从HTTP连接池中租借指定路由的连接,但是该类自己本身不维护执行线程,而是提供一个Future接口让调用者自己决定请求连接的时机;

CPool扩展了AbstractConnPool抽象类,在构造时增加了连接存活时间属性,当指定了存活时间后,从该池中的创建所有连接的存活时间都是该值;


CPool中也有租用队列、可用队列和等待队列用来储存请求任务或者连接,但与RouteSpecificPool不同的是前者保存的是所有路由的连接,而后者只储存特定路由的连接;
HttpClientConnectionOperator

负责执行建立Socket连接操作,默认实现是DefaultHttpClientConnectionOperator

里面包含一组ConnectionSocketFactory注册表、DNS解析器和端口解析器,根据HTTP或者HTTPS来选择PlainConnectionSocketFactory或者SSLConnectionSocketFactory来创建Socket对象并连接,同时将Socket和LoggingManagedHttpClientConnection对象的socket关联起来;

PoolingHttpClientConnectionManager

PoolingHttpClientConnectionManager是前面提及过HttpClientConnectionManage的默认实现,承担了最核心的连接池管理操作;内部维护了一个HttpClientConnections池,并且能够为来自多个执行线程的连接请求提供服务。连接是按请求路由的分隔的,对于已经在池中拥有可用的持久连接的路由请求,将通过从池中租借连接而不是创建一个新的连接。

PoolingHttpClientConnectionManager在每个路由和总数上维护最大的连接限制。默认情况下,该实现将为每个给定路由创建不超过2个并发连接,并且在池中创建总共不超过20个连接。

总结下来各个组件所处的位置如下

上一篇下一篇

猜你喜欢

热点阅读