聊聊HttpClient的KeepAlive

本文详细解释了ApacheHttpClient中ConnectionKeepAliveStrategy接口、DefaultConnectionKeepAliveStrategy实现、ConnectionReuseStrategy接口以及如何通过这些策略管理HTTP连接的持久使用,包括超时时间计算和连接关闭机制。

本文主要研究一下HttpClient的KeepAlive

ConnectionKeepAliveStrategy

org/apache/http/conn/ConnectionKeepAliveStrategy.java

public interface ConnectionKeepAliveStrategy {

    /**
     * Returns the duration of time which this connection can be safely kept
     * idle. If the connection is left idle for longer than this period of time,
     * it MUST not reused. A value of 0 or less may be returned to indicate that
     * there is no suitable suggestion.
     *
     * When coupled with a {@link org.apache.http.ConnectionReuseStrategy}, if
     * {@link org.apache.http.ConnectionReuseStrategy#keepAlive(
     *   HttpResponse, HttpContext)} returns true, this allows you to control
     * how long the reuse will last. If keepAlive returns false, this should
     * have no meaningful impact
     *
     * @param response
     *            The last response received over the connection.
     * @param context
     *            the context in which the connection is being used.
     *
     * @return the duration in ms for which it is safe to keep the connection
     *         idle, or <=0 if no suggested duration.
     */
    long getKeepAliveDuration(HttpResponse response, HttpContext context);

}

ConnectionKeepAliveStrategy接口定义了getKeepAliveDuration方法,用于返回该connection空间多久以内被复用是安全的

DefaultConnectionKeepAliveStrategy

org/apache/http/impl/client/DefaultConnectionKeepAliveStrategy.java

@Contract(threading = ThreadingBehavior.IMMUTABLE)
public class DefaultConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {

    public static final DefaultConnectionKeepAliveStrategy INSTANCE = new DefaultConnectionKeepAliveStrategy();

    @Override
    public long getKeepAliveDuration(final HttpResponse response, final HttpContext context) {
        Args.notNull(response, "HTTP response");
        final HeaderElementIterator it = new BasicHeaderElementIterator(
                response.headerIterator(HTTP.CONN_KEEP_ALIVE));
        while (it.hasNext()) {
            final HeaderElement he = it.nextElement();
            final String param = he.getName();
            final String value = he.getValue();
            if (value != null && param.equalsIgnoreCase("timeout")) {
                try {
                    return Long.parseLong(value) * 1000;
                } catch(final NumberFormatException ignore) {
                }
            }
        }
        return -1;
    }

}

DefaultConnectionKeepAliveStrategy实现了ConnectionKeepAliveStrategy接口,它主要是从response的Keep-Alive的header读取timeout参数

ConnectionReuseStrategy

org/apache/http/ConnectionReuseStrategy.java

public interface ConnectionReuseStrategy {

    /**
     * Decides whether a connection can be kept open after a request.
     * If this method returns {@code false}, the caller MUST
     * close the connection to correctly comply with the HTTP protocol.
     * If it returns {@code true}, the caller SHOULD attempt to
     * keep the connection open for reuse with another request.
     * <p>
     * One can use the HTTP context to retrieve additional objects that
     * may be relevant for the keep-alive strategy: the actual HTTP
     * connection, the original HTTP request, target host if known,
     * number of times the connection has been reused already and so on.
     * </p>
     * <p>
     * If the connection is already closed, {@code false} is returned.
     * The stale connection check MUST NOT be triggered by a
     * connection reuse strategy.
     * </p>
     *
     * @param response
     *          The last response received over that connection.
     * @param context   the context in which the connection is being
     *          used.
     *
     * @return {@code true} if the connection is allowed to be reused, or
     *         {@code false} if it MUST NOT be reused
     */
    boolean keepAlive(HttpResponse response, HttpContext context);

}

ConnectionReuseStrategy接口定义了keepAlive方法,用于判断该connection是否保持连接继续复用,还是直接关闭

DefaultClientConnectionReuseStrategy

org/apache/http/impl/client/DefaultClientConnectionReuseStrategy.java

public class DefaultClientConnectionReuseStrategy extends DefaultConnectionReuseStrategy {

    public static final DefaultClientConnectionReuseStrategy INSTANCE = new DefaultClientConnectionReuseStrategy();

