setrequestproperty getoutputstream example ejemplo java android tcp httpsurlconnection

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.