You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2024/08/10 09:39:58 UTC

(httpcomponents-core) branch master updated: HTTPCORE-761: support ExtendedSocketOption (#473)

This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git


The following commit(s) were added to refs/heads/master by this push:
     new d5f903a7f HTTPCORE-761: support ExtendedSocketOption (#473)
d5f903a7f is described below

commit d5f903a7f7203b50f1d5cfc97eef4279266b2d9c
Author: kkewwei <ke...@bytedance.com>
AuthorDate: Sat Aug 10 17:39:52 2024 +0800

    HTTPCORE-761: support ExtendedSocketOption (#473)
---
 .../core5/http/impl/bootstrap/HttpRequester.java   |  14 +++
 .../hc/core5/http/impl/bootstrap/HttpServer.java   |  14 +++
 .../core5/http/impl/bootstrap/RequestListener.java |  14 +++
 .../org/apache/hc/core5/http/io/SocketConfig.java  |  86 ++++++++++++++++
 .../apache/hc/core5/reactor/IOReactorConfig.java   |  77 ++++++++++++++
 .../hc/core5/reactor/SingleCoreIOReactor.java      |  34 +++++-
 .../java/org/apache/hc/core5/util/NetUtils.java    |  39 +++++++
 .../org/apache/hc/core5/util/ReflectionUtils.java  |  57 ++++++++++-
 .../hc/core5/reactor/IOReactorConfigTest.java      |   6 ++
 .../apache/hc/core5/util/TestReflectionUtils.java  | 114 +++++++++++++++++++++
 10 files changed, 449 insertions(+), 6 deletions(-)

diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpRequester.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpRequester.java
index 608368979..1dacdd236 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpRequester.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpRequester.java
@@ -82,9 +82,14 @@ import org.apache.hc.core5.pool.ManagedConnPool;
 import org.apache.hc.core5.pool.PoolEntry;
 import org.apache.hc.core5.pool.PoolStats;
 import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.ReflectionUtils;
 import org.apache.hc.core5.util.TimeValue;
 import org.apache.hc.core5.util.Timeout;
 
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPCOUNT;
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPIDLE;
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPINTERVAL;
+
 /**
  * HTTP/1.1 client side message exchange initiator.
  *
@@ -249,6 +254,15 @@ public class HttpRequester implements ConnPoolControl<HttpHost>, ModalCloseable
         if (socketConfig.getSndBufSize() > 0) {
             sock.setSendBufferSize(socketConfig.getSndBufSize());
         }
+        if (this.socketConfig.getTcpKeepIdle() > 0) {
+            ReflectionUtils.setOption(sock, TCP_KEEPIDLE, this.socketConfig.getTcpKeepIdle());
+        }
+        if (this.socketConfig.getTcpKeepInterval() > 0) {
+            ReflectionUtils.setOption(sock, TCP_KEEPINTERVAL, this.socketConfig.getTcpKeepInterval());
+        }
+        if (this.socketConfig.getTcpKeepCount() > 0) {
+            ReflectionUtils.setOption(sock, TCP_KEEPCOUNT, this.socketConfig.getTcpKeepCount());
+        }
         final int linger = socketConfig.getSoLinger().toMillisecondsIntBound();
         if (linger >= 0) {
             sock.setSoLinger(true, linger);
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpServer.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpServer.java
index 8c17d1b62..8363c70c2 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpServer.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpServer.java
@@ -57,9 +57,14 @@ import org.apache.hc.core5.io.CloseMode;
 import org.apache.hc.core5.io.Closer;
 import org.apache.hc.core5.io.ModalCloseable;
 import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.ReflectionUtils;
 import org.apache.hc.core5.util.TimeValue;
 import org.apache.hc.core5.util.Timeout;
 
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPCOUNT;
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPIDLE;
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPINTERVAL;
+
 /**
  * HTTP/1.1 server side message exchange handler.
  *
@@ -145,6 +150,15 @@ public class HttpServer implements ModalCloseable {
             if (this.socketConfig.getRcvBufSize() > 0) {
                 this.serverSocket.setReceiveBufferSize(this.socketConfig.getRcvBufSize());
             }
+            if (this.socketConfig.getTcpKeepIdle() > 0) {
+                ReflectionUtils.setOption(this.serverSocket, TCP_KEEPIDLE, this.socketConfig.getTcpKeepIdle());
+            }
+            if (this.socketConfig.getTcpKeepInterval() > 0) {
+                ReflectionUtils.setOption(this.serverSocket, TCP_KEEPINTERVAL, this.socketConfig.getTcpKeepInterval());
+            }
+            if (this.socketConfig.getTcpKeepCount() > 0) {
+                ReflectionUtils.setOption(this.serverSocket, TCP_KEEPCOUNT, this.socketConfig.getTcpKeepCount());
+            }
             if (this.sslSetupHandler != null && this.serverSocket instanceof SSLServerSocket) {
                 final SSLServerSocket sslServerSocket = (SSLServerSocket) this.serverSocket;
                 final SSLParameters sslParameters = sslServerSocket.getSSLParameters();
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/RequestListener.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/RequestListener.java
index f9b325eaa..2b50caa11 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/RequestListener.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/RequestListener.java
@@ -45,6 +45,11 @@ import org.apache.hc.core5.http.io.HttpConnectionFactory;
 import org.apache.hc.core5.http.io.HttpServerConnection;
 import org.apache.hc.core5.http.io.SocketConfig;
 import org.apache.hc.core5.io.Closer;
+import org.apache.hc.core5.util.ReflectionUtils;
+
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPCOUNT;
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPIDLE;
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPINTERVAL;
 
 class RequestListener implements Runnable {
 
@@ -91,6 +96,15 @@ class RequestListener implements Runnable {
         if (this.socketConfig.getSoLinger().toSeconds() >= 0) {
             socket.setSoLinger(true, this.socketConfig.getSoLinger().toSecondsIntBound());
         }
+        if (this.socketConfig.getTcpKeepIdle() > 0) {
+            ReflectionUtils.setOption(this.serverSocket, TCP_KEEPIDLE, this.socketConfig.getTcpKeepIdle());
+        }
+        if (this.socketConfig.getTcpKeepInterval() > 0) {
+            ReflectionUtils.setOption(this.serverSocket, TCP_KEEPINTERVAL, this.socketConfig.getTcpKeepInterval());
+        }
+        if (this.socketConfig.getTcpKeepCount() > 0) {
+            ReflectionUtils.setOption(this.serverSocket, TCP_KEEPCOUNT, this.socketConfig.getTcpKeepCount());
+        }
         if (!(socket instanceof SSLSocket) && sslSocketFactory != null) {
             final SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, null, -1, false);
             sslSocket.setUseClientMode(false);
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/io/SocketConfig.java b/httpcore5/src/main/java/org/apache/hc/core5/http/io/SocketConfig.java
index 492bc0685..ee3824c29 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/http/io/SocketConfig.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/http/io/SocketConfig.java
@@ -56,6 +56,9 @@ public class SocketConfig {
     private final int sndBufSize;
     private final int rcvBufSize;
     private final int backlogSize;
+    private final int tcpKeepIdle;
+    private final int tcpKeepInterval;
+    private final int tcpKeepCount;
     private final SocketAddress socksProxyAddress;
 
     SocketConfig(
@@ -67,6 +70,9 @@ public class SocketConfig {
             final int sndBufSize,
             final int rcvBufSize,
             final int backlogSize,
+            final int tcpKeepIdle,
+            final int tcpKeepInterval,
+            final int tcpKeepCount,
             final SocketAddress socksProxyAddress) {
         super();
         this.soTimeout = soTimeout;
@@ -77,6 +83,9 @@ public class SocketConfig {
         this.sndBufSize = sndBufSize;
         this.rcvBufSize = rcvBufSize;
         this.backlogSize = backlogSize;
+        this.tcpKeepIdle = tcpKeepIdle;
+        this.tcpKeepInterval = tcpKeepInterval;
+        this.tcpKeepCount = tcpKeepCount;
         this.socksProxyAddress = socksProxyAddress;
     }
 
@@ -139,6 +148,30 @@ public class SocketConfig {
         return backlogSize;
     }
 
+    /**
+     *  @see Builder#setTcpKeepIdle(int)
+     * @since 5.3
+     */
+    public int getTcpKeepIdle() {
+        return this.tcpKeepIdle;
+    }
+
+    /**
+     * @see Builder#setTcpKeepInterval(int)
+     * @since 5.3
+     */
+    public int getTcpKeepInterval() {
+        return this.tcpKeepInterval;
+    }
+
+    /**
+     * @see Builder#setTcpKeepCount(int)
+     * @since 5.3
+     */
+    public int getTcpKeepCount() {
+        return this.tcpKeepCount;
+    }
+
     /**
      * @see Builder#setSocksProxyAddress(SocketAddress)
      */
@@ -157,6 +190,9 @@ public class SocketConfig {
                 .append(", sndBufSize=").append(this.sndBufSize)
                 .append(", rcvBufSize=").append(this.rcvBufSize)
                 .append(", backlogSize=").append(this.backlogSize)
+                .append(", tcpKeepIdle=").append(this.tcpKeepIdle)
+                .append(", tcpKeepInterval=").append(this.tcpKeepInterval)
+                .append(", tcpKeepCount=").append(this.tcpKeepCount)
                 .append(", socksProxyAddress=").append(this.socksProxyAddress)
                 .append("]");
         return builder.toString();
@@ -177,6 +213,9 @@ public class SocketConfig {
             .setSndBufSize(config.getSndBufSize())
             .setRcvBufSize(config.getRcvBufSize())
             .setBacklogSize(config.getBacklogSize())
+            .setTcpKeepIdle(config.getTcpKeepIdle())
+            .setTcpKeepInterval(config.getTcpKeepInterval())
+            .setTcpKeepCount(config.getTcpKeepCount())
             .setSocksProxyAddress(config.getSocksProxyAddress());
     }
 
@@ -190,6 +229,9 @@ public class SocketConfig {
         private int sndBufSize;
         private int rcvBufSize;
         private int backlogSize;
+        private int tcpKeepIdle;
+        private int tcpKeepInterval;
+        private int tcpKeepCount;
         private SocketAddress socksProxyAddress;
 
         Builder() {
@@ -201,6 +243,9 @@ public class SocketConfig {
             this.sndBufSize = 0;
             this.rcvBufSize = 0;
             this.backlogSize = 0;
+            this.tcpKeepIdle = -1;
+            this.tcpKeepInterval = -1;
+            this.tcpKeepCount = -1;
             this.socksProxyAddress = null;
         }
 
@@ -340,6 +385,46 @@ public class SocketConfig {
             return this;
         }
 
+        /**
+         * Determines the time (in seconds) the connection needs to remain idle before TCP starts
+         * sending keepalive probes.
+         * <p>
+         * Default: {@code -1} (system default)
+         * </p>
+         * @return the time (in seconds) the connection needs to remain idle before TCP starts
+         * @since 5.3
+         */
+        public Builder setTcpKeepIdle(final int tcpKeepIdle) {
+            this.tcpKeepIdle = tcpKeepIdle;
+            return this;
+        }
+
+        /**
+         * Determines the time (in seconds) between individual keepalive probes.
+         * <p>
+         * Default: {@code -1} (system default)
+         * </p>
+         * @return the time (in seconds) between individual keepalive probes.
+         * @since 5.3
+         */
+        public Builder setTcpKeepInterval(final int tcpKeepInterval) {
+            this.tcpKeepInterval = tcpKeepInterval;
+            return this;
+        }
+
+        /**
+         * Determines the maximum number of keepalive probes TCP should send before dropping the connection.
+         * <p>
+         * Default: {@code -1} (system default)
+         * </p>
+         * @return the maximum number of keepalive probes TCP should send before dropping the connection.
+         * @since 5.3
+         */
+        public Builder setTcpKeepCount(final int tcpKeepCount) {
+            this.tcpKeepCount = tcpKeepCount;
+            return this;
+        }
+
         /**
          * The address of the SOCKS proxy to use.
          */
@@ -354,6 +439,7 @@ public class SocketConfig {
                     soReuseAddress,
                     soLinger != null ? soLinger : TimeValue.NEG_ONE_SECOND,
                     soKeepAlive, tcpNoDelay, sndBufSize, rcvBufSize, backlogSize,
+                    tcpKeepIdle, tcpKeepInterval, tcpKeepCount,
                     socksProxyAddress);
         }
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOReactorConfig.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOReactorConfig.java
index 70ef1d7d0..6232ebcdf 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOReactorConfig.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/IOReactorConfig.java
@@ -57,6 +57,9 @@ public final class IOReactorConfig {
     private final int sndBufSize;
     private final int rcvBufSize;
     private final int backlogSize;
+    private final int tcpKeepIdle;
+    private final int tcpKeepInterval;
+    private final int tcpKeepCount;
     private final SocketAddress socksProxyAddress;
     private final String socksProxyUsername;
     private final String socksProxyPassword;
@@ -73,6 +76,9 @@ public final class IOReactorConfig {
             final int sndBufSize,
             final int rcvBufSize,
             final int backlogSize,
+            final int tcpKeepIdle,
+            final int tcpKeepInterval,
+            final int tcpKeepCount,
             final SocketAddress socksProxyAddress,
             final String socksProxyUsername,
             final String socksProxyPassword) {
@@ -88,6 +94,9 @@ public final class IOReactorConfig {
         this.sndBufSize = sndBufSize;
         this.rcvBufSize = rcvBufSize;
         this.backlogSize = backlogSize;
+        this.tcpKeepIdle = tcpKeepIdle;
+        this.tcpKeepInterval = tcpKeepInterval;
+        this.tcpKeepCount = tcpKeepCount;
         this.socksProxyAddress = socksProxyAddress;
         this.socksProxyUsername = socksProxyUsername;
         this.socksProxyPassword = socksProxyPassword;
@@ -182,6 +191,33 @@ public final class IOReactorConfig {
         return backlogSize;
     }
 
+    /**
+     * @see Builder#setTcpKeepIdle(int)
+     *
+     * @since 5.3
+     */
+    public int getTcpKeepIdle() {
+        return this.tcpKeepIdle;
+    }
+
+    /**
+     * @see Builder#setTcpKeepInterval(int)
+     *
+     * @since 5.3
+     */
+    public int getTcpKeepInterval() {
+        return this.tcpKeepInterval;
+    }
+
+    /**
+     * @see Builder#setTcpKeepCount(int)
+     *
+     * @since 5.3
+     */
+    public int getTcpKeepCount() {
+        return this.tcpKeepCount;
+    }
+
     /**
      * @see Builder#setSocksProxyAddress(SocketAddress)
      */
@@ -220,6 +256,9 @@ public final class IOReactorConfig {
             .setSndBufSize(config.getSndBufSize())
             .setRcvBufSize(config.getRcvBufSize())
             .setBacklogSize(config.getBacklogSize())
+            .setTcpKeepIdle(config.getTcpKeepIdle())
+            .setTcpKeepInterval(config.getTcpKeepInterval())
+            .setTcpKeepCount(config.getTcpKeepCount())
             .setSocksProxyAddress(config.getSocksProxyAddress())
             .setSocksProxyUsername(config.getSocksProxyUsername())
             .setSocksProxyPassword(config.getSocksProxyPassword());
@@ -265,6 +304,9 @@ public final class IOReactorConfig {
         private int sndBufSize;
         private int rcvBufSize;
         private int backlogSize;
+        private int tcpKeepIdle;
+        private int tcpKeepInterval;
+        private int tcpKeepCount;
         private SocketAddress socksProxyAddress;
         private String socksProxyUsername;
         private String socksProxyPassword;
@@ -281,6 +323,9 @@ public final class IOReactorConfig {
             this.sndBufSize = 0;
             this.rcvBufSize = 0;
             this.backlogSize = 0;
+            this.tcpKeepIdle = -1;
+            this.tcpKeepInterval = -1;
+            this.tcpKeepCount = -1;
             this.socksProxyAddress = null;
             this.socksProxyUsername = null;
             this.socksProxyPassword = null;
@@ -462,6 +507,37 @@ public final class IOReactorConfig {
             return this;
         }
 
+        /**
+         * Determines the time (in seconds) the connection needs to remain idle before TCP starts
+         * sending keepalive probes.
+         *
+         * @since 5.3
+         */
+        public Builder setTcpKeepIdle(final int tcpKeepIdle) {
+            this.tcpKeepIdle = tcpKeepIdle;
+            return this;
+        }
+
+        /**
+         * Determines the time (in seconds) between individual keepalive probes.
+         *
+         * @since 5.3
+         */
+        public Builder setTcpKeepInterval(final int tcpKeepInterval) {
+            this.tcpKeepInterval = tcpKeepInterval;
+            return this;
+        }
+
+        /**
+         * Determines the maximum number of keepalive probes TCP should send before dropping the connection.
+         *
+         * @since 5.3
+         */
+        public Builder setTcpKeepCount(final int tcpKeepCount) {
+            this.tcpKeepCount = tcpKeepCount;
+            return this;
+        }
+
         /**
          * The address of the SOCKS proxy to use.
          */
@@ -497,6 +573,7 @@ public final class IOReactorConfig {
                     tcpNoDelay,
                     trafficClass,
                     sndBufSize, rcvBufSize, backlogSize,
+                    tcpKeepIdle, tcpKeepInterval, tcpKeepCount,
                     socksProxyAddress, socksProxyUsername, socksProxyPassword);
         }
 
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/reactor/SingleCoreIOReactor.java b/httpcore5/src/main/java/org/apache/hc/core5/reactor/SingleCoreIOReactor.java
index 95d764320..598f27078 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/reactor/SingleCoreIOReactor.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/reactor/SingleCoreIOReactor.java
@@ -31,6 +31,7 @@ import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.net.SocketAddress;
+import java.net.SocketOption;
 import java.net.UnknownHostException;
 import java.nio.channels.CancelledKeyException;
 import java.nio.channels.ClosedChannelException;
@@ -51,6 +52,11 @@ import org.apache.hc.core5.net.NamedEndpoint;
 import org.apache.hc.core5.util.Args;
 import org.apache.hc.core5.util.Timeout;
 
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPCOUNT;
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPIDLE;
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPINTERVAL;
+import static org.apache.hc.core5.util.ReflectionUtils.getExtendedSocketOptionOrNull;
+
 class SingleCoreIOReactor extends AbstractSingleCoreIOReactor implements ConnectionInitiator {
 
     private static final int MAX_CHANNEL_REQUESTS = 10000;
@@ -186,7 +192,7 @@ class SingleCoreIOReactor extends AbstractSingleCoreIOReactor implements Connect
             final SocketChannel socketChannel = entry.channel;
             final Object attachment = entry.attachment;
             try {
-                prepareSocket(socketChannel.socket());
+                prepareSocket(socketChannel);
                 socketChannel.configureBlocking(false);
             } catch (final IOException ex) {
                 logException(ex);
@@ -261,7 +267,8 @@ class SingleCoreIOReactor extends AbstractSingleCoreIOReactor implements Connect
         return sessionRequest;
     }
 
-    private void prepareSocket(final Socket socket) throws IOException {
+    private void prepareSocket(final SocketChannel socketChannel) throws IOException {
+        final Socket socket = socketChannel.socket();
         socket.setTcpNoDelay(this.reactorConfig.isTcpNoDelay());
         socket.setKeepAlive(this.reactorConfig.isSoKeepAlive());
         if (this.reactorConfig.getSndBufSize() > 0) {
@@ -277,6 +284,27 @@ class SingleCoreIOReactor extends AbstractSingleCoreIOReactor implements Connect
         if (linger >= 0) {
             socket.setSoLinger(true, linger);
         }
+        if (this.reactorConfig.getTcpKeepIdle() > 0) {
+            setExtendedSocketOption(socketChannel, TCP_KEEPIDLE, this.reactorConfig.getTcpKeepIdle());
+        }
+        if (this.reactorConfig.getTcpKeepInterval() > 0) {
+            setExtendedSocketOption(socketChannel, TCP_KEEPINTERVAL, this.reactorConfig.getTcpKeepInterval());
+        }
+        if (this.reactorConfig.getTcpKeepInterval() > 0) {
+            setExtendedSocketOption(socketChannel, TCP_KEEPCOUNT, this.reactorConfig.getTcpKeepCount());
+        }
+    }
+
+    /**
+     * @since 5.3
+     */
+    <T> void setExtendedSocketOption(final SocketChannel socketChannel,
+                                             final String optionName, final T value) throws IOException {
+        final SocketOption<T> socketOption = getExtendedSocketOptionOrNull(optionName);
+        if (socketOption == null) {
+            throw new UnsupportedOperationException(optionName + " is not supported in the current jdk");
+        }
+        socketChannel.setOption(socketOption, value);
     }
 
     private void validateAddress(final SocketAddress address) throws UnknownHostException {
@@ -311,7 +339,7 @@ class SingleCoreIOReactor extends AbstractSingleCoreIOReactor implements Connect
 
     private void processConnectionRequest(final SocketChannel socketChannel, final IOSessionRequest sessionRequest) throws IOException {
         socketChannel.configureBlocking(false);
-        prepareSocket(socketChannel.socket());
+        prepareSocket(socketChannel);
 
         validateAddress(sessionRequest.localAddress);
         if (sessionRequest.localAddress != null) {
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/util/NetUtils.java b/httpcore5/src/main/java/org/apache/hc/core5/util/NetUtils.java
new file mode 100644
index 000000000..5dd3d6aca
--- /dev/null
+++ b/httpcore5/src/main/java/org/apache/hc/core5/util/NetUtils.java
@@ -0,0 +1,39 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.util;
+
+import org.apache.hc.core5.annotation.Internal;
+
+@Internal
+public class NetUtils {
+
+    public static final String TCP_KEEPIDLE = "TCP_KEEPIDLE";
+    public static final String TCP_KEEPINTERVAL = "TCP_KEEPINTERVAL";
+    public static final String TCP_KEEPCOUNT = "TCP_KEEPCOUNT";
+
+}
diff --git a/httpcore5/src/main/java/org/apache/hc/core5/util/ReflectionUtils.java b/httpcore5/src/main/java/org/apache/hc/core5/util/ReflectionUtils.java
index fad06bca2..08f741b8c 100644
--- a/httpcore5/src/main/java/org/apache/hc/core5/util/ReflectionUtils.java
+++ b/httpcore5/src/main/java/org/apache/hc/core5/util/ReflectionUtils.java
@@ -27,7 +27,10 @@
 
 package org.apache.hc.core5.util;
 
+import java.io.IOException;
+import java.lang.reflect.Field;
 import java.lang.reflect.Method;
+import java.net.SocketOption;
 
 import org.apache.hc.core5.annotation.Internal;
 
@@ -45,11 +48,24 @@ public final class ReflectionUtils {
     }
 
     public static <T> T callGetter(final Object object, final String getterName, final Class<T> resultType) {
+        return callGetter(object, getterName, null, null, resultType);
+    }
+
+    public static <T> T callGetter(final Object object, final String getterName, final Object arg, final Class argType, final Class<T> resultType) {
         try {
             final Class<?> clazz = object.getClass();
-            final Method method = clazz.getMethod("get" + getterName);
-            method.setAccessible(true);
-            return resultType.cast(method.invoke(object));
+            final Method method;
+            if (arg != null) {
+                assert argType != null;
+                method = clazz.getMethod("get" + getterName, argType);
+                method.setAccessible(true);
+                return resultType.cast(method.invoke(object, arg));
+            } else {
+                assert argType == null;
+                method = clazz.getMethod("get" + getterName);
+                method.setAccessible(true);
+                return resultType.cast(method.invoke(object));
+            }
         } catch (final Exception ignore) {
             return null;
         }
@@ -72,4 +88,39 @@ public final class ReflectionUtils {
         return 7;
     }
 
+    /**
+     * @since 5.3
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> SocketOption<T> getExtendedSocketOptionOrNull(final String fieldName) {
+        try {
+            final Class<?> extendedSocketOptionsClass = Class.forName("jdk.net.ExtendedSocketOptions");
+            final Field field = extendedSocketOptionsClass.getField(fieldName);
+            return (SocketOption)field.get((Object)null);
+        } catch (final Exception ignore) {
+            return null;
+        }
+    }
+
+    /**
+     * object can be ServerSocket or Socket
+     *
+     * @since 5.3
+     */
+    public static <T> void setOption(final T object, final String fieldName, final T value) throws IOException {
+        try {
+            final Class<?> serverSocketClass = object.getClass();
+            final Method setOptionMethod = serverSocketClass.getMethod("setOption", SocketOption.class, Object.class);
+            final SocketOption<Integer> socketOption = getExtendedSocketOptionOrNull(fieldName);
+            if (socketOption == null) {
+                throw new UnsupportedOperationException(fieldName + " is not supported in the current jdk");
+            }
+            setOptionMethod.invoke(object, socketOption, value);
+        } catch (final UnsupportedOperationException e) {
+            throw e;
+        } catch (final Exception e) {
+            throw new IOException("failed to call setOption", e);
+        }
+    }
+
 }
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/IOReactorConfigTest.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/IOReactorConfigTest.java
index 9c4942771..ff1bb120b 100644
--- a/httpcore5/src/test/java/org/apache/hc/core5/reactor/IOReactorConfigTest.java
+++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/IOReactorConfigTest.java
@@ -48,6 +48,9 @@ class IOReactorConfigTest {
                 .setSndBufSize(32767)
                 .setRcvBufSize(8192)
                 .setBacklogSize(5)
+                .setTcpKeepIdle(100)
+                .setTcpKeepInterval(12)
+                .setTcpKeepCount(4)
                 .setSocksProxyAddress(new InetSocketAddress(8888))
                 .setSocksProxyUsername("socksProxyUsername")
                 .setSocksProxyPassword("socksProxyPassword")
@@ -64,6 +67,9 @@ class IOReactorConfigTest {
         Assertions.assertEquals(32767, reactorConfig.getSndBufSize());
         Assertions.assertEquals(8192, reactorConfig.getRcvBufSize());
         Assertions.assertEquals(5, reactorConfig.getBacklogSize());
+        Assertions.assertEquals(100, reactorConfig.getTcpKeepIdle());
+        Assertions.assertEquals(12, reactorConfig.getTcpKeepInterval());
+        Assertions.assertEquals(4, reactorConfig.getTcpKeepCount());
         Assertions.assertEquals(new InetSocketAddress(8888), reactorConfig.getSocksProxyAddress());
         Assertions.assertEquals("socksProxyUsername", reactorConfig.getSocksProxyUsername());
         Assertions.assertEquals("socksProxyPassword", reactorConfig.getSocksProxyPassword());
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/util/TestReflectionUtils.java b/httpcore5/src/test/java/org/apache/hc/core5/util/TestReflectionUtils.java
new file mode 100644
index 000000000..b29c448a9
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/util/TestReflectionUtils.java
@@ -0,0 +1,114 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.core5.util;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import javax.net.ServerSocketFactory;
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketOption;
+
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPCOUNT;
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPIDLE;
+import static org.apache.hc.core5.util.NetUtils.TCP_KEEPINTERVAL;
+import static org.apache.hc.core5.util.ReflectionUtils.callGetter;
+import static org.apache.hc.core5.util.ReflectionUtils.determineJRELevel;
+import static org.apache.hc.core5.util.ReflectionUtils.setOption;
+import static org.apache.hc.core5.util.ReflectionUtils.getExtendedSocketOptionOrNull;
+
+public class TestReflectionUtils {
+
+    @Test
+    public void testGetExtendedSocketOptionOrNull() {
+        testGetExtendedSocketOption(TCP_KEEPIDLE);
+        testGetExtendedSocketOption(TCP_KEEPINTERVAL);
+        testGetExtendedSocketOption(TCP_KEEPCOUNT);
+    }
+
+    private <T> void testGetExtendedSocketOption(final String option) {
+        final SocketOption<T> socketOption = getExtendedSocketOptionOrNull(option);
+        // 1.Partial versions of jdk1.8 contain TCP_KEEPIDLE, TCP_KEEPINTERVAL, TCP_KEEPCOUNT.
+        // 2. Windows may not support TCP_KEEPIDLE, TCP_KEEPINTERVAL, TCP_KEEPCOUNT.
+        if (determineJRELevel() > 8 && isWindows() == false) {
+            Assertions.assertNotNull(socketOption);
+        }
+    }
+
+    @Test
+    public void testSetOption() throws IOException {
+        if (determineJRELevel() > 8 && isWindows() == false) {
+            {
+                // test Socket
+                final Socket sock = new Socket();
+                setOption(sock, TCP_KEEPIDLE, 20);
+                setOption(sock, TCP_KEEPINTERVAL, 21);
+                setOption(sock, TCP_KEEPCOUNT, 22);
+
+                final SocketOption<Integer> tcpKeepIdle = getExtendedSocketOptionOrNull(TCP_KEEPIDLE);
+                assert tcpKeepIdle != null;
+                Assertions.assertEquals(20, callGetter(sock, "Option", tcpKeepIdle, SocketOption.class, Integer.class));
+
+                final SocketOption<Integer> tcpKeepInterval = getExtendedSocketOptionOrNull(TCP_KEEPINTERVAL);
+                assert tcpKeepInterval != null;
+                Assertions.assertEquals(21, callGetter(sock, "Option", tcpKeepInterval, SocketOption.class, Integer.class));
+
+                final SocketOption<Integer> tcpKeepCount = getExtendedSocketOptionOrNull(TCP_KEEPCOUNT);
+                assert tcpKeepCount != null;
+                Assertions.assertEquals(22, callGetter(sock, "Option", tcpKeepCount, SocketOption.class, Integer.class));
+            }
+
+            {
+                // test ServerSocket
+                final ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket();
+                setOption(serverSocket, TCP_KEEPIDLE, 20);
+                setOption(serverSocket, TCP_KEEPINTERVAL, 21);
+                setOption(serverSocket, TCP_KEEPCOUNT, 22);
+
+                final SocketOption<Integer> tcpKeepIdle = getExtendedSocketOptionOrNull(TCP_KEEPIDLE);
+                assert tcpKeepIdle != null;
+                Assertions.assertEquals(20, callGetter(serverSocket, "Option", tcpKeepIdle, SocketOption.class, Integer.class));
+
+                final SocketOption<Integer> tcpKeepInterval = getExtendedSocketOptionOrNull(TCP_KEEPINTERVAL);
+                assert tcpKeepInterval != null;
+                Assertions.assertEquals(21, callGetter(serverSocket, "Option", tcpKeepInterval, SocketOption.class, Integer.class));
+
+                final SocketOption<Integer> tcpKeepCount = getExtendedSocketOptionOrNull(TCP_KEEPCOUNT);
+                assert tcpKeepCount != null;
+                Assertions.assertEquals(22, callGetter(serverSocket, "Option", tcpKeepCount, SocketOption.class, Integer.class));
+            }
+        }
+    }
+
+    public static boolean isWindows() {
+        return System.getProperty("os.name").contains("Windows");
+    }
+
+}