httpClient4.5.9 连接池及重试配置处理

 

近期改进项目中的httpClient请求,由原来的复用单连接,改为使用连接池,解决并发调用的问题,最近又报出以下异常。

e:org.apache.http.NoHttpResponseException: 【ip:port】 failed to respond at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:141) at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56) at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:259) at org.apache.http.impl.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:163)

经排查,是服务器每秒5次调用服务接口,猜测可能是服务器端负载过大,导致在收到请求后无法处理的结果,查阅资料

其他博友的说法是:

博友1:https://www.cnblogs.com/756623607-zhang/p/10960782.html

在某些情况下,通常是在高负载下,web服务器可能能够接收请求,但无法处理它们。缺乏足够的资源(比如工作线程)就是一个很好的例子。这可能导致服务器在不给出任何响应的情况下断开到客户机的连接。HttpClient在遇到这种情况时抛出NoHttpResponseException。在大多数情况下,重试使用NoHttpResponseException失败的方法是安全的。

官网给出的解决办法就是:重试调用失败的方法

博友2:https://blog.csdn.net/liubenlong007/article/details/78180333

当服务器端由于负载过大等情况发生时,可能会导致在收到请求后无法处理(比如没有足够的线程资源),会直接丢弃链接而不进行处理。此时客户端就回报错:NoHttpResponseException。
官方建议出现这种情况时,可以选择重试。但是重试一定要限制重试次数,避免雪崩。

 

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.http.Consts;
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NoHttpResponseException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;


@Service
@Slf4j
public class HttpService {

    private static int MAX_FAIL_RETRY_COUNT = 3;
    private HttpClient httpClient;


