org.apache.http.NoHttpResponseException: failed to respond-服务端响应异常

博客围绕Java使用Apache HTTP客户端多次上传文件时出现的异常展开。分析了服务端keep - alive超时断开连接、服务器负载过大丢弃链接两个原因,通过抓包分析明确问题。并给出客户端捕获异常重试、增加KeepAliveStrategy策略等解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

异常现象

多次上传文件时,偶尔会出现一次failed to respond异常,但是重试一次又正常了。

错误日志

在这里插入图片描述

原因分析

服务端keep-alive超时断开连接

spring resttemplate使用apache httpclient4.4 连接池。
主要是因为httpclient之前与服务端建立的连接断开,但是没有通知客户端或者客户端还没有收到通知,导致下次请求该服务时httpclient继续使用该连接导致报错。
服务端tomcat 默认的keep-alive timeout :60s,httpclient的连接池中设置的时间大于60s,连接空闲时间超过60s后再次从连接池拿出进行请求时,就会出现failed to respond异常。

服务器端负载过大,丢弃链接

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

抓包分析

可以通过tcp报文分析出,客户端和服务器连接的最大空闲时间,看看报文的交互过程。
在这里插入图片描述

  • 注意到图中第2882个包,服务器返回前一个请求的响应完成(10:16:25),到第2888个包(10:16:46)客户端发送的下一个请求包。直接有21s的空闲间隔,结合多个完整的连接请求断开的时间,可以判断出服务器在美国连接空闲20s后自动就会发起断开连接。
  • 客户端发出的第2888个包在收到服务器发送的2891个FIN包之前,客户端发送了2888和2889两个请求报文(客户端此时为收到服务器FIN报文)。但发送后,服务端发送的FIN包立刻就到了客户端,可以推测出,服务端在发送FIN报文前还没有收到客户端的请求报文,但是刚刚发送FIN报文却没有收到[FIN、ACK]报文,因此服务器无法判断是否是正常结束,所有就发出来RST包,关闭连接。
  • 客户端使用的httpclient的60s的长连接发送请求,使用的http1.1协议默认的keepalive的,同一个线程的多个请求可以复用同一个长连接。正是由于服务器发出的FIN包的时间与客户端在连接空闲了20s时扔使用这个连接发送数据时之间微秒的时间差(服务器发送了FIN报文,但是客户端还没有收到,但是客户端已经发送了请求数据包),所以导致出现NoHttpResponseException异常。

解决方案

客户端捕获异常重试(推荐)

推荐使用重发机制。
http请求使用重发机制,捕获NohttpResponseException的异常,重新发送请求,重发3次后还是失败才停止。由于服务器不知道客户端捕获到NohttpResponseException这个异常后,客户端是否已经关闭这个连接,因此每次重发都需要建立连接请求。新建连接不存在太长的空闲时间问题。 参考代码: https://blog.csdn.net/u010800970/article/details/79996698

客户端增加KeepAliveStrategy策略

配置keepAlive策略,目的是让客户端在服务端还没有发送断开连接报文时,客户端提前发送断开连接请求。
即客户端的keepAlive时间要配置的比服务端的keepAlive小(服务端默认:keepAlive 60s)。

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder
                .setConnectTimeout(Duration.ofMillis(1000)) // 连接建立超时时间
                .setReadTimeout(Duration.ofMillis(2000)) // 响应数据超时时间
                .requestFactory(this::requestFactory) // 请求工厂
                .build();
    }
@Bean
public HttpComponentsClientHttpRequestFactory requestFactory() {
    PoolingHttpClientConnectionManager connectionManager =
            new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
    connectionManager.setMaxTotal(200);
    connectionManager.setDefaultMaxPerRoute(20);

    CloseableHttpClient httpClient = HttpClients.custom()
            .setConnectionManager(connectionManager)
            .evictIdleConnections(30, TimeUnit.SECONDS)
            .disableAutomaticRetries()
            // 有 Keep-Alive 认里面的值,没有的话永久有效
            //.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
            // 换成自定义的
            .setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())
            .build();

    HttpComponentsClientHttpRequestFactory requestFactory =
            new HttpComponentsClientHttpRequestFactory(httpClient);

    return requestFactory;
}

/**
 * KeepAlive策略
 */
public class CustomConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {
    // 连接超过20s没有数据就主动断开与服务器的连接
    private final long DEFAULT_SECONDS = 20;

    @Override
    public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
        long timeOut = Arrays.asList(response.getHeaders(HTTP.CONN_KEEP_ALIVE))
                .stream()
                .filter(h -> StringUtils.equalsIgnoreCase(h.getName(), "timeout")
                        && StringUtils.isNumeric(h.getValue()))
                .findFirst()
                .map(h -> NumberUtils.toLong(h.getValue(), DEFAULT_SECONDS))
                .orElse(DEFAULT_SECONDS) * 1000;
        System.out.println(timeOut);
        return timeOut;
    }
}

}

客户端http连接不允许复用

不推荐使用,这样完全发挥不错线程池的优势。

HttpPost httpPost = new HttpPost(url);
// 设置不使用长连接
httpPost.setHeader("Connection", "close");

 
 

服务端修改配置

不推荐,服务同时使用默认keepAlive 60s,connection timeout 60s。

参考

https://blog.csdn.net/siantbaicn/article/details/80854528

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值