    @Override
    public boolean keepAlive(final HttpResponse response, final HttpContext context) {

        final HttpRequest request = (HttpRequest) context.getAttribute(HttpCoreContext.HTTP_REQUEST);
        if (request != null) {
            final Header[] connHeaders = request.getHeaders(HttpHeaders.CONNECTION);
            if (connHeaders.length != 0) {
                final TokenIterator ti = new BasicTokenIterator(new BasicHeaderIterator(connHeaders, null));
                while (ti.hasNext()) {
                    final String token = ti.nextToken();
                    if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) {
                        return false;
                    }
                }
            }
        }
        return super.keepAlive(response, context);
    }

}

DefaultClientConnectionReuseStrategy继承了DefaultConnectionReuseStrategy,其keepAlive方法先判断Connection这个header的值是不是Close,若是则返回false,其他逻辑复用父类的方法

MainClientExec

org/apache/http/impl/execchain/MainClientExec.java

    @Override
    public CloseableHttpResponse execute(
            final HttpRoute route,
            final HttpRequestWrapper request,
            final HttpClientContext context,
            final HttpExecutionAware execAware) throws IOException, HttpException {

            //......
               response = requestExecutor.execute(request, managedConn, context);

                // The connection is in or can be brought to a re-usable state.
                if (reuseStrategy.keepAlive(response, context)) {
                    // Set the idle duration of this connection
                    final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
                    if (this.log.isDebugEnabled()) {
                        final String s;
                        if (duration > 0) {
                            s = "for " + duration + " " + TimeUnit.MILLISECONDS;
                        } else {
                            s = "indefinitely";
                        }
                        this.log.debug("Connection can be kept alive " + s);
                    }
                    connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
                    connHolder.markReusable();
                } else {
                    connHolder.markNonReusable();
                }
            //......

    }        

MainClientExec的execute方法会通过reuseStrategy.keepAlive判断连接是否可以复用,是的话则通过keepAliveStrategy.getKeepAliveDuration来获取keepAlive时间,同时设置setValidFor(keepalive)及markReusable

releaseConnection

org/apache/http/impl/conn/PoolingHttpClientConnectionManager.java

    @Override
    public void releaseConnection(
            final HttpClientConnection managedConn,
            final Object state,
            final long keepalive, final TimeUnit timeUnit) {
        Args.notNull(managedConn, "Managed connection");
        synchronized (managedConn) {
            final CPoolEntry entry = CPoolProxy.detach(managedConn);
            if (entry == null) {
                return;
            }
            final ManagedHttpClientConnection conn = entry.getConnection();
            try {
                if (conn.isOpen()) {
                    final TimeUnit effectiveUnit = timeUnit != null ? timeUnit : TimeUnit.MILLISECONDS;
                    entry.setState(state);
                    entry.updateExpiry(keepalive, effectiveUnit);
                    if (this.log.isDebugEnabled()) {
                        final String s;
                        if (keepalive > 0) {
                            s = "for " + (double) effectiveUnit.toMillis(keepalive) / 1000 + " seconds";
                        } else {
                            s = "indefinitely";
                        }
                        this.log.debug("Connection " + format(entry) + " can be kept alive " + s);
                    }
                    conn.setSocketTimeout(0);
                }
            } finally {
                this.pool.release(entry, conn.isOpen() && entry.isRouteComplete());
                if (this.log.isDebugEnabled()) {
                    this.log.debug("Connection released: " + format(entry) + formatStats(entry.getRoute()));
                }
            }
        }
    }


PoolingHttpClientConnectionManager的releaseConnection方法在连接是open的时候执行entry.updateExpiry(keepalive, effectiveUnit)

PoolEntry

org/apache/http/pool/PoolEntry.java

    public synchronized void updateExpiry(final long time, final TimeUnit timeUnit) {
        Args.notNull(timeUnit, "Time unit");
        this.updated = System.currentTimeMillis();
        final long newExpiry;
        if (time > 0) {
            newExpiry = this.updated + timeUnit.toMillis(time);
        } else {
            newExpiry = Long.MAX_VALUE;
        }
        this.expiry = Math.min(newExpiry, this.validityDeadline);
    }

    public synchronized boolean isExpired(final long now) {
        return now >= this.expiry;
    }

它在keepalive大于0的时候更新newExpiry为当前时间+keepalive时间,否则更新newExpiry为Long.MAX_VALUE,最后取newExpiry与validityDeadline的最小值作为entry的expiry;其isExpired方法用当前时间与expiry对比,大于等于的返回true

closeExpired

org/apache/http/pool/AbstractConnPool.java

    /**
     * Closes expired connections and evicts them from the pool.
     */
    public void closeExpired() {
        final long now = System.currentTimeMillis();
        enumAvailable(new PoolEntryCallback<T, C>() {

            @Override
            public void process(final PoolEntry<T, C> entry) {
                if (entry.isExpired(now)) {
                    entry.close();
                }
            }

        });
    }

