如何从 HttpClient 设置 TCP keep Alive?

How to set TCP keep Alive from HttpClient?

我的 Java 驻留在 AWS 私有子网中的应用程序通过 AWS Nat 网关连接到 http 服务器。我正在通过 HttpClient 向 HTTP 服务器调用 POST 请求。该请求将需要 10 多分钟才能完成。我已经配置了 1 小时的套接字超时和连接超时,因为这是一个后台任务。但是中间 AWS NAT 网关将在 300 秒 [5 分钟] 后发回 RST 数据包并导致连接重置,我无法增加 NAT 网关超时。所以我需要从我的应用程序端处理这个问题。

我的策略是使用 TCP 保持活动时间,它会每 240 秒发送一个数据包以保持连接处于活动状态。我已经配置了这个 如下

CloseableHttpClient httpClient = HttpClients.createDefault()
HttpParams params = httpClient.getParams();
HttpConnectionParams.setConnectionTimeout(params, 3600000); //connection Timeout
HttpConnectionParams.setSoTimeout(params, 3600000); // Socket Time out
HttpConnectionParams.setSoKeepalive(params, true); //Enable Socket level keep alive time

然后通过execute方法调用post请求

HttpPost post = new HttpPost("http://url");
HttpResponse response = httpClient.execute(post);

由于我使用的是 Linux 系统,因此我已使用以下 sysctl 值配置服务器:

sysctl -w net.ipv4.tcp_keepalive_time=240 
sysctl -w net.ipv4.tcp_keepalive_intvl=240
sysctl -w net.ipv4.tcp_keepalive_probes=10

但是在执行程序时,没有启用保持活动状态,连接失败。

我已经使用 netstat -o 选项检查了这一点,如下所示,keep alive 已关闭

tcp        0      0 192.168.1.141:43770     public_ip:80          ESTABLISHED 18134/java           off (0.00/0/0)

有什么方法可以使用 httpclient 从 java 代码设置 TCP keep alive。我还可以看到 HttpConnectionParams 已被弃用。但是我找不到任何新的 class 可以设置 keep alive

我找到了解决问题的方法。奇怪的情况是我无法在 httpclient 中使用一些构建器 class 来传递 socket keep alive 。我在问题中指定的一种方法是使用 HttpConnectionParams,如下所示,但这不起作用,现在不推荐使用此 class。

HttpParams params = httpClient.getParams();
HttpConnectionParams.setSoKeepalive(params, true);

因此,在检查 apache http 文档时,我可以看到现在连接参数通过 RequestConfig class 传递给了 httpclient。此 class 的构建者提供了设置 connection_time_out 和 socket_time_out 的解决方案。但是检查这个源代码我看不到启用 SocketKeepAlive 的选项,这正是我们想要的。因此唯一的解决方案是使用 SocketBuilder class 直接创建 Socket 并将其传递给 HttpClientBuilder。

以下是工作代码

SocketConfig socketConfig = SocketConfig.custom().setSoKeepAlive(true).setSoTimeout(3600000).build(); //We need to set socket keep alive
        RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(3600000).build();
        CloseableHttpClient httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).
                                           setDefaultSocketConfig(socketConfig).build();
HttpPost post = new HttpPost(url.toString());
HttpResponse response = httpClient.execute(post);

在上面执行时,我可以看到根据我在 linux 内核

中设置的 sysctl 值,在套接字中正确设置了 keep alive
tcp        0      0 localip:48314     public_ip:443     ESTABLISHED 14863/java          keepalive (234.11/0/0)

如果有人有更好的解决方案来从 Requestconfig class 或任何其他高级构建器 class 启用 Socket Keep alive,我愿意接受建议。

保持 HTTP 连接打开但长时间不活动是一个糟糕的设计选择。 HTTP 是一种请求-响应协议,意味着请求和响应很快。

保持连接打开会保持资源。从服务器(以及网络防火墙和路由器)的角度来看,打开连接并开始请求(在您的情况下为 POST)但长时间不发送任何字节的客户端与客户端无法区分将永远不会 发送任何更多数据,因为它是错误的或恶意的(进行 DOS 攻击)。服务器(和网络硬件)正确地得出结论,正确的做法是关闭连接并回收用于它的资源。您正试图与出于充分理由而发生的正确行为作斗争。即使您设法解决 TCP 关闭问题,您也会发现其他问题,例如 HTTP 服务器超时和数据库超时。

您应该重新考虑两个组件之间的通信设计。也就是说,这看起来像一个 XY 问题。您可能会考虑

  • 在 开始 POST 之前,让客户端等到它完成上传以执行
  • 将上传分成更小、更频繁的上传。
  • 使用 HTTP 以外的协议。

上面使用 Socket 的方法在 AWS 网络负载均衡器超时以下重置 tcp_keepalive_intvl 值时效果很好。使用两者,重置允许 java 小时以上连接的 NLB tcp 空闲超时。

有时,如果配置被覆盖,配置不采取 effect.My buildClient 中的 setDefaultSocketConfig 初始修改没有采取 effect.Because 它被 getConnectionManager() 覆盖

    public CloseableHttpClient buildClient() throws Exception {
    HttpClientBuilder builder = HttpClientBuilder.create()
            .setDefaultSocketConfig(SocketConfig.custom().setSoKeepAlive(true).build())  // did not work
            .setConnectionManager(getConnectionManager())
            .setRetryHandler(getRequestRetryHandler())
            .setConnectionReuseStrategy(getConnectionReuseStrategy())
            .setDefaultConnectionConfig(getConnectionConfig())
            .setDefaultRequestConfig(getRequestConfig())
            .setDefaultHeaders(getDefaultHeaders())
            .setDefaultCredentialsProvider(getDefaultCredentialsProvider())
            .disableContentCompression() // gzip is not needed. Use lz4 when compress=1
            .setDefaultCookieStore(cookieStoreProvider.getCookieStore(properties))
            .disableRedirectHandling();

    String clientName = properties != null ? properties.getClientName() : null;
    if (!Utils.isNullOrEmptyString(clientName)) {
        builder.setUserAgent(clientName);
    }
    
    return builder.build();

然后我将配置移动到 getConnectionManager(),它工作了。

    private PoolingHttpClientConnectionManager getConnectionManager()
    throws CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException {
    RegistryBuilder<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
      .register("http", PlainConnectionSocketFactory.getSocketFactory());

    if (properties.getSsl()) {
        HostnameVerifier verifier = "strict".equals(properties.getSslMode()) ? SSLConnectionSocketFactory.getDefaultHostnameVerifier() : NoopHostnameVerifier.INSTANCE;
        registry.register("https", new SSLConnectionSocketFactory(getSSLContext(), verifier));
    }

    //noinspection resource
    PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(
        registry.build(),
        null,
        null,
        new IpVersionPriorityResolver(),
        properties.getTimeToLiveMillis(),
        TimeUnit.MILLISECONDS
    );

    connectionManager.setDefaultMaxPerRoute(properties.getDefaultMaxPerRoute());
    connectionManager.setMaxTotal(properties.getMaxTotal());
    connectionManager.setDefaultConnectionConfig(getConnectionConfig());
    connectionManager.setDefaultSocketConfig(SocketConfig.custom().setSoKeepAlive(true).build());
    return connectionManager;
}