Apache HttpClient 超时问题

59

有没有一种方法可以为HttpClient的整个执行指定超时时间?

我尝试了以下方法:

httpClient.getParams().setParameter("http.socket.timeout", timeout * 1000);
httpClient.getParams().setParameter("http.connection.timeout", timeout * 1000);
httpClient.getParams().setParameter("http.connection-manager.timeout", new Long(timeout * 1000));
httpClient.getParams().setParameter("http.protocol.head-body-timeout", timeout * 1000);

实际上,它的工作正常,除非远程主机发送数据 - 即使是每秒一个字节 - 它仍将一直读取!但是我想在最多10秒钟内中断连接,无论主机是否响应。


请查看http://stackoverflow.com/a/20971271/236743 - Dori
1
@RealMan的答案对我有用。 - shellbye
如果它是异步的,就会有超时。但它是同步的,因此无法按照HttpClient的方式进行。 - WesternGun
7个回答

64

如果需要使用较新版本的httpclient(例如http components 4.3 - https://hc.apache.org/httpcomponents-client-4.3.x/index.html):

int CONNECTION_TIMEOUT_MS = timeoutSeconds * 1000; // Timeout in millis.
RequestConfig requestConfig = RequestConfig.custom()
    .setConnectionRequestTimeout(CONNECTION_TIMEOUT_MS)
    .setConnectTimeout(CONNECTION_TIMEOUT_MS)
    .setSocketTimeout(CONNECTION_TIMEOUT_MS)
    .build();

HttpPost httpPost = new HttpPost(URL);
httpPost.setConfig(requestConfig);

9
添加了单位,我讨厌它只表明“setTimeout”,应该是“setTimeoutMs”,确实是毫秒。 - Christophe Roussy
2
这个设置不会允许总请求生命周期> CONNECTION_TIMEOUT_MS吗?您已在请求的多个阶段指定了相同的超时时间。 - Todd Freed
29
我认为这不是原问题的正确答案。连接请求超时文档说明:“返回从连接管理器请求连接时使用的超时时间(以毫秒为单位)”,这 不是 执行请求所需的总时间,只是为了从连接管理器获取连接。因此,如果服务器以每秒1字节的速度返回数据,就像OP所问的那样,这很容易超过连接请求超时时间。 - mR_fr0g
3
同意,这不是给定问题的解决方案。如果请求一直从套接字读取数据包,请求将永远不会超时。该问题是关于硬超时的。 - Valchkou

39

目前还没有一种方法可以设置这样的最大请求持续时间:基本上您想说的是我不关心任何特定请求阶段是否超时,但整个请求不能超过15秒(例如)。

您最好的选择是运行一个单独的计时器,当它到期时获取由HttpClient实例使用的连接管理器并关闭连接,这应该可以终止链接。如果这对您有用,请告诉我。


2
是的,可能必须这样做:计时器应该在一个独立的线程中运行。 - Femi
2
这值得提出功能请求吗?它似乎是一个常见的用例。 - Chad Van De Hey
2
在此您可以找到这种解决方案的实现,请参阅 https://www.baeldung.com/httpclient-timeout#hard_timeout - Sam

11

定时器很危险! 使用定时器、执行器或任何创建每个请求的线程/可运行对象的机制都是一个非常糟糕的想法。请明智思考,不要这样做。否则,您将很快遇到各种更多或更少真实环境下的内存问题。想象一下每分钟1000个请求意味着每分钟1000个线程或工作者。GC会很辛苦。我提出的解决方案只需要1个看门狗线程,可以节省您的资源、时间和神经。 基本上,您需要执行3个步骤。

  1. 将请求放入缓存中。
  2. 在完成后从缓存中删除请求。
  3. 取消那些没有在您规定的时间内完成的请求。

您的缓存以及看门狗线程可能如下所示。

import org.apache.http.client.methods.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;

public class RequestCache {

private static final long expireInMillis = 300000;
private static final Map<HttpUriRequest, Long> cache = new ConcurrentHashMap<>();
private static final ScheduledExecutorService exe = Executors.newScheduledThreadPool(1);

static {
    // run clean up every N minutes
    exe.schedule(RequestCache::cleanup, 1, TimeUnit.MINUTES);
}

public static void put(HttpUriRequest request) {
    cache.put(request, System.currentTimeMillis()+expireInMillis);
}

public static void remove(HttpUriRequest request) {
    cache.remove(request);
}

private static void cleanup() {
    long now = System.currentTimeMillis();
    // find expired requests
    List<HttpUriRequest> expired = cache.entrySet().stream()
            .filter(e -> e.getValue() > now)
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());

    // abort requests
    expired.forEach(r -> {
        if (!r.isAborted()) {
            r.abort();
        }
        cache.remove(r);
      });
    }
  }

以下是如何使用缓存的伪代码

import org.apache.http.client.methods.*;

