2016-01-12 4 views
21

Резюме: Я использую класс HttpsUrlConnection в приложении для Android, чтобы отправить несколько запросов в последовательном порядке по TLS. Все запросы одного типа и отправляются на один и тот же хост. Сначала я получал новое TCP-соединение для каждого запроса. Я смог это исправить, но не без других проблем на некоторых версиях Android, связанных с readTimeout. Я надеюсь, что будет более надежный способ повторного использования TCP-соединений.Повторное использование соединений TCP с HttpsUrlConnection


фон

При проверке сетевого трафика в Android приложение, которое я сейчас работаю с Wireshark я заметил, что каждый запрос привел к новому соединению TCP создается и новый TLS квитирование выполняется. Это приводит к достаточной задержке, особенно если вы находитесь на 3G/4G, где каждая поездка в оба конца может занять сравнительно много времени. Затем я попытался использовать тот же сценарий без TLS (то есть HttpUrlConnection). В этом случае я видел только одно TCP-соединение, которое было установлено, а затем повторно использовалось для последующих запросов. Таким образом, поведение с новыми установленными TCP-соединениями было специфичным для HttpsUrlConnection.

Вот пример кода, чтобы проиллюстрировать проблему (реальный код, очевидно, имеет проверку сертификата, обработку ошибок и т.д.):

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();   
} 

Примечание: В моем реальном коде я использую запросы POST, поэтому я использую оба выходной поток (для записи тела запроса) и входной поток (чтобы прочитать тело ответа). Но я хотел, чтобы этот пример был коротким и простым.

Если я вызываю метод testRequest раз я в конечном итоге следующее в Wireshark (в сокращенном виде):

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

ли не называть меня conn.disconnect не оказывает никакого влияния на поведение.

Итак, я изначально хотя «Хорошо, я создам пул HttpsUrlConnection объектов и повторное использование установленных соединений, когда это возможно». К сожалению, нет кубиков, так как Http(s)UrlConnection экземпляры, видимо, не предназначены для повторного использования. Действительно, считывание данных ответа приводит к закрытию выходного потока, и попытка повторного открытия выходного потока запускает java.net.ProtocolException с сообщением об ошибке "cannot write request body after response has been read".

Следующая вещь, которую я должен был рассмотреть путь, в котором настройке HttpsUrlConnection отличается от создания в HttpUrlConnection, а именно, что вы создаете SSLContext и SSLSocketFactory. Поэтому я решил сделать оба этих static и поделиться ими для всех запросов.

Это похоже на работу в том смысле, что я получил повторное использование соединения. Но в некоторых версиях Android возникла проблема, когда все запросы, кроме первого, занимали очень много времени. При дальнейшем осмотре я заметил, что звонок getOutputStream будет блокироваться в течение времени, равного таймауту с setReadTimeout.

Моя первая попытка исправить это было добавить еще один звонок к setReadTimeout с очень небольшим значением после того, как я закончил читать данные ответа, но это, казалось, вообще не имело никакого эффекта.
То, что я сделал тогда, было установлено гораздо более короткий тайм-аут чтения (несколько сотен миллисекунд) и реализовать собственный механизм повтора, который пытается повторно считывать данные ответа до тех пор, пока все данные не будут прочитаны или не будет достигнут первоначальный намеченный тайм-аут.
Увы, теперь я получал тайм-ауты установления связи TLS на некоторых устройствах. Так что я тогда должен был добавить звонок к setReadTimeout с довольно большим значением прямо перед вызовом getOutputStream, а затем изменить тайм-аут чтения на пару сотен мс перед чтением данных ответа. На самом деле это казалось солидным, и я тестировал его на 8 или 10 различных устройствах, запускал разные версии Android и получил желаемое поведение на всех из них.

Быстрая перемотка вперед на пару недель, и я решил проверить свой код на Nexus 5, работающем с последним заводским изображением (6.0.1 (MMB29S)). Теперь я вижу ту же проблему, где getOutputStream будет блокировать на протяжении всего моего readTimeout по каждому запросу, кроме первого.

Update 1: Побочный эффект всех TCP соединений будучи установленным, что на некоторых Android версии (4,1 - 4,3 IIRC), что можно запустить на ошибку в ОС, где ваш процесс в конечном итоге работает (?) из дескрипторов файлов. Это вряд ли произойдет в реальных условиях, но это может быть вызвано автоматическим тестированием.

Update 2:OpenSSLSocketImpl class имеет публичный setHandshakeTimeout метод, который может быть использован для указания квитирования тайм-аут, который отделен от ReadTimeOut. Однако, поскольку этот метод существует для сокета, а не HttpsUrlConnection, это немного сложно вызвать его. И хотя это возможно, в этот момент вы полагаетесь на детали реализации классов, которые могут или не могут быть использованы в результате открытия HttpsUrlConnection.

Вопрос

Это кажется невероятным мне, что повторное соединение не должно «просто работать», так что я предполагаю, что я делаю что-то неправильно. Есть ли кто-нибудь, кто смог надежно получить HttpsUrlConnection для повторного использования подключений на Android и может определить любую ошибку (-ы), которую я делаю? Я бы очень хотел избежать использования каких-либо сторонних библиотек, если это совершенно неизбежно.
Обратите внимание, что все идеи, которые вы могли бы думать о необходимости работать с minSdkVersion из 16.

+1

Почему бы вам не попробовать реализацию okHTTP? См. Ссылку http://square.github.io/okhttp/ –

+0

Дождитесь, пока Google начнет использовать источник OpenJDK. Тогда это произойдет автоматически. – EJP

+0

@EJP: Возможно, хотя я бы не стал ставить на него свою жизнь. И это не решает мою непосредственную проблему, потому что Android N еще далеко, и некоторые устройства никогда не получат обновление. Это не просто проблема низкой производительности клиента; это может быть проблемой для сервера во время высокой нагрузки, если каждый клиент устанавливает множество соединений, когда им действительно нужно только 1 или 2. – Michael

ответ

1

Я предлагаю вам попробовать повторно использовать SSLContexts вместо того, чтобы создавать новую каждый раз, и изменения значения по умолчанию для HttpURLConnection Дитто. Он обязательно заблокирует объединение пулов, как у вас.

NB getAcceptedIssuers() не разрешено возвращать null.

+0

«Я предлагаю вам попробовать повторно использовать« SSLContexts »вместо создания нового каждый раз». _ «Следующее, что я сделал, это рассмотреть способ, которым настройка« HttpsUrlConnection »отличается от настройки« HttpUrlConnection », а именно: вы создаете« SSLContext »и« SSLSocketFactory ». ** Поэтому я решил сделать оба эти 'static' и делиться ими для всех запросов **." _ "' getAcceptedIssuers() 'не разрешено возвращать null" _ ", настоящий код, очевидно, имеет проверку сертификата, обработку ошибок и т. д." _ – Michael

+0

@Michael I комментируя код, который вы опубликовали. Если это не настоящий код, ваш вопрос бесполезен. Пожалуйста, исправьте свой вопрос. – EJP

+0

Я не являюсь владельцем авторских прав на код приложения, поэтому я не могу поделиться с кем-либо. Код в вопросе является минимальным примером того, как воспроизводить исходную проблему без повторного использования соединения, если кто-то заинтересован в ее тестировании. Затем я объясняю все, что я пытался изменить, и результаты этих изменений. Если вы чувствуете, что эти объяснения + пример недостаточно ясны, я полагаю, что я мог бы собрать еще один пример, который объединяет исходный пример с этими изменениями. Придется ждать до понедельника. – Michael

Смежные вопросы