HttpClient 理解与实战

HTTPClient 理解与实战

推荐使用 HttpClient 的主要优点:

在 keep-alive 时间内,可以使用同一个 tcp 连接发起多次 http 请求。
如果不使用连接池,在大并发的情况下,每次连接都会打开一个端口,使系统资源很快耗尽,无法建立新的连接,可以限定最多打开的端口数。
httpClient 维护着两个 Set,leased(被占用的连接集合) 和 avaliabled(可用的连接集合) 两个集合,释放连接就是将被占用连接放到可用连接里面。

HTTPClient 组成图

HttpClient 经典配置详解

import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.DnsResolver;
import org.apache.http.conn.HttpConnectionFactory;
import org.apache.http.conn.ManagedHttpClientConnection;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.client.*;
import org.apache.http.impl.conn.DefaultHttpResponseParserFactory;
import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.conn.SystemDefaultDnsResolver;
import org.apache.http.impl.io.DefaultHttpRequestWriterFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.util.concurrent.TimeUnit;


@Configuration
public class RestTemplateConfig {

    private static final Logger LOGGER = LoggerFactory.getLogger(RestTemplateConfig.class);

    @Bean
    public RestTemplate restTemplate() {
        ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(getHttpClient());
        return new RestTemplate(requestFactory);
    }

    // httpclient 4.5.2使用连接池的经典配置
    private CloseableHttpClient getHttpClient() {
        // 注册访问协议相关的Socket工厂
        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.INSTANCE)
                .register("https", SSLConnectionSocketFactory.getSocketFactory())
                .build();

        // HttpConnectionFactory:配置写请求/解析响应处理器
        HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connectionFactory = new ManagedHttpClientConnectionFactory(
                DefaultHttpRequestWriterFactory.INSTANCE,
                DefaultHttpResponseParserFactory.INSTANCE
        );

        // DNS解析器
        DnsResolver dnsResolver = SystemDefaultDnsResolver.INSTANCE;
        // 创建连接池管理器
        PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager(socketFactoryRegistry, connectionFactory, dnsResolver);
        // 设置默认的socket参数
        manager.setDefaultSocketConfig(SocketConfig.custom().setTcpNoDelay(true).build());
        // 设置最大连接数。高于这个值时,新连接请求,需要阻塞,排队等待
        manager.setMaxTotal(300);
        // 路由是对MaxTotal的细分。
        // 每个路由实际最大连接数默认值是由DefaultMaxPerRoute控制。
        // MaxPerRoute设置的过小,无法支持大并发:ConnectionPoolTimeoutException:Timeout waiting for connection from pool
        // 每个路由的最大连接
        manager.setDefaultMaxPerRoute(200);
        // 在从连接池获取连接时,连接不活跃多长时间后需要进行一次验证,默认为2s
        manager.setValidateAfterInactivity(5 * 1000);

        // 配置默认的请求参数
        RequestConfig defaultRequestConfig = RequestConfig.custom()
            	// 连接超时设置为2s。建立TCP连接的时间
                .setConnectTimeout(2 * 1000)
                // 等待数据超时设置为5s。等待服务器返回数据的时间
                .setSocketTimeout(5 * 1000)
             	// 从连接池获取连接的等待超时时间设置为2s
                .setConnectionRequestTimeout(2 * 1000)
             	// 设置代理
                .setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.0.2", 1234)))
                .build();

        CloseableHttpClient closeableHttpClient = HttpClients.custom()
                .setConnectionManager(manager)
            	// 连接池不是共享模式,这个共享是指与其它httpClient是否共享
                .setConnectionManagerShared(false)
            	// 定期回收空闲连接
                .evictIdleConnections(60, TimeUnit.SECONDS)
            	// 回收过期连接
                .evictExpiredConnections()
				// 连接存活时间,如果不设置,则根据长连接信息决定
                .setConnectionTimeToLive(60, TimeUnit.SECONDS)
           	 	// 设置默认的请求参数
                .setDefaultRequestConfig(defaultRequestConfig)
            	// 连接重用策略
              .setConnectionReuseStrategy(DefaultConnectionReuseStrategy.INSTANCE)
            	// 长连接配置,即获取长连接生产多长时间
              .setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
            	// 设置重试次数,默认为3次;当前是禁用掉
               .setRetryHandler(new DefaultHttpRequestRetryHandler(0, false))
               .build();

        /**
         *JVM停止或重启时,关闭连接池释放掉连接
         */
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                try {
                    LOGGER.info("closing http client");
                    closeableHttpClient.close();
                    LOGGER.info("http client closed");
                } catch (IOException e) {
                    LOGGER.error(e.getMessage(), e);
                }
            }
        });
        return closeableHttpClient;
    }
}

