java - getoutputstream - Reutilizando conexiones TCP con HttpsUrlConnection
httpurlconnection java example (1)
Le sugiero que intente reutilizar SSLContexts
lugar de crear uno nuevo cada vez y cambie el valor predeterminado para el HttpURLConnection
. Es seguro inhibir la agrupación de conexiones de la forma en que lo tiene.
NB getAcceptedIssuers()
no tiene permitido devolver nulo.
Resumen ejecutivo: estoy usando la clase HttpsUrlConnection
en una aplicación de Android para enviar una cantidad de solicitudes, de manera serial, a través de TLS. Todas las solicitudes son del mismo tipo y se envían al mismo host. Al principio obtendría una nueva conexión TCP para cada solicitud. Pude solucionarlo, pero no sin causar otros problemas en algunas versiones de Android relacionadas con readTimeout. Espero que haya una forma más robusta de lograr la reutilización de la conexión TCP.
Fondo
Al inspeccionar el tráfico de red de la aplicación de Android en la que estoy trabajando con Wireshark, observé que cada solicitud daba como resultado que se establecía una nueva conexión TCP y que se realizaba un nuevo protocolo de enlace TLS. Esto resulta en una buena cantidad de latencia, especialmente si está en 3G / 4G, donde cada viaje de ida y vuelta puede llevar un tiempo relativamente largo. Luego intenté el mismo escenario sin TLS (es decir, HttpUrlConnection
). En este caso, solo vi que se establecía una única conexión TCP y luego se reutilizaba para solicitudes posteriores. Por lo tanto, el comportamiento con las nuevas conexiones TCP que se establecieron fue específico de HttpsUrlConnection
.
Aquí hay un código de ejemplo para ilustrar el problema (el código real obviamente tiene validación de certificado, manejo de errores, etc.):
class NullHostNameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
protected void testRequest(final String uri) {
new AsyncTask<Void, Void, Void>() {
protected void onPreExecute() {
}
protected Void doInBackground(Void... params) {
try {
URL url = new URL("https://www.ssllabs.com/ssltest/viewMyClient.html");
try {
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null,
new X509TrustManager[] { new X509TrustManager() {
@Override
public void checkClientTrusted( final X509Certificate[] chain, final String authType ) {
}
@Override
public void checkServerTrusted( final X509Certificate[] chain, final String authType ) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
} },
new SecureRandom());
} catch (Exception e) {
}
HttpsURLConnection.setDefaultHostnameVerifier(new NullHostNameVerifier());
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(sslContext.getSocketFactory());
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Android");
// Consume the response
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
StringBuffer response = new StringBuffer();
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
conn.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
protected void onPostExecute(Void result) {
}
}.execute();
}
Nota: en mi código real, uso las solicitudes POST, así que uso tanto el flujo de salida (para escribir el cuerpo de la solicitud) como el flujo de entrada (para leer el cuerpo de la respuesta). Pero quería mantener el ejemplo corto y simple.
Si llamo el método testRequest
repetidamente, testRequest
lo siguiente en Wireshark (resumido):
TCP 61047 -> 443 [SYN]
TLSv1 Client Hello
TLSv1 Server Hello
TLSv1 Certificate
TLSv1 Server Key Exchange
TLSv1 Application Data
TCP 61050 -> 443 [SYN]
TLSv1 Client Hello
TLSv1 Server Hello
TLSv1 Certificate
... and so on, for each request ...
Si llamo o no a conn.disconnect
no tiene ningún efecto en el comportamiento.
Así que inicialmente pensé: "Bien, crearé un grupo de objetos HttpsUrlConnection
y reutilizaré las conexiones establecidas cuando sea posible". Sin dados, desafortunadamente, las instancias de Http(s)UrlConnection
aparentemente no están destinadas a ser reutilizadas De hecho, la lectura de los datos de respuesta hace que la secuencia de salida se cierre, y al intentar volver a abrir la secuencia de salida se activa una java.net.ProtocolException
con el mensaje de error "cannot write request body after response has been read"
.
Lo siguiente que hice fue considerar la forma en que la configuración de una HttpsUrlConnection
diferencia de la configuración de una HttpUrlConnection
, es decir, que creas un SSLContext
y una SSLSocketFactory
. Así que decidí hacer ambas cosas static
y compartirlas para todas las solicitudes.
Esto pareció funcionar bien en el sentido de que obtuve la reutilización de la conexión. Pero hubo un problema en ciertas versiones de Android donde todas las solicitudes, excepto la primera, tardarían mucho tiempo en ejecutarse. Tras una inspección adicional, getOutputStream
que la llamada a getOutputStream
se bloquearía durante un período de tiempo igual al tiempo de espera establecido con setReadTimeout
.
Mi primer intento de arreglar eso fue agregar otra llamada a setReadTimeout
con un valor muy pequeño después de que termine de leer los datos de respuesta, pero eso parece no tener ningún efecto.
Lo que hice entonces fue establecer un tiempo de espera de lectura mucho más corto (un par de cientos de milisegundos) e implementar mi propio mecanismo de reintento que intenta leer los datos de respuesta repetidamente hasta que se hayan leído todos los datos o se haya alcanzado el tiempo de espera original previsto.
Por desgracia, ahora estaba teniendo tiempo de espera de TLS en algunos dispositivos. Entonces, lo que hice fue agregar una llamada a setReadTimeout
con un valor bastante grande justo antes de llamar a getOutputStream
, y luego cambiar el tiempo de espera de lectura a un par de cientos de ms antes de leer los datos de respuesta. Esto realmente parecía sólido, y lo probé en 8 o 10 dispositivos diferentes, ejecutando diferentes versiones de Android, y obtuve el comportamiento deseado en todos ellos.
Avancé un par de semanas y decidí probar mi código en un Nexus 5 con la última imagen de fábrica (6.0.1 (MMB29S)). Ahora estoy viendo el mismo problema donde getOutputStream
se bloqueará durante la duración de mi readTimeout en cada solicitud, excepto la primera.
Actualización 1: Un efecto colateral de todas las conexiones TCP que se están estableciendo es que en algunas versiones de Android (4.1 - 4.3 IIRC) es posible encontrar un error en el sistema operativo (?) Donde su proceso finalmente se queda sin descriptores de archivos. Esto no es probable que ocurra en condiciones reales, pero puede activarse mediante pruebas automáticas.
Actualización 2: la clase OpenSSLSocketImpl tiene un método público setHandshakeTimeout
que se podría usar para especificar un tiempo de espera de handshake independiente del readTimeout. Sin embargo, dado que este método existe para el socket en lugar de la HttpsUrlConnection
, es un poco difícil invocarlo. Y aunque es posible hacerlo, en ese momento está confiando en los detalles de implementación de las clases que pueden o no usarse como resultado de abrir una HttpsUrlConnection
.
La pregunta
Me parece improbable que la reutilización de la conexión no deba "simplemente funcionar", así que supongo que estoy haciendo algo mal. ¿Hay alguien que haya conseguido que HttpsUrlConnection
pueda reutilizar las conexiones en Android de manera confiable y pueda detectar los errores que estoy cometiendo? Realmente me gustaría evitar recurrir a las bibliotecas de terceros a menos que sea completamente inevitable.
Tenga en cuenta que cualquier idea que pueda pensar necesita trabajar con una minSdkVersion
de 16.