    //优化并发场景 HttpClient 单线程问题
    @PostConstruct
    public void init2() throws Exception {

        try {
            // 1. 根据证书来调用
            SSLContext sslContext = sslContent();
            SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);

            //存活的策略
            ConnectionKeepAliveStrategy myStrategy = ka();

            // 设置协议http和https对应的处理socket链接工厂的对象
            Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                    .register("http", PlainConnectionSocketFactory.INSTANCE)
                    .register("https", sslConnectionSocketFactory)
                    .build();

            //创建ConnectionManager,添加Connection配置信息
            //最大连接数
            //例如默认每路由最高50并发,具体依据业务来定,一般和setMaxTotal 一致
            PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
            connectionManager.setMaxTotal(200);
            connectionManager.setDefaultMaxPerRoute(200);
            //检测有效连接的间隔 2s
            connectionManager.setValidateAfterInactivity(2000);

            RequestConfig requestConfig = RequestConfig.custom()
                    //.setConnectionRequestTimeout(6000)//设定连接服务器超时时间
                    //.setConnectTimeout(2000)//设定从连接池获取可用连接的时间
                    //.setSocketTimeout(6000)//设定获取数据的超时时间
                    .build();

            ConnectionConfig connectionConfig = ConnectionConfig.custom()
                    .setBufferSize(4128)
                    .build();

            httpClient = HttpClients.custom()
                    .setConnectionManager(connectionManager)
                    .setKeepAliveStrategy(myStrategy)
                    .setDefaultRequestConfig(requestConfig)
                    .setSSLHostnameVerifier(new NoopHostnameVerifier())
                    //不使用这种方式,不方便看日志,使用下面自定义的retry
                    //.setRetryHandler(new DefaultHttpRequestRetryHandler(3,true))
                    .setRetryHandler(new MyRetryHandler())
                    .setDefaultConnectionConfig(connectionConfig)
                    .build();

            log.info("注册https证书成功");
        } catch (Exception e) {
            log.error("注册https证书失败,e:{}", e);
        }
    }


  
    public String postJSON(String url, String json) throws IOException {
        InputStream inStream = null;
        HttpEntity entity = null;
        try {
            HttpPost httpPost = new HttpPost(url);
            httpPost.setEntity(new StringEntity(json, Consts.UTF_8));
            HttpResponse httpResponse = httpClient.execute(httpPost);
            entity = httpResponse.getEntity();

            if (httpResponse.getStatusLine().getStatusCode() == 200) {
                inStream = entity.getContent();
                return IOUtils.toString(inStream);
            } else {
                throw new IOException("Unexpected code " + httpResponse);
            }
        } finally {
            EntityUtils.consume(entity);
            if (inStream != null) {
                inStream.close();
            }
        }
    }


    /**
     * 请求重试处理器
     */
    private static class MyRetryHandler implements HttpRequestRetryHandler {

        @Override
        public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {

            HttpClientContext clientContext = HttpClientContext.adapt(context);
            HttpRequest request = clientContext.getRequest();
            String uri = request.getRequestLine().getUri();

            if (executionCount >= MAX_FAIL_RETRY_COUNT) {
                log.warn("{}-{}重试次数大于等于3次", uri, executionCount);
                return false;
            }

            boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
            if (idempotent) {
                // 如果请求被认为是幂等的,则重试
                log.warn("幂等接口重试:{},次数:{}", uri, executionCount);
                return true;
            }

            //NoHttpResponseException 重试
            if (exception instanceof NoHttpResponseException) {
                log.warn("NoHttpResponseException 异常重试,接口:{},次数:{} ", uri, executionCount);
                return true;
            }

            //连接超时重试
            if (exception instanceof ConnectTimeoutException) {
                log.warn("ConnectTimeoutException异常重试 ,接口:{},次数:{} ", uri, executionCount);
                return true;
            }

            // 响应超时不重试,避免造成业务数据不一致
            if (exception instanceof SocketTimeoutException) {
                return false;
            }

            if (exception instanceof InterruptedIOException) {
                // 超时
                return false;
            }
            if (exception instanceof UnknownHostException) {
                // 未知主机
                return false;
            }

            if (exception instanceof SSLException) {
                // SSL handshake exception
                return false;
            }

            return false;
        }
    }

    private ConnectionKeepAliveStrategy ka() {

        //就是通过Keep-Alive头信息中,获得timeout的值,作为超时时间;单位毫秒;
        //如请求头中 Keep-Alive: timeout=5, max=100
        //DefaultConnectionKeepAliveStrategy strategy = DefaultConnectionKeepAliveStrategy.INSTANCE;
        ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
            @Override
            public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
                HeaderElementIterator it = new BasicHeaderElementIterator
                        (response.headerIterator(HTTP.CONN_KEEP_ALIVE));
                while (it.hasNext()) {
                    HeaderElement he = it.nextElement();
                    String param = he.getName();
                    String value = he.getValue();
                    if (value != null && param.equalsIgnoreCase("timeout")) {
                        return Long.parseLong(value) * 1000;
                    }
                }
                //如果没有约定,则默认定义时长为60s
                return 60 * 1000;
            }
        };
        return myStrategy;
    }

    private SSLContext sslContent() throws Exception {

        SSLContext sslcontext = null;
        InputStream instream = null;
        KeyStore trustStore = null;
        try {
            trustStore = KeyStore.getInstance("jks");
            instream = new ClassPathResource("config/release/test.jks").getInputStream();
            trustStore.load(instream, "123456".toCharArray());

            // 相信自己的CA和所有自签名的证书
            TrustStrategy ts = new TrustStrategy() {
                @Override
                public boolean isTrusted(X509Certificate[] chain, String authType) {
                    return true;
                }
            };
            sslcontext = SSLContexts.custom()
                    //.loadTrustMaterial(trustStore, new TrustSelfSignedStrategy())
                    .loadTrustMaterial(trustStore, ts)
                    .build();
        } finally {
            try {
                instream.close();
            } catch (IOException e) {
            }
        }

        return sslcontext;
    }
}

推荐另一位博友的连接池配置:

https://blog.csdn.net/Kincym/article/details/81318492

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
配置HttpClient连接池,你需要使用Apache HttpClient库。下面是一个简单的示例,展示了如何配置连接池: ```java import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; public class HttpClientPoolExample { public static void main(String[] args) { // 创建连接池配置 RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5000) // 连接超时时间 .setSocketTimeout(5000) // 数据传输超时时间 .build(); // 创建连接池 PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); connManager.setMaxTotal(100); // 设置连接池最大连接数 // 创建HttpClient实例 CloseableHttpClient httpClient = HttpClientBuilder.create() .setDefaultRequestConfig(requestConfig) // 设置默认的请求配置 .setConnectionManager(connManager) // 设置连接池 .build(); // 使用HttpClient发送请求 HttpHost targetHost = new HttpHost("www.example.com", 80, "http"); HttpGet request = new HttpGet("/"); try (CloseableHttpResponse response = httpClient.execute(targetHost, request)) { // 处理响应 // ... } catch (IOException e) { e.printStackTrace(); } } } ``` 在上面的示例中,我们创建了一个连接池配置(RequestConfig),其中设置了连接超时时间和数据传输超时时间。然后,我们创建了一个连接池(PoolingHttpClientConnectionManager),并设置了最大连接数。最后,我们使用HttpClientBuilder来构建HttpClient实例,并将连接池和请求配置设置到其中。 这只是一个简单的示例,你可以根据自己的需求进行更详细的配置,例如设置路由最大连接数、设置代理等。另外,记得在使用完HttpClient后关闭它,以释放资源。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值