HTTPClient 使用Demo

/**
 *普通的GET请求
 */
public class DoGET {
    public static void main(String[] args) throws Exception {
        // 创建Httpclient对象
        CloseableHttpClient httpclient = HttpClients.createDefault();
        // 创建http GET请求
        HttpGet httpGet = new HttpGet("http://www.baidu.com");
        CloseableHttpResponse response = null;
        try {
            // 执行请求
            response = httpclient.execute(httpGet);
            // 判断返回状态是否为200
            if (response.getStatusLine().getStatusCode() == 200) {
                //请求体内容
                String content = EntityUtils.toString(response.getEntity(), "UTF-8");
                //内容写入文件
                FileUtils.writeStringToFile(new File("E:\\devtest\\baidu.html"), content, "UTF-8");
                System.out.println("内容长度:"+content.length());
            }
        } finally {
            if (response != null) {
                response.close();
            }
            //相当于关闭浏览器
            httpclient.close();
        }
    }
}





import org.apache.commons.io.FileUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.File;

/**
 * 常规post请求
 *    可以设置Header来伪装浏览器请求
 */
public class DoPOST {
    public static void main(String[] args) throws Exception {
        // 创建Httpclient对象
        CloseableHttpClient httpclient = HttpClients.createDefault();
        // 创建http POST请求
        HttpPost httpPost = new HttpPost("http://www.oschina.net/");
        //伪装浏览器请求
        httpPost.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36");
        CloseableHttpResponse response = null;
        try {
            // 执行请求
            response = httpclient.execute(httpPost);
            // 判断返回状态是否为200
            if (response.getStatusLine().getStatusCode() == 200) {
                String content = EntityUtils.toString(response.getEntity(), "UTF-8");
                //内容写入文件
                FileUtils.writeStringToFile(new File("E:\\devtest\\oschina.html"), content, "UTF-8");
                System.out.println("内容长度:"+content.length());
            }
        } finally {
            if (response != null) {
                response.close();
            }
            httpclient.close();
        }
    }
}

HttpClient 可能出现的异常

connectionRequestTimeout:从连接池中获取连接的超时时间,超过该时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
connectTimeout:连接上服务器(握手成功)的时间,超出该时间抛出connect timeout
socketTimeout:服务器返回数据(response)的时间,超过该时间抛出read timeout

以上3个超时相关的参数如果未配置,默认为-1,意味着无限大,就是一直阻塞等待!
HttpClients在我们没有指定连接工厂的时候默认使用的是连接池工厂org.apache.http.impl.conn.PoolingHttpClientConnectionManager.PoolingHttpClientConnectionManager(Registry<ConnectionSocketFactory>)

HTTPClient 连接池的连接保持、超时、和失效机制

  1. 如何判断连接是否可以保持?

检查返回response报文头的Transfer-Encoding字段,若该字段值存在且不为chunked,则连接不保持,直接关闭。
检查返回的response报文头的Content-Length字段,若该字段值为空或者格式不正确(多个长度,值不是整数),则连接不保持,直接关闭。
检查返回的response报文头的Connection字段(若该字段不存在,则为Proxy-Connection字段)值:
如果这俩字段都不存在,则1.1版本默认为保持, 1.0版本默认为连接不保持,直接关闭。
如果字段存在,若字段值为close 则连接不保持,直接关闭;若字段值为keep-alive则连接标记为保持。

  1. 保持多长时间?

保持时间计时开始时间为连接交换至连接池的时间。 保持时长计算规则为:获取response中 Keep-Alive字段中timeout值,若该存在,则保持时间为 timeout值*1000,单位毫秒。若不存在,则连接保持时间设置为-1,表示为无穷。

  1. 保持过程中如何保证连接没有失效?

很难保证。传统阻塞I/O模型,只有当I/O操做的时候,socket才能响应I/O事件。当TCP连接交给连接管理器后,它可能还处于“保持连接”的状态,但是无法监听socket状态和响应I/O事件。如果这时服务器将连接关闭的话,客户端是没法知道这个状态变化的,从而也无法采取适当的手段来关闭连接。

针对这种情况,HttpClient采取一个策略,通过一个后台的监控线程定时的去检查连接池中连接是否还“新鲜”,如果过期了,或者空闲了一定时间则就将其从连接池里删除掉。ClientConnectionManager提供了 closeExpiredConnections和closeIdleConnections两个方法。

引用文章:

https://www.cnblogs.com/softidea/p/6964347.html

https://www.cnblogs.com/fnlingnzb-learner/p/10832471.html

posted @ 2022-03-19 15:49  生活是很好玩的  阅读(409)  评论(0编辑  收藏  举报