На сегодняшний день в Google Play насчитывается более 800 тысяч приложений. Многие из них реализованы на основе клиент-серверного общения. При разработке таких приложений нужно учесть три основных момента, о которых пойдет речь в этой статье.
О чем нужно помнить при реализации сетевой части приложения
Первое – это трафик. Не всегда есть возможность работать по бесплатному Wi-Fi-соединению, а мобильный интернет всё еще дорогой, и об этом нужно помнить, потому что трафик – это деньги пользователя.
Второе – это лимит батарейки. Мобильные устройства необходимы пользователю для каких-то повседневных дел, совещаний, прогулок, бизнеса, и когда батарейка садится в самый неподходящий момент, пользователь негодует.
Третье – это безопасность. Так как все-таки речь идет о мобильных клиентах, и данные гуляют по сети от клиента к серверу и обратно, то их необходимо защищать.
Подходы по реализации сетевого взаимодействия
Для начала вспомним, какие способы реализации клиент-серверного общения существуют и популярны на сегодняшний день.
Первый подход — на основе сокетов (здесь я имею в виду работу непосредственно с Socket API). Он часто используется в приложениях, где важна скорость доставки сообщения, важен порядок доставки сообщений и необходимо держать стабильное соединение с сервером. Такой способ зачастую реализуется в мессенджерах и играх.
Второй подход — это частые опросы (polling): клиент посылает запрос на сервер и говорит ему: «Дай мне свежие данные»; сервер отвечает на запрос клиента и отдает все, что у него накопилось к этому моменту.
Минус такого подхода в том, что клиент не знает, появились ли свежие данные на сервере. По сети лишний раз гоняется трафик, в первую очередь из-за частых установок соединений с сервером.
Третий подход — длинные опросы (long polling) — заключается в том, что клиент посылает «ожидающий» запрос на сервер. Сервер смотрит, есть ли свежие данные для клиента, если их нет, то он держит соединение с клиентом до тех пор, пока эти данные не появятся. Как только данные появились, он «пушит» их обратно клиенту. Клиент, получив данные от сервера, тут же посылает следующий «ожидающий» запрос и т.д.
Реализация этого подхода достаточно сложна на мобильном клиенте в первую очередь из-за нестабильности мобильного соединения. Зато при этом подходе трафика расходуется меньше, чем при обычном polling’e, т.к. сокращается количество установок соединений с сервером.
Механизм long polling, или пуш-уведомлений (push notifications), реализован в самой платформе Android. И, наверное, для большинства задач будет лучше использовать его, а не реализовывать самим. Ваше приложение подписывается у сервиса Google Cloud Messaging (GCM) на получение пуш-уведомлений.
Тем самым разрывается связь непосредственно между сервером и клиентом за счет того, что сервер работает с сервисом GCM и отправляет свежие данные всегда на этот сервис, а он уже в свою очередь реализует всю логику доставки этих данных до вашего приложения. Плюсы этого подхода в том, что устраняется необходимость частых установок соединения с сервером за счет того, что вы точно знаете, что данные появились, и об этом вас оповещает сервис GCM.
Из этих четырех подходов наиболее популярными при разработке бизнес-приложений являются пуш-уведомления и частые опросы. При реализации этих подходов нам так или иначе придется устанавливать соединение с сервером и передавать данные. Далее речь пойдет об инструментах, которые есть в наличии у разработчика для работы по HTTP/HTTPS-протоколам.
HttpUrlConnection и HttpClient
В арсенале Android-разработчика есть два класса для работы по этим протоколам. Первый – это java.net.HttpURLConnection, второй – org.apache.http.client.HttpClient. Обе эти библиотеки включены в Android SDK. Далее будут подробно рассмотрены основные моменты, которые будут влиять на трафик, батарею и безопасность при работе с каждой из этих библиотек.
С HttpURLConnection все просто. Один класс и все. Это объясняется тем, что родительский класс URLConnection был спроектирован для работы не только по HTTP-протоколу, а еще по таким, как file, mailto, ftp и т.п.
HttpClient спроектирован более объектно-ориентированно. В нем есть четкое разделение абстракций. В самом просто случае мы будем работать с пятью разными интерфейсами: HttpRequest, HttpResponse, HttpEntity и HttpContext. Поэтому апачевский клиент намного тяжеловеснее HttpUrlConnection.
Как правило, на все приложение существует всего один экземпляр класса HttpClient. Это обусловлено его тяжеловесностью. Использование отдельного экземпляра на каждый запрос будет расточительным. Мы можем, к примеру, хранить экземпляр HTTP-клиента в наследнике класса Application.
В случае HttpUrlConnection следует создавать на каждый запрос новый экземпляр клиента.
Из чего складывается трафик?
Во время работы нашего приложения картинка будет примерно такая.
Количество и частота запросов будет зависеть от функционала и насыщенности UI – интерфейса приложения. Каждый такой запрос устанавливает TCP-соединение с сервером. В данном случае трафик, который будет потрачен, будет равняться сумме установок соединений и сумме переданных данных. Понизить расход трафика в данном случае можно за счет использования долгоживущего соединения (keep alive).
Основная идея keep alive-соединения заключается в использовании одного и то же TCP-соединения для отправки и приема HTTP-запросов. Главные преимущества — снижение трафика и времени выполнения запроса. Мной был проделан простенький тест, который заключался в том, что выполнялось последовательно 10 запросов на один и тот же хост. Данные представлены в таблице ниже. При выключенном keep alive видно, что среднее время выполнения запроса составляло примерно две секунды. В случае с включенным keep alive это время снизилось до 1,7 секунды, что на 16% быстрее. Это обуславливается в первую очередь тем, что устраняется необходимость частой установки соединения с сервером. При использовании защищенного HTTPS-соединения разница была бы заметнее, т.к. процедура SSL Handshake гораздо тяжелее процедуры TCP Handshake.
Важным параметром keep alive-cоединения является keep alive duration. Он означает временной интервал. Если приходит несколько HTTP-запросов в пределах этого интервала, то будет переиспользоваться уже установленное TCP-соединение.
Из рисунка видно, что время между четвертым и третьим запросом превысило keep alive duration, поэтому создается новое TCP-соединение с сервером.
Давайте посмотрим, как можно настроить вышеописанный параметр. В случае HttpClient все хорошо, в нашем распоряжении есть интерфейс ConnectionKeepAliveStrategy. Переопределив метод getKeepAliveDuration, мы можем вернуть нужное значение параметра keep alive duration.
httpClient.setKeepAliveStrategy(
new ConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(
HttpResponse response, HttpContext context) {
return KEEP_ALIVE_DURATION_MILLISECONDS;
}
});
Работая с HttpUrlConnection, нужно помнить о баге в платформе Android 2.2 и отключать keep alive на платформах <= 2.2.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
System.setProperty("http.keepAlive", "false");
}
Второй неприятный (может быть, только для меня) момент при работе с HttpUrlConnection заключается в том, что параметр keep alive duration не поддается настройке (я легального способа не нашел, если кто подскажет — буду признателен). По умолчанию он примерно равен 5 секундам.
Также при работе с keep alive-соединением необходимо стараться вычитывать данные полностью из установленного соединения (коннекшена). Если не донца вычитывать данные, то можно получить коннекшены, которые буду висеть в пуле и «думать», что их еще кто-то собирается читать. Если InputStream успешно получен, читайте тело ответа полностью. Полное вычитывание очищает коннекшн от данных, и этот коннекшн будет переиспользован с большей вероятностью.
Работа с кукой (cookie)
Кука — небольшой фрагмент данных, отправленный веб-сервером и хранимый на компьютере пользователя. В нашем случае компьютером является Android-устройство. На практике куки обычно используются для аутентификации пользователя, хранения его персональных предпочтений и настроек, отслеживания состояния сессии доступа пользователя, ведения статистики о пользователях. Большое количество сервисов начинает разрабатывать мобильные приложения после того, как уже сделана мобильная версия сайта. В такой ситуации интересен вопрос: «Можно ли, получив авторизационную куку в мобильном приложении, установить ее в браузер (в WebView)?». При решении такой задачи есть пара деталей, которые помогут сэкономить ваше время:
- На плафтормах >= 4.0.3 (API Level 15) должна стоять точка в начале домена
- После вызова метода sync() у CookieSyncManager кука проставится только в WebView внутри вашего приложения, а в браузере — нет. Это ограничение накладывает система Android в целях безопасности
Защищенное соединение (HTTPS)
В завершение данной статьи я рассмотрю, как включить HTTPS в Android. Насколько мне известно, на других мобильных платформах достаточно включить HTTPS-схему, механизм транспорта SSL — и все должно работать. В Android есть некоторые проблемы, которые следует учитывать и решать. Для начала вспомним, как устанавливается защищенное соединение. На проблемное место указывает красная стрелка – это проверка подлинности сертификата.
На платформах < Android 4.0 при попытке выполнить сетевой запрос по HTTPS вылетит SSLHandshakeException. Возможность установить доверенный сертификат появилась на Android 4.0 с помощью KeyChain API. Но как быть с платформами ниже 4.0? На этих плаформах у нас остается два пути:
- Создавать локальное хранилище сертификатов
- Доверять любым сертификатам. В этом случае трафик тоже будет шифроваться, но угроза атаки man in the middle останется
Если выбор пал на создание локального хранилища сертификатов, то после того как оно будет создано, подключить его можно следующим образом:
— в случае HttpUrlConnection:
…
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(algorithm);
KeyStore keyStore = KeyStore.getInstance("BKS");
InputStream in =
context.getResources().openRawResource(mykeystore);
keyStore.load(in, "mysecret".toCharArray());
in.close();
tmf.init(keyStore);
SSLContext sslc = SSLContext.getInstance("TLS");
sslc.init(null, tmf.getTrustManagers(),new SecureRandom());
…
— в случае HttpClient:
private SSLSocketFactory createSslSocketFactory() {
SSLSocketFactory sf = null;
try {
KeyStore keyStore = KeyStore.getInstance("BKS");
InputStream in =
context.getResources().openRawResource(mykeystore);
keyStore.load(in, "mysecret".toCharArray());
in.close();
sf = new SSLSocketFactory(keyStore);
sf.setHostnameVerifier(STRICT_HOSTNAME_VERIFIER);
} catch (Exception e) {
e.printStackTrace();
}
return sf;
}.
C помощью KeyStore.getInstance(«BKS») мы получаем объект KeyStore, который поддерживает Bounce Castle KeyStore format (Java-пакет для работы с крипто-алгоритмами). mykeystore – id ресурса, в котором лежат сертификаты.
mysecret – пароль от KeyStore. Более подробную информацию можно найти по ссылке на локальное хранилище сертификатов, приведенной выше.
Если же выбор пал на «Доверять любым сертификатом», то достаточно реализовать два интерфейса следующим образом:
private class DummyHostnameVerifier implements HostnameVerifier{
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
private class DummyTrustManager implements X509TrustManager{
@Override
public void checkClientTrusted(...) throws CertificateException {
//empty
}
@Override
public void checkServerTrusted(...) throws CertificateException {
//empty
}
...
}
Затем следует применить эти реализации либо для HttpClient, либо для HttpUrlConnection.
А что с батарейкой?
Расход батареи напрямую зависит от количества устанавливаемых соединений с сервером, т.к. каждая установка активизирует беспроводное радио, которое расходует заряд батареи. О том, как работает беспроводное радио в Android и что влияет на расход батареи, рекомендую почитать в этой статье: Transferring Data Without Draining the Battery.
Прочитав статью, вы наверняка зададитесь вопросом, какой инструмент использовать — HttpClient или HttpUrlConnection? Разработчики платформы Android рекомендуют в новых приложениях использовать HttpUrlConnection, т.к. он прост в использовании, его будут развивать дальше и адаптировать под платформу. HttpClient следует использовать на платформах ниже Android 2.3, в первую очередь из-за серьезного бага с keep alive-соединением.
Эти три момента мы, конечно, учитываем и при разработке наших мобильных приложений. А о чем, в первую очередь, думаете вы?
Автор: azacepin