public class RequestSample {

public void processRequest() {
    HttpUriRequest req = null;
    try {
        req = createRequest();

        RequestCache.put(req);

        execute(req);

    } finally {
        RequestCache.remove(req);
    }
  }
}

抱歉晚回复,如果我们不在每次调用中使用时间,而是使用您的方法仅使用1个计划线程,那么如果调用时出现e.abort(),我们该如何返回呢? - Sen
HTTP框架会为您处理这个问题。当连接中断时,它会抛出HTTP错误,类似于连接超时或连接中断的情况。 - Valchkou

11

按照Femi的建议,运行良好。谢谢!

Timer timer = new Timer();
timer.schedule(new TimerTask() {
    public void run() {
        if(getMethod != null) {
            getMethod.abort();
        }
    }
}, timeout * 1000);

9
需要一个锁来避免NPE。在检查和调用中止方法之间, getMethod 可能会变为空! - Tony BenBrahim
@TonyBenBrahim,你能展示一下如何添加这个锁的例子吗? - Tony Chan
2
@Turbo: synchronized(foo){ ... getMethod=null; }@Turbo:synchronized(foo){ 如果(getMethod!=null){ getMethod.abort(); } } - Tony BenBrahim
@TonyBenBrahim 谢谢。我还不太懂同步。getMethod=null; 这一部分仅仅是使用 HttpClient 的方法的一个例子,对吗?也就是说,getMethod 不会被故意设置为 null,但当该方法退出时可能会变成 null,因此应该与 Timer 线程在相同对象上同步。 - Tony Chan
4
这个解决方案不适用于生产环境。要记住,每个计时器都会启动一个线程。如果每分钟有1000个请求,那么就会启动1000个线程。这种情况会不断积累,直到由于内存不足而导致服务失败。 - Valchkou

4

在其他答案的基础上,我的解决方案是使用一个HttpRequestInterceptor将中止可运行程序添加到每个请求中。另外,我将Timer替换为ScheduledExecutorService

public class TimeoutInterceptor implements HttpRequestInterceptor {

private int requestTimeout = 1 * DateTimeConstants.MILLIS_PER_MINUTE;

private ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

public TimeoutInterceptor() {  }

public TimeoutInterceptor(final int requestTimeout) {
    this.requestTimeout = requestTimeout;
}

@Override
public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
    if (request instanceof AbstractExecutionAwareRequest) {
        final AbstractExecutionAwareRequest abortableRequest = (AbstractExecutionAwareRequest) request;
        setAbort(abortableRequest);
    } else if (request instanceof HttpRequestWrapper) {
        HttpRequestWrapper wrapper = (HttpRequestWrapper) request;
        this.process(wrapper.getOriginal(), context);
    }

}

/**
 * @param abortableRequest
 */
private void setAbort(final AbstractExecutionAwareRequest abortableRequest) {
    final SoftReference<AbstractExecutionAwareRequest> requestRef = new SoftReference<AbstractExecutionAwareRequest>(abortableRequest);

    executorService.schedule(new Runnable() {

        @Override
        public void run() {
            AbstractExecutionAwareRequest actual = requestRef.get();
            if (actual != null && !actual.isAborted()) {
                actual.abort();
            }
        }
    }, requestTimeout, TimeUnit.MILLISECONDS);

}

public void setRequestTimeout(final int requestTimeout) {
    this.requestTimeout = requestTimeout;
}
}

1
这是一个可行的解决方案,但你需要为每个请求创建一个可运行的实例。在真正高负载的环境中,这很快就会成为一个问题。每分钟1000个请求将导致每分钟1000个可运行实例。可运行实例将排队等待。事实上,你将遇到各种问题,导致服务失败。 - Valchkou
其实我觉得这不会引起太多问题。每分钟1000个请求,你的队列里总是会有大约1000个可运行的任务(带有1分钟的超时时间)。既然仍然只有一个线程,我想那应该没问题。 - undefined

2
在HttpClient 4.3版本中,您可以使用以下示例...比如5秒钟。
int timeout = 5;
RequestConfig config = RequestConfig.custom()
  .setConnectTimeout(timeout * 1000)
  .setConnectionRequestTimeout(timeout * 1000)
  .setSocketTimeout(timeout * 1000).build();
CloseableHttpClient client = 
  HttpClientBuilder.create().setDefaultRequestConfig(config).build();
HttpGet request = new HttpGet("http://localhost:8080/service"); // GET Request
response = client.execute(request);

-5

这也可以工作:

HttpClient client = new DefaultHttpClient();
HttpConnectionParams.setConnectionTimeout(client.getParams(), timeout * 1000);
HttpConnectionParams.setSoTimeout(client.getParams(), timeout * 1000);

3
实际上,这正是他在问题中所做的,显然对他而言并不奏效(也不幸地对我也一样)。 - Dean Hiller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接