How to set the keepalive timeout in Android?
I'd like to lower the TCP keepalive t开发者_如何学Pythonime on a Socket I'm opening from 2 hours to something on the order of ten minutes. I can make it use keepalive with socket.setKeepAlive(true), but how can I control the time before a keepalive packet is sent?
It looks like I could do this if I was using the NDK, but I want to distribute this code as a jar, so that's not ideal for me.
I think it might be quite important to be able to set the keepalive timeouts on an per app level, especially on a mobile device, because it might be under bad network conditions (wifi/mobile). If the app does not send (m)any data but uses a persistent connection, the socket will not detect whether the connection is lost, unless it sends tcp keepalive probes. Setting this option is usually possible via the setsockopt(2) call, but the android sdk provides only the setKeepAlive(boolean)
option. Deeper in the stack, that functions calls libcore.io.ForwardingOs.setsockoptInt(...), which is not available directly, nor the required file descriptor. By using java reflection, setting the keepalive timeouts is possible anyway, e.g like this:
private final static int SOL_TCP = 6;
private final static int TCP_KEEPIDLE = 4;
private final static int TCP_KEEPINTVL = 5;
private final static int TCP_KEEPCNT = 6;
protected void setKeepaliveSocketOptions(Socket socket, int idleTimeout, int interval, int count) {
try {
socket.setKeepAlive(true);
try {
Field socketImplField = Class.forName("java.net.Socket").getDeclaredField("impl");
if(socketImplField != null) {
socketImplField.setAccessible(true);
Object plainSocketImpl = socketImplField.get(socket);
Field fileDescriptorField = Class.forName("java.net.SocketImpl").getDeclaredField("fd");
if(fileDescriptorField != null) {
fileDescriptorField.setAccessible(true);
FileDescriptor fileDescriptor = (FileDescriptor)fileDescriptorField.get(plainSocketImpl);
Class libCoreClass = Class.forName("libcore.io.Libcore");
Field osField = libCoreClass.getDeclaredField("os");
osField.setAccessible(true);
Object libcoreOs = osField.get(libCoreClass);
Method setSocketOptsMethod = Class.forName("libcore.io.ForwardingOs").getDeclaredMethod("setsockoptInt", FileDescriptor.class, int.class, int.class, int.class);
if(setSocketOptsMethod != null) {
setSocketOptsMethod.invoke(libcoreOs, fileDescriptor, SOL_TCP, TCP_KEEPIDLE, idleTimeout);
setSocketOptsMethod.invoke(libcoreOs, fileDescriptor, SOL_TCP, TCP_KEEPINTVL, interval);
setSocketOptsMethod.invoke(libcoreOs, fileDescriptor, SOL_TCP, TCP_KEEPCNT, count);
}
}
}
}
catch (Exception reflectionException) {}
} catch (SocketException e) {}
}
This works at least until the following requirements are met:
libcore.io.ForwardingOs.setsockoptInt/4
exists at current sdk versionjava.net.Socket
has animpl
member at the current sdk versionjava.net.Socket->impl
is instance ofjava.net.SocketImpl
at the current sdk versionjava.net.SocketImpl
has afd
member at the current sdk versionTCP_KEEPIDLE
,TCP_KEEPINTVL
andTCP_KEEPCNT
have the same values (4
,5
and6
) at the current sdk version and all android devices / architectures.
That seems to be true at least for android versions from 4.0.1 / November 2011 up to recent version 5.1.1 r9.
See luni/src/main/java/libcore/io/Os.java
, luni/src/main/java/java/net/Socket.java
and luni/src/main/java/java/net/SocketImpl.java
from the platform/libcore repository.
TCP_KEEPIDLE
, TCP_KEEPINTVL
and TCP_KEEPCNT
seem to have the same values for android versions since 2.2.3 r2 and all architectures. This can be validated e.g. by executing find . -name tcp.h | xargs grep -ho "TCP_KEEP\w\+\s\+\d\+" | sort | uniq -c
in the android platform/ndk repository.
Android is based on Linux, and Linux supports the TCP_KEEPIDLE
and TCP_KEEPINTVL
socket options via the setsocketopt()
function, which is wrapped by the java.net.SocketOptions
interface. java.net.SocketImpl
implements SocketOptions
, and java.net.Socket
wraps a SocketImpl
. What I don't know is whether it is possible to access the SocketImpl
of a given Socket
object.
What you could try doing is use Socket.setSocketImplFactory()
to implement your own custom SocketImplFactory
class, which is responsible for creating SocketImpl
instances for Socket
objects. That way, your factory could call SocketOptions.setOption()
for TCP_KEEPIDLE
and TCP_KEEPINTVL
for any sockets your app creates.
This is probably too obvious an answer [i.e. it's not an option for your specific case] but you can of course implement your own keepalive by sending 1 throw-away byte (in either direction) every 10 minutes.
Digging into high voted answer and reflection restrictions on my Android 10 device i found a working solutions on modern Android APIs.
Firstly, checking the blocklists of java's sysem methods for restriction setsockoptInt
on different Android versions i found 4 packages with this method and the only package without block was core Android android.system.Os
public package. Surprisingly, this package allows to acess low-level system functions without using java reflection directly.
Started from Android 8 (api 26) this package enriched with Os.setsockoptInt
method.
The second, getting FileDescriptor
of socket can be also more gracefully by casting SocketInputStream
to a FileInputStream
as in that answer.
In result the code that works very well is here:
private final static int SOL_TCP = 6;
private final static int TCP_KEEPIDLE = 4;
private final static int TCP_KEEPINTVL = 5;
private final static int TCP_KEEPCNT = 6;
protected void setKeepAliveSocketOptions(Socket socket, int idleTimeout, int interval, int count) {
try {
socket.setKeepAlive(true);
FileDescriptor socketFileDescriptor = ((FileInputStream)socket.getInputStream()).getFD();
if (socketFileDescriptor != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Os.setsockoptInt(socketFileDescriptor, SOL_TCP, TCP_KEEPIDLE, idleTimeout);
Os.setsockoptInt(socketFileDescriptor, SOL_TCP, TCP_KEEPINTVL, interval);
Os.setsockoptInt(socketFileDescriptor, SOL_TCP, TCP_KEEPCNT, count);
} else {
Class libCoreClass = Class.forName("libcore.io.Libcore");
Field osField = libCoreClass.getDeclaredField("os");
osField.setAccessible(true);
Object libcoreOs = osField.get(libCoreClass);
Method setSocketOptsMethod = Class.forName("libcore.io.ForwardingOs")
.getDeclaredMethod("setsockoptInt", FileDescriptor.class, int.class, int.class, int.class);
setSocketOptsMethod.invoke(libcoreOs, socketFileDescriptor, SOL_TCP, TCP_KEEPIDLE, idleTimeout);
setSocketOptsMethod.invoke(libcoreOs, socketFileDescriptor, SOL_TCP, TCP_KEEPINTVL, interval);
setSocketOptsMethod.invoke(libcoreOs, socketFileDescriptor, SOL_TCP, TCP_KEEPCNT, count);
}
}
} catch (Exception e) {
if (BuildConfig.DEBUG) e.printStackTrace();
}
}
精彩评论