closeExpired主要是遍历available,挨个判断是否expired,是则执行close

小结

HttpClient的MainClientExec的execute方法会通过reuseStrategy.keepAlive判断连接是否可以复用,是的话则通过keepAliveStrategy.getKeepAliveDuration来获取keepAlive时间,同时设置setValidFor(keepalive)及markReusable;IdleConnectionEvictor线程每隔指定时间会执行closeExpired方法,它是依据当前时间与entry的expiry时间进行比较得出,而expiry时间则取newExpiry与validityDeadline的最小值,其中newExpiry的时间取决于keepAliveStrategy.getKeepAliveDuration,而validityDeadline取决于connTimeToLive值。若connTimeToLive值没有设置则默认为-1,那么validityDeadline的值是Long.MAX_VALUE,那么isExpired方法则取决于keepAliveStrategy.getKeepAliveDuration返回的值。若response的header没有Keep-Alive或者Keep-Alive的header没有timeout参数则keepAliveStrategy.getKeepAliveDuration返回-1(indefinitely),则newExpiry将是Long.MAX_VALUE。

默认keepalive是开启的,如果走systemProperties,且http.keepAlive设置为false,则ConnectionReuseStrategy会被设置为NoConnectionReuseStrategy(keepAlive方法返回false),连接归还的时候会被直接关闭。

doc

