Работа с сетью в Android: трафик, безопасность и батарейка

    На сегодняшний день в 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)?». При решении такой задачи есть пара деталей, которые помогут сэкономить ваше время:

    1. На плафтормах >= 4.0.3 (API Level 15) должна стоять точка в начале домена
    2. После вызова метода sync() у CookieSyncManager кука проставится только в WebView внутри вашего приложения, а в браузере — нет. Это ограничение накладывает система Android в целях безопасности




    Защищенное соединение (HTTPS)

    В завершение данной статьи я рассмотрю, как включить HTTPS в Android. Насколько мне известно, на других мобильных платформах достаточно включить HTTPS-схему, механизм транспорта SSL — и все должно работать. В Android есть некоторые проблемы, которые следует учитывать и решать. Для начала вспомним, как устанавливается защищенное соединение. На проблемное место указывает красная стрелка – это проверка подлинности сертификата.



    На платформах < Android 4.0 при попытке выполнить сетевой запрос по HTTPS вылетит SSLHandshakeException. Возможность установить доверенный сертификат появилась на Android 4.0 с помощью KeyChain API. Но как быть с платформами ниже 4.0? На этих плаформах у нас остается два пути:

    1. Создавать локальное хранилище сертификатов
    2. Доверять любым сертификатам. В этом случае трафик тоже будет шифроваться, но угроза атаки 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-соединением.

    Эти три момента мы, конечно, учитываем и при разработке наших мобильных приложений. А о чем, в первую очередь, думаете вы?
    Mail.ru Group
    Building the Internet

    Comments 19

      +4
      Стоит упомянуть, что в последей версии «пушей» от гугла есть несколько важных нововведений.

      Например, поддержка так называемого «upstream messaging», то есть обмен сообщениями не в одну сторону ( Your Server => GCM Cloud => Device, как это есть сейчас), но еще и в обратную ( Device <=> GCM Cloud <=> Your Server ).

      Называется оно CCS ( developer.android.com/google/gcm/ccs.html ), и поскольку использует XMPP как транспорт (кстати говоря, Device => GCM Cloud использует тот же XMPP), то можно реализовать перманентное (условно перманентное, потому что имеют место разрывы сети итд) соединение не только между Device и GCM Cloud (как сейчас), но и между GCM Service и Your Server, что позволяет сократить время обмена сообщениями между девайсом и Вашим сервисом за счет времени коннекта и хендшейка.
        0
        Да, это интересно, спасибо за ссылку. Я немного слышал об этом, на практике пока не довелось попробовать. Вы, кстати, нашли уже применение этой технологии?
          0
          Да, я писал proof of concept мессенджер, с использованием AppEngine как бекенда для хранения regid и роутинга сообщений и CCS как транспорта.
          Хотя апстрим там используeтся исключительно для подписки на получение пуш-нотификакий.
        +4
        Интересно читать эту статью здесь (особенно когда рядом реклама: «Агент Mail.Ru cо звонками»). Запустил я как то раз этот агент у себя на телефоне, так он как начал жрать мою батарею. И продолжал жрать пока я его процесс не убил.
          +3
          Иногда мне приходится использовать программы, которые жрут батарею. И удалять — не выход.
          Немного помогает Greenify (только для тех, кто уже получил root)
            +13
            Мэйл Агент не грех удалить с любого девайса.
          0
          Что-то не нашел в статье упоминания об Android Volley Library.
            0
            Спасибо за замечание. Еще не успел посмотреть на Volley Library в действии, т.к. всего месяц назад ее презентовали на Google IO 2013, обязательно скоро попробую и может быть добавлю в статью.
            +1
            Мое личное ИМХО относительно абстрактных моделей Android Framework — скоро Gingerbread уйдет в небытие — и соответственно использование HttpClient от Apache уже медленно, но верно теряет смысл, HttpURLConnection выйдет на первый план. Кстати, вот ссылочки на неплохие обертки, работают как на Java так и на Android (здесь и здесь)

            Меня больше интересует другой момент, как Вы производите отладку http-трафика? При использовании HttpClient спасал их Logger, а вот как Вы справляетесь при использовании HttpURLConnection? Я к сожалению не смог найти чего-либо более вменяемого, чем отладка трафика с эмулятора через Wireshark.
              0
              скоро Gingerbread уйдет в небытие — и соответственно использование HttpClient от Apache уже медленно, но верно теряет смысл,

              что уйдет в небытие — согласен, насчет как скоро сказать не решусь, т.к. плафтормы <= 2.3 все еще занимают порядка 25% из всех Android OS.

              Меня больше интересует другой момент, как Вы производите отладку http-трафика?

              Для отладки используем тоже Wireshark, также Network Traffic Tool. Еще рекомендую посмотреть в сторону новой библиотеки для работы с сетью Volley, наверняка, там есть фичи, связанные с отладкой трафика.
              +1
              Наш безопасник использует Charles для отладки HTTP- и HTTPS-трафика. Можно ставить брейкпоинты на определённые запросы, дропать их.
                0
                Да, штучка интересная — говорят хорошая альтернатива для wireshark, возьму на заметку, спасибо!
                  0
                  Да, ещё я сам использовал ZAP code.google.com/p/zaproxy/, но он тормозной местами, а безопасник использует Charles, т.к. ему нужно редактировать запросы и ответы.

                  Wireshark — штука хорошая, но для высокоуровневых протоколов вроде HTTP не очень удобная. Вот ручную работу с сокетами через него отлаживать — самое то.
              +3
              А о чем, в первую очередь, думаете вы?

              Я в первую очередь думаю, что речь в статье о доставке обновлений с сервера на клиент, а не об общем случае сетевого взаимодействия в вакууме — это исходя из описания. Во вторую очередь я думаю о том, что для этого кейза есть сто раз описанная и обговоренная связка типа:

              1) если размер обновлений невелик, то сервер пересылает все данные обновлений в самой пуш-нотификации
              2) если размер обновлений велик, то сервер пересылает в пуш-нотификации только сигнал о наличии и количестве обновлений
              3) клиент либо сразу обновляет контент в случае 1) либо делает запрос на сервер и получает пачку апдейтов в случае 2)

              Все, других формул нет. Единственный возможный вариант — это свой long-polling для каких-либо нужд. Но не используют его не потому что

              Реализация этого подхода достаточно сложна на мобильном клиенте в первую очередь из-за нестабильности мобильного соединения

              а потому что «у нас сервер такого количества одновременно висящих соединений не выдержит» или «наши программисты не смогут такое сделать быстро, а клиент нужен вчера». Да и в общем случае встроенного механизма более чем хватает.

              В третью очередь, глядя на эту статью, я думаю о том, что каждый раз, когда вставал вопрос о Keep-Alive в соединении (который, черт побери, опять же создает серьезную нагрузку на сервер) — всегда это происходило в одном и том же случае:

              — Ребята, а где у нас запрос на получение контента Вьюшки Номер Один?
              — Чувак, глянь спеку API, десяток разных запросов — и будет тебе счастье.
              — Сколько?!!!
              — Ну, десять, что ли… А потом еще по три запроса на каждый из десяти

              И тут, что характерно, есть два решения. Первое — это оптимизировать в поте лица, но это паршивое решение. Это означает, что ты а) борешься со следствием, а не с причиной б) огребаешь последствия в виде усложения структуры приложения в) поощряешь рост профнепригодности парней на серверной стороне, или по крайней мере того гения, который такой API разработал. Хорошее решение — это сделать один (я настаиваю, один, не два, не три, а ровно один) аггрегирующий запрос для мобильного приложения, либо дать возможность каким-либо образом создавать произвольные аггрегирующие запросы.

              Второй неприятный (может быть, только для меня) момент при работе с HttpUrlConnection заключается в том, что параметр keep alive duration не поддается настройке (я легального способа не нашел, если кто подскажет — буду признателен).

              Правильно не поддается. Потому что время жизни keep-alive соединения настраивается на сервере, там же настраивается и пресловутый keep-alive duration, который на самом деле keep-alive timeout. Если оно настроено, то сервер в респонсе вернет что-то вроде

              Keep-Alive: timeout=5, max=100
              Connection: Keep-Alive

              Соответственно, при использовании HttpUrlConnection необходимо будет самому трекать timeout, определяя, какой коннекшн еще можно использовать, а какой нет, а при использовании HttpClient пробросить этот параметр как значение getKeepAliveDuration (пример тут)
                0
                Спасибо за комментарий.

                Я в первую очередь думаю, что речь в статье о доставке обновлений с сервера на клиент


                Речь в статье, в первую очередь, какие аспекты при работе с http клиентами (urlconnection и httpclient) влияют потребление трафика, безопасность и расход батареи.

                Во вторую очередь я думаю о том, что для этого кейза есть сто раз описанная и обговоренная связка типа:

                1) если размер обновлений невелик, то сервер пересылает все данные обновлений в самой пуш-нотификации
                2) если размер обновлений велик, то сервер пересылает в пуш-нотификации только сигнал о наличии и количестве обновлений
                3) клиент либо сразу обновляет контент в случае 1) либо делает запрос на сервер и получает пачку апдейтов в случае 2)


                согласен, много описано об этом: Google I/O 2010 — Building push applications for Android, Google I/O 2012 — Google Cloud Messaging for Android и Google I/O 2013 — Google Cloud Messaging. Поэтому в этой статье об этом ничего и не рассказывается, а просто упоминается в начале о механизме пуш-уведомлений.

                Все, других формул нет. Единственный возможный вариант — это свой long-polling для каких-либо нужд.

                не соглашусь. Обычный polling — это тоже способ доставки обновлений на клиент. Далеко не все приложения используют GCM или пишут свою реализацию long-polling'a.

                а при использовании HttpClient пробросить этот параметр как значение getKeepAliveDuration


                вы не внимательно прочитали статью, о том как настроить keep alive duration в HttpClient в ней рассказывается.
                  0
                  не соглашусь. Обычный polling — это тоже способ доставки обновлений на клиент. Далеко не все приложения используют GCM или пишут свою реализацию long-polling'a.

                  Способ — да, хороший в плане баланса «реалтайм-нагрузка на сервер» — нет.
                  вы не внимательно прочитали статью, о том как настроить keep alive duration в HttpClient в ней рассказывается.

                  Статью я прочитал внимательно. Ссылку на пример я скинул потому, что там верно сделана обработка keep alive timeout. В случае изменения времени жизни коннекта на сервере ваш код вызовет снижение скорости работы с сетью у всех юзеров. Код по ссылке — только у тех, у кого очередной злобный проксятник отрежет хедер Keep-Alive.
                    0
                    Способ — да, хороший в плане баланса «реалтайм-нагрузка на сервер» — нет.

                    Cогласен
                    Код по ссылке — только у тех, у кого очередной злобный проксятник отрежет хедер Keep-Alive.

                    Приму к сведению. Спасибо.

              Only users with full accounts can post comments. Log in, please.