2016-01-12 10 views
21

Sommario esecutivo: Sto utilizzando la classe HttpsUrlConnection in un'applicazione Android per inviare un numero di richieste, in modo seriale, su TLS. Tutte le richieste sono dello stesso tipo e vengono inviate allo stesso host. All'inizio avrei avuto una nuova connessione TCP per ogni richiesta. Sono stato in grado di risolverlo, ma non senza causare altri problemi su alcune versioni di Android relative a readTimeout. Spero che ci sarà un modo più efficace per ottenere il riutilizzo della connessione TCP.Riutilizzo di connessioni TCP con HttpsUrlConnection


Sfondo

Ispezionando il traffico di rete del app Android su cui sto lavorando con Wireshark ho osservato che ogni richiesta ha provocato una nuova connessione TCP sia stabilita, e una nuova stretta di mano TLS in corso di esecuzione. Ciò si traduce in una buona dose di latenza, specialmente se si è in 3G/4G, in cui ogni round trip può impiegare un tempo relativamente lungo. Ho quindi provato lo stesso scenario senza TLS (ad esempio HttpUrlConnection). In questo caso ho visto solo una singola connessione TCP stabilita e quindi riutilizzata per richieste successive. Pertanto, il comportamento con le nuove connessioni TCP stabilite era specifico per HttpsUrlConnection.

Ecco qualche esempio di codice per illustrare il problema (il codice vero e proprio, ovviamente, ha la convalida dei certificati, la gestione degli errori, ecc):

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: Nel mio codice vero e proprio io uso le richieste POST, per cui uso sia la flusso di output (per scrivere il corpo della richiesta) e il flusso di input (per leggere il corpo della risposta). Ma volevo mantenere l'esempio breve e semplice.

Se chiamo il metodo testRequest volte finisco i seguenti nella Wireshark (abbreviata):

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 ... 

O se non che io chiamo conn.disconnect non ha alcun effetto sul comportamento.

Quindi inizialmente pensavo "Ok, creerò un pool di oggetti HttpsUrlConnection e riutilizzerò le connessioni stabilite quando possibile". Niente dado, sfortunatamente, le istanze di Http(s)UrlConnection apparentemente non sono pensate per essere riutilizzate. Infatti, la lettura dei dati di risposta provoca la chiusura del flusso di output e il tentativo di riaprire il flusso di output innesca uno java.net.ProtocolException con il messaggio di errore "cannot write request body after response has been read".

La prossima cosa che ho fatto è stato quello di prendere in considerazione il modo in cui la creazione di un HttpsUrlConnection differisce dalla creazione di un HttpUrlConnection, vale a dire che si crea un SSLContext ed un SSLSocketFactory. Così ho deciso di rendere entrambi quelli static e condividerli per tutte le richieste.

Questo sembra funzionare bene nel senso che ho ottenuto il riutilizzo della connessione. Ma c'era un problema su alcune versioni di Android in cui tutte le richieste, tranne la prima, richiederebbero molto tempo per l'esecuzione. Dopo un'ulteriore ispezione ho notato che la chiamata a getOutputStream avrebbe bloccato per un periodo di tempo uguale al timeout impostato con setReadTimeout.

Il mio primo tentativo di aggiustare quello era aggiungere un'altra chiamata a setReadTimeout con un valore molto piccolo dopo che ho finito di leggere i dati di risposta, ma che sembrava non avere alcun effetto.
Quello che ho fatto è stato impostare un timeout di lettura molto più breve (un paio di centinaia di millisecondi) e implementare il mio meccanismo di tentativi che tenta di leggere i dati di risposta ripetutamente fino a quando tutti i dati sono stati letti o il timeout originariamente previsto è stato raggiunto.
Ahimè, ora ricevevo timeout di handshake TLS su alcuni dispositivi. Quindi, ciò che ho fatto è stato aggiungere una chiamata a setReadTimeout con un valore piuttosto grande appena prima di chiamare getOutputStream e quindi modificare il timeout di lettura di nuovo a un paio di centinaia di ms prima di leggere i dati di risposta. Questo in realtà sembrava solido, e l'ho testato su 8 o 10 dispositivi diversi, eseguendo diverse versioni di Android, e ho ottenuto il comportamento desiderato su tutti loro.

