Created
April 20, 2021 08:26
-
-
Save Camerash/f3ca4a930d537306c040ec3555865a76 to your computer and use it in GitHub Desktop.
Enable TCP Keepalive per application in Android using OkHttp
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import okhttp3.OkHttpClient; | |
public class OkHttpUtil { | |
public static OkHttpClient getOkHttpClient() { | |
return new OkHttpClient.Builder() | |
.socketFactory(new TCPKeepaliveSocketFactory(60, 60, 5)) | |
.build(); | |
} | |
public static OkHttpClient getSSLOkHttpClient() { | |
return new OkHttpClient.Builder() | |
.sslSocketFactory(new TCPKeepaliveSSLSocketFactory(getSSLSocketFactory(), 60, 60, 5), getX509TrustManager()) | |
.build(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.IOException; | |
import java.net.InetAddress; | |
import java.net.Socket; | |
import java.net.UnknownHostException; | |
import javax.net.SocketFactory; | |
public class TCPKeepaliveSocketFactory extends SocketFactory { | |
private final int idleTimeout; | |
private final int interval; | |
private final int count; | |
public TCPKeepaliveSocketFactory(int idleTimeout, int interval, int count) { | |
this.idleTimeout = idleTimeout; | |
this.interval = interval; | |
this.count = count; | |
} | |
@Override | |
public Socket createSocket(String host, int port) throws IOException, UnknownHostException { | |
Socket socket = new Socket(host, port); | |
TCPKeepaliveSocketUtil.setTCPKeepAlive(socket, idleTimeout, interval, count); | |
return socket; | |
} | |
@Override | |
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { | |
Socket socket = new Socket(host, port, localHost, localPort); | |
TCPKeepaliveSocketUtil.setTCPKeepAlive(socket, idleTimeout, interval, count); | |
return socket; | |
} | |
@Override | |
public Socket createSocket(InetAddress host, int port) throws IOException { | |
Socket socket = new Socket(host, port); | |
TCPKeepaliveSocketUtil.setTCPKeepAlive(socket, idleTimeout, interval, count); | |
return socket; | |
} | |
@Override | |
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { | |
Socket socket = new Socket(address, port, localAddress, localPort); | |
TCPKeepaliveSocketUtil.setTCPKeepAlive(socket, idleTimeout, interval, count); | |
return socket; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.system.Os; | |
import java.io.FileDescriptor; | |
import java.lang.reflect.Field; | |
import java.net.Socket; | |
public class TCPKeepaliveSocketUtil { | |
/** | |
* See https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml | |
*/ | |
private final static int SOL_TCP_LEVEL = 6; | |
/** | |
* See https://cs.android.com/android/platform/superproject/+/android-10.0.0_r30:bionic/libc/kernel/uapi/linux/tcp.h | |
*/ | |
private final static int TCP_KEEPIDLE_OPTION = 4; | |
private final static int TCP_KEEPINTVL_OPTION = 5; | |
private final static int TCP_KEEPCNT_OPTION = 6; | |
/** | |
* Enable and configure TCP Keep Alive on provided socket | |
* Notice the socket should not be an SSLSocket as it's file descriptor will be unrecognisable by the OS | |
* To use an SSLSocket, pass a normal socket to this function first, then construct an SSLSocket using the normal socket. | |
* | |
* @param socket Socket to be configured. | |
* @param idleTimeout The interval between the last data packet sent (simple ACKs are not considered data) and the first keepalive probe in seconds | |
* @param interval The interval between subsequential keepalive probes in seconds, regardless of what the connection has exchanged in the meantime | |
* @param count The number of unacknowledged probes to send before considering the connection dead and notifying the application layer | |
*/ | |
@SuppressWarnings("JavaReflectionMemberAccess") | |
public static void setTCPKeepAlive(Socket socket, int idleTimeout, int interval, int count) { | |
try { | |
socket.setKeepAlive(true); | |
Field socketImplField = Class.forName("java.net.Socket").getDeclaredField("impl"); | |
if (socketImplField != null) { | |
socketImplField.setAccessible(true); | |
Object socketImpl = socketImplField.get(socket); | |
Field fileDescriptorField = Class.forName("java.net.SocketImpl").getDeclaredField("fd"); | |
if (fileDescriptorField != null) { | |
fileDescriptorField.setAccessible(true); | |
final FileDescriptor fileDescriptor = (FileDescriptor) fileDescriptorField.get(socketImpl); | |
Os.setsockoptInt(fileDescriptor, SOL_TCP_LEVEL, TCP_KEEPIDLE_OPTION, idleTimeout); | |
Os.setsockoptInt(fileDescriptor, SOL_TCP_LEVEL, TCP_KEEPINTVL_OPTION, interval); | |
Os.setsockoptInt(fileDescriptor, SOL_TCP_LEVEL, TCP_KEEPCNT_OPTION, count); | |
} | |
} | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.IOException; | |
import java.net.InetAddress; | |
import java.net.Socket; | |
import java.net.UnknownHostException; | |
import javax.net.ssl.SSLSocketFactory; | |
public class TCPKeepaliveSSLSocketFactory extends SSLSocketFactory { | |
private final SSLSocketFactory internalSSLSocketFactory; | |
private int idleTimeout = 50; | |
private int interval = 50; | |
private int count = 3; | |
public TCPKeepaliveSSLSocketFactory(SSLSocketFactory sslSocketFactory) { | |
this.internalSSLSocketFactory = sslSocketFactory; | |
} | |
public TCPKeepaliveSSLSocketFactory(SSLSocketFactory sslSocketFactory, int idleTimeout, int interval, int count) { | |
this.internalSSLSocketFactory = sslSocketFactory; | |
this.idleTimeout = idleTimeout; | |
this.interval = interval; | |
this.count = count; | |
} | |
@Override | |
public Socket createSocket(String host, int port) throws IOException, UnknownHostException { | |
Socket socket = internalSSLSocketFactory.createSocket(host, port); | |
TCPKeepaliveSocketUtil.setTCPKeepAlive(socket, idleTimeout, interval, count); | |
return socket; | |
} | |
@Override | |
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { | |
Socket socket = internalSSLSocketFactory.createSocket(host, port, localHost, localPort); | |
TCPKeepaliveSocketUtil.setTCPKeepAlive(socket, idleTimeout, interval, count); | |
return socket; | |
} | |
@Override | |
public Socket createSocket(InetAddress host, int port) throws IOException { | |
Socket socket = internalSSLSocketFactory.createSocket(host, port); | |
TCPKeepaliveSocketUtil.setTCPKeepAlive(socket, idleTimeout, interval, count); | |
return socket; | |
} | |
@Override | |
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { | |
Socket socket = internalSSLSocketFactory.createSocket(address, port, localAddress, localPort); | |
TCPKeepaliveSocketUtil.setTCPKeepAlive(socket, idleTimeout, interval, count); | |
return socket; | |
} | |
@Override | |
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { | |
TCPKeepaliveSocketUtil.setTCPKeepAlive(s, idleTimeout, interval, count); // Use underlying real socket to set TCP keep alive instead of wrapped SSLSocket | |
return internalSSLSocketFactory.createSocket(s, host, port, autoClose); | |
} | |
@Override | |
public String[] getDefaultCipherSuites() { | |
return internalSSLSocketFactory.getDefaultCipherSuites(); | |
} | |
@Override | |
public String[] getSupportedCipherSuites() { | |
return internalSSLSocketFactory.getSupportedCipherSuites(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Based on this fantastic answer on StackOverflow, updated with new Android API and adapted to OkHttp
Tested working on both Android 9 and 10