<think>我们正在讨论如何在Java HttpClient中启用Keep-Alive配置。根据用户的问题,我们需要配置HttpClient以支持Keep-Alive,即持久连接,以减少建立连接的开销,提高性能。 在Java中,我们可以使用java.net.http.HttpClientJava 11引入)或者Apache HttpClient等库。这里我们主要讨论Java 11+的标准HttpClient,因为它是现代Java应用的首选。 Java 11的HttpClient默认就支持HTTP/2,并且对于HTTP/1.1也支持持久连接(Keep-Alive)。实际上,持久连接是默认行为,不需要特别开启。但是,我们可以通过设置连接超时等参数来管理连接。 然而,用户可能希望明确配置以确保Keep-Alive被启用,或者调整相关参数。下面我们给出一个使用Java HttpClient的示例,并说明如何配置连接池和超时设置以管理持久连接。 ### 使用Java 11+ HttpClientJava 11中,HttpClient默认使用持久连接。我们可以通过`HttpClient.newBuilder()`来创建客户端,并设置连接超时和连接池等。 ```java import java.net.http.HttpClient; import java.time.Duration; public class HttpClientExample { public static void main(String[] args) { HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(30)) // 连接超时时间 // 默认情况下,连接池是开启的,我们可以通过系统属性来调整连接池大小 // 例如:-Djdk.httpclient.keepalive.timeout=30 (秒) 设置空闲连接保持时间 // 或者 -Djdk.httpclient.connectionPoolSize=10 设置连接池大小 .build(); // 使用httpClient发送请求... } } ``` 注意:在Java 11的HttpClient中,持久连接是默认启用的,我们不需要显式开启。但是,我们可以通过系统属性来调整连接池的行为: - `jdk.httpclient.keepalive.timeout`:设置空闲连接在连接池中保持的时间(秒),默认是1200秒(20分钟)。 - `jdk.httpclient.connectionPoolSize`:设置连接池的大小,默认是0,表示无上限(实际上受限于打开文件描述符的限制等)。 ### 使用Apache HttpClient 如果用户使用的是Apache HttpClient(例如4.x或5.x版本),那么配置Keep-Alive需要显式设置。 #### Apache HttpClient 5.x示例 ```java import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.apache.hc.core5.util.Timeout; public class ApacheHttpClientExample { public static void main(String[] args) { // 创建连接池管理器 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(100); // 最大连接数 connectionManager.setDefaultMaxPerRoute(20); // 每个路由的最大连接数 // 创建HttpClient CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) // 设置Keep-Alive策略,默认是开启的,但我们可以自定义 // 例如,设置连接存活时间(可选) .setKeepAliveStrategy((response, context) -> Timeout.ofSeconds(30)) // 设置连接存活时间为30秒 .build(); // 使用httpClient发送请求... } } ``` 在Apache HttpClient中,默认也是支持Keep-Alive的,但通过连接池管理可以更高效地复用连接。 ### 注意事项 1. **服务器支持**:Keep-Alive需要服务器支持。大多数现代HTTP服务器都支持。 2. **超时设置**:合理设置连接超时和Keep-Alive时间,避免资源浪费。 3. **连接池管理**:在高并发场景下,连接池的大小和路由的最大连接数需要根据实际情况调整。 ### 总结 在Java中启用Keep-Alive,对于Java 11+的HttpClient,默认就是开启的,我们只需要调整相关参数即可。对于Apache HttpClient,同样默认支持,但我们可以通过连接池和Keep-Alive策略进行更细粒度的控制。 如果用户使用的是其他HttpClient库,配置方式可能有所不同,但原理相似。 ### 相关问题 1. 如何在Java中配置HttpClient的连接超时和读取超时? 2. 如何监控Java HttpClient的连接池状态? 3. 在HTTP/2下,Keep-Alive还有必要吗? 4. 如何确保HttpClient在长时间空闲后仍然保持连接有效?</think>### Java HttpClient 启用 Keep-Alive 配置指南 在 Java 中启用 HTTP Keep-Alive 可显著提升性能,通过复用 TCP 连接减少握手开销。以下是具体实现方法: #### 1. Java 11+ 标准 HttpClient(推荐) ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class KeepAliveDemo { public static void main(String[] args) { // 创建支持 Keep-AliveHttpClient HttpClient httpClient = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) // 必须使用 HTTP/1.1 .connectTimeout(Duration.ofSeconds(30)) .build(); // 创建 HTTP 请求 HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://example.com/api")) .header("Connection", "keep-alive") // 显式声明 Keep-Alive .timeout(Duration.ofSeconds(20)) .build(); // 发送请求 HttpResponse<String> response = httpClient.send( request, HttpResponse.BodyHandlers.ofString()); } } ``` **关键点**: - 默认启用 Keep-Alive(无需额外配置) - 通过 `.version(HttpClient.Version.HTTP_1_1)` 强制使用 HTTP/1.1 - 连接池大小通过系统属性控制: ```bash -Djdk.httpclient.keepalive.timeout=60 # 空闲连接超时(秒) -Djdk.httpclient.connectionPoolSize=10 # 连接池大小 ``` #### 2. Apache HttpClient 5.x ```java import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; public class ApacheKeepAlive { public static void main(String[] args) { // 配置连接池 PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); cm.setMaxTotal(100); // 最大连接数 cm.setDefaultMaxPerRoute(20); // 每路由最大连接数 // 创建支持 Keep-Alive 的客户端 try (CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(cm) .setKeepAliveStrategy((response, context) -> 30_000) // 保持30秒 .build()) { // 发送请求 (自动处理 Keep-Alive) } } } ``` #### 3. Spring RestTemplate ```java @Bean public RestTemplate restTemplate() { // 使用连接池配置 PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); cm.setMaxTotal(200); cm.setDefaultMaxPerRoute(50); CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(cm) .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy()) .build(); return new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient)); } ``` #### 关键配置参数 | 参数 | 建议值 | 说明 | |------|--------|------| | 连接超时 | 10-30秒 | 建立 TCP 连接的最长时间 | |空闲超时 | 30-60秒 | 空闲连接保持时间 | |最大连接数 | 100-200 | 防止资源耗尽 | |每路由连接数 | 20-50 | 单目标主机连接限制 | #### 验证 Keep-Alive 是否生效 1. 使用网络抓包工具(如 Wireshark)观察 TCP 连接复用 2. 检查响应头是否包含 `Connection: keep-alive` 3. 监控日志是否出现大量 `[CONNECT]` 日志(复用成功则无此日志) > **注意**:Keep-Alive 需要服务器端同时支持,现代 HTTP 服务器(如 Tomcat/Nginx)默认启用。在 JDK 16+ 中,Alpine Linux 支持通过 musl libc 优化容器环境下的连接性能[^1]。 ### 相关问题 1. 如何检测 Java HttpClient 的连接池使用情况? 2. HTTP/2 协议下 Keep-Alive 是否仍然必要? 3. 如何优化高并发场景下的 HttpClient 连接参数? 4. 在容器化环境中部署时有哪些特殊配置注意事项? 5. 如何为 HttpClient 添加统一的请求拦截器(如自动添加时间戳)[^3]? [^1]: 在 JDK 16 中,引入了对 Alpine Linux 的官方支持。Alpine Linux 是一个轻量级的 Linux 发行版,主要用于容器化应用程序。 [^3]: 继承 BasePathMatchInterceptor 编写拦截处理器,可自动添加时间戳等通用参数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值