Avanzamento veloce un paio di settimane e ho deciso di testare il mio codice su un Nexus 5 con l'ultima immagine di fabbrica (6.0.1 (MMB29S)). Ora vedo lo stesso problema in cui getOutputStream verrà bloccato per la durata del mio readTimeout su ogni richiesta tranne la prima.

Update 1: Un effetto collaterale di tutte le connessioni TCP di essere stabiliti è che su alcune versioni di Android (4,1-4,3 IIRC) è possibile imbattersi in un bug nel sistema operativo in cui il processo alla fine viene eseguito (?) fuori dai descrittori di file. È improbabile che ciò accada in condizioni reali, ma può essere attivato da test automatici.

Update 2: IlOpenSSLSocketImpl class ha un metodo pubblico setHandshakeTimeout che potrebbe essere utilizzato per specificare un timeout stretta di mano che è separato dal ReadTimeout. Tuttavia, poiché questo metodo esiste per il socket piuttosto che per lo HttpsUrlConnection, è un po 'complicato evocarlo. E anche se è possibile farlo, a quel punto si sta facendo affidamento sui dettagli di implementazione delle classi che potrebbero essere o meno utilizzati a seguito dell'apertura di uno HttpsUrlConnection.

La domanda

Mi sembra improbabile a me che il collegamento riutilizzo non dovrebbe "solo lavoro", in modo da sto indovinando che sto facendo qualcosa di sbagliato. C'è qualcuno che è riuscito a ottenere in modo affidabile lo HttpsUrlConnection per riutilizzare le connessioni su Android e può individuare gli errori che sto facendo? Mi piacerebbe davvero evitare di ricorrere a qualsiasi libreria di terze parti a meno che non sia assolutamente inevitabile.
Nota che qualunque idee si potrebbe pensare di necessità di lavorare con un minSdkVersion di 16.

+1

Perché non provi l'implementazione di okHTTP? Vedi link http://square.github.io/okhttp/ –

+0

Attendi fino a quando Google inizia a utilizzare il sorgente OpenJDK. Quindi avverrà automaticamente. – EJP

+0

@EJP: Forse, anche se non ci scommetterei la vita. E non risolve il mio problema immediato, perché Android N è ancora lontano e alcuni dispositivi non riceveranno mai l'aggiornamento. Neanche questo è solo un aspetto delle scarse prestazioni del cliente; può essere potenzialmente un problema per il server durante i periodi di alto carico se ogni cliente stabilisce molte connessioni quando hanno solo bisogno di 1 o 2. – Michael

risposta

1

vi consiglio di provare a riutilizzare SSLContexts invece di crearne uno nuovo ogni volta e cambiare il default per HttpURLConnection idem. È sicuro di inibire la connessione mettendo in comune il modo in cui lo hai.

NB getAcceptedIssuers() non è autorizzato a restituire null.

+0

"Suggerisco di provare a riutilizzare' SSLContexts' invece di crearne uno nuovo ogni volta ". _ "La prossima cosa che ho fatto è stata considerare il modo in cui la configurazione di un' HttpsUrlConnection' differisce dall'impostare un 'HttpUrlConnection', ovvero che tu crei un' SSLContext' e un 'SSLSocketFactory'. ** Quindi ho deciso di fare entrambi questi 'static' e li condividono per tutte le richieste **." _ "' getAcceptedIssuers() 'non può restituire null" _ "il codice reale ha ovviamente convalida del certificato, gestione degli errori, ecc." _ – Michael

+0

@ Michael I sto commentando il codice che hai postato Se questo non è il vero codice, la tua domanda è inutile. Per favore aggiusta la tua domanda. – EJP

+0

Non possiedo il copyright del codice dell'applicazione, quindi non è qualcosa che posso condividere con chiunque. Il codice nella domanda è un esempio minimale di come riprodurre il problema originale del mancato riutilizzo della connessione, se qualcuno è interessato a testarlo. Spiego quindi tutte le cose che ho tentato di cambiare e i risultati di tali cambiamenti. Se ritieni che quelle spiegazioni + l'esempio non siano abbastanza chiare, suppongo che potrei mettere insieme un altro esempio che combina l'esempio originale con quelle modifiche. Dovrà aspettare fino a lunedì però. – Michael

Problemi correlati