You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Java中Apache HttpClient与Java 11 HttpClient循环请求时触发IOException的问题排查及解决

问题分析与解决方案

看起来你遇到的是典型的连接池未复用 + 服务端连接数限制导致的连接重置问题,而且104这个固定数字也能佐证这一点——先给你拆解原因,再给你具体的代码修复方案:

为什么会出现固定104次请求后报错?

你的两段代码有一个共同的核心问题:每次请求都新建一个HttpClient实例,而不是复用全局的单例。

每次新建HttpClient都会创建新的TCP连接,当这些请求完成后,连接会进入TIME_WAIT状态(TCP协议的特性,防止迟到的数据包干扰新连接)。当短时间内创建的连接数量达到服务端的并发连接限制(或者客户端本地的临时端口耗尽,不过104这个数更偏向服务端的阈值设置),服务端就会主动重置后续的连接,也就是你看到的Connection reset/connection reset by peer错误。

而手动请求时,你是单个连接,远没达到服务端的阈值,所以能正常响应;当一批错误请求后,之前的连接逐渐从TIME_WAIT状态释放,服务端的连接数又回到阈值以下,所以请求又能正常进行了。

解决方案:复用HttpClient + 连接池优化

1. Apache HttpClient 修复方案

Apache HttpClient是线程安全的,应该全局复用一个实例,并且使用连接池来管理连接,避免频繁创建销毁连接:

// 全局单例的HttpClient和连接池
private static CloseableHttpClient httpClient;

static {
    CredentialsProvider provider = new BasicCredentialsProvider();
    provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(env.getProperty("user"), env.getProperty("password")));
    
    // 配置连接池
    PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
    // 设置最大总连接数
    connectionManager.setMaxTotal(200);
    // 设置每个路由的最大连接数(针对目标服务的并发连接数)
    connectionManager.setDefaultMaxPerRoute(50);
    
    httpClient = HttpClientBuilder.create()
            .setDefaultCredentialsProvider(provider)
            .setConnectionManager(connectionManager)
            // 配置连接存活时间,复用连接
            .setKeepAliveStrategy((response, context) -> 60000) // 60秒
            .build();
}

public String ApacheHTTP_getIceCatSpecifications(Integer Id) {
    String list = ""; // 原代码ArrayList<String>定义为String是笔误,这里对应调整
    HttpGet httpGet = new HttpGet("http://somesite/" + Id + ".xml");
    
    try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
        final HttpEntity entity = response.getEntity();
        if (entity != null) {
            try (InputStream inputStream = entity.getContent()) {
                // 从inputStream读取数据到list
            }
        }
    } catch (ClientProtocolException e) {
        log.error("协议错误", e);
    } catch (IOException e) {
        log.error("IO异常,产品ID:{}", Id, e);
        // 针对连接重置异常添加重试逻辑
        if (e.getCause() instanceof SocketException && e.getMessage().contains("Connection reset")) {
            try {
                Thread.sleep(1000); // 间隔1秒重试一次
                return ApacheHTTP_getIceCatSpecifications(Id);
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
        }
    }
    return list;
}

2. Java 11 HttpClient 修复方案

Java 11的HttpClient同样是线程安全的,全局复用一个实例即可,同时可以配置连接池和超时参数:

// 全局单例的HttpClient
private static HttpClient httpClient;

static {
    httpClient = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_1_1)
            .authenticator(new Authenticator() {
                @Override
                protected PasswordAuthentication getPasswordAuthentication() {
                    return new PasswordAuthentication(
                            env.getProperty("user"),
                            env.getProperty("password").toCharArray());
                }
            })
            .connectTimeout(Duration.ofSeconds(10)) // 连接超时
            .build();
}

public List<String> getSpecifications_JAVA11(Integer Id) {
    List<String> specificationList = new ArrayList<>();
    HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("http://somesite/" + Id + ".xml"))
            .timeout(Duration.ofSeconds(15)) // 请求超时
            .build();
    
    try {
        HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
        try (InputStream inputStream = response.body()) {
            // 从inputStream读取数据到specificationList
        }
    } catch (IOException e) {
        e.printStackTrace();
        // 针对连接重置异常添加重试
        if (e.getMessage().contains("connection reset by peer")) {
            try {
                Thread.sleep(1000); // 间隔1秒重试
                return getSpecifications_JAVA11(Id);
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        e.printStackTrace();
    }
    return specificationList;
}

额外优化建议

  • 控制重试次数:不要无限重试,建议设置最多2-3次重试,避免给服务端造成不必要的压力。
  • 确认服务端限制:如果有条件联系外部服务的维护方,可以确认他们的连接数限制,调整客户端的连接池参数(比如DefaultMaxPerRoute)来匹配。
  • 监控连接状态:添加日志记录连接的复用情况,方便后续排查类似问题。

内容的提问来源于stack exchange,提问作者DwnBrd

火山引擎 最新活动