Резюме: Я использую класс 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.
Почему бы вам не попробовать реализацию okHTTP? См. Ссылку http://square.github.io/okhttp/ –
Дождитесь, пока Google начнет использовать источник OpenJDK. Тогда это произойдет автоматически. – EJP
@EJP: Возможно, хотя я бы не стал ставить на него свою жизнь. И это не решает мою непосредственную проблему, потому что Android N еще далеко, и некоторые устройства никогда не получат обновление. Это не просто проблема низкой производительности клиента; это может быть проблемой для сервера во время высокой нагрузки, если каждый клиент устанавливает множество соединений, когда им действительно нужно только 1 или 2. – Michael