Статьи

Поговорим: эффективная связь для PHP и Android, часть 2

Первая часть этой серии статей была посвящена настройке приложения Android для выполнения HTTP-запроса. Во второй части мы сконцентрируемся на реализации сериализации и сжатия данных на стороне запросов Android и PHP.

Определение формата сериализации данных

Прежде чем данные могут быть переданы клиенту, они должны быть сериализованы в поток данных, который клиент может впоследствии преобразовать обратно в полезную структуру данных. В вашей службе PHP REST вам необходимо определить наиболее подходящий формат сериализации данных для использования. Это включает в себя анализ заголовка Accept, отправленного клиентом, чтобы определить, какой формат поддерживается сервером.

Если вы используете среду PHP для создания службы REST, вам следует ознакомиться с ее документацией о том, как это сделать, но вот как может выглядеть тело метода для определения формата ответа:

<?php
// Define types supported by the server
$supportedTypes = array();
if (extension_loaded("json")) {
    $supportedTypes["application/json"] = "json_encode";
}
if (extension_loaded("msgpack")) {
    $supportedTypes["application/x-msgpack"] = "msgpack_pack";
}

// Get client-supported media types ordered from most to least preferred 
$typeEntries = array_map("trim", explode(",", $_SERVER["HTTP_ACCEPT"])); 
$acceptedTypes = array(); 
foreach ($typeEntries as $entry) { 
    $entry = preg_split('#;s*q=#', $entry); 
    $mediaType = array_shift($entry); 
    $qualityValue = count($entry) ? array_shift($entry) : 1; 
    $acceptedTypes[$mediaType] = $qualityValue; 
}
arsort($acceptedTypes); 

// Find the most preferred media type supported by client and server 
$supportedMediaTypes = array_keys($supportedTypes);
foreach ($acceptedTypes as $mediaType => $qualityValue) { 
    $pattern = "#" . str_replace("*", ".*", $mediaType) . "#"; 
    if ($matches = preg_grep($pattern, $supportedMediaTypes)) { 
        $supportedType = reset($matches); 
        return array($supportedType, $supportedTypes[$supportedType]);
    }
}
return null;

Этот код анализирует значение заголовка Accept

Вы можете использовать это значение в своем ответе, как в примере ниже, где $contentType Если взаимно поддерживаемый формат не может быть найден, соответствующий код ответа возвращается согласно RFC 2616, раздел 10.4.7 .

 <?php
if ($contentType) {
    list ($name, $callback) = $contentType;
    header($_SERVER["SERVER_PROTOCOL"] . " 200 OK");
    header("Content-Type: " . $name);
    $data = call_user_func($callback, $data);
}
else {
    header($_SERVER["SERVER_PROTOCOL"] . " 406 Not Acceptable");
}

Определение формата сжатия данных

После того, как данные сериализуются, они должны быть сжаты, чтобы минимизировать объем полосы пропускания, необходимый для передачи данных с сервера на клиент. Этот процесс определения формата сжатия данных аналогичен определению формата сериализации данных, но вместо этого анализирует заголовок Accept-Encoding Это позволяет вам определять порядок приоритета, если клиент поддерживает несколько форматов сжатия.

Опять же, если у вас есть выбранный PHP-фреймворк, вам следует обратиться к его документации, чтобы узнать, есть ли более распространенный способ сделать это, но я включил необработанный пример PHP ниже. Дополнительные кодировки могут быть легко добавлены, но я включил те, которые наиболее часто используются в HTTP-ответах с bzip2, имеющим наивысший приоритет, потому что он имеет лучшее сжатие.

 <?php
// Determine what encodings the client supports 
$clientEncodings = array_map("trim", explode(",", $_SERVER["HTTP_ACCEPT_ENCODING"])); 

// Determine what encodings the server supports 
$serverEncodings = array(); 
if (extension_loaded("bz2"))  {
    $serverEncodings["bzip2"] = "bzcompress"; 
} 
if (extension_loaded("zlib"))  {
    $serverEncodings["gzip"] = "gzdeflate"; 
    $serverEncodings["deflate"] = "gzcompress"; 
} 

// Return an encoding supported by both if one is found 
foreach ($serverEncodings as $encoding => $callback)  {
    if (in_array($encoding, $clientEncodings))  {
        return array($encoding, $callback); 
    } 
} 
return array();

В приведенном ниже примере $encoding Если найден поддерживаемый формат сжатия данных, его имя возвращается в соответствующем заголовке согласно RFC 2616, раздел 14.11 .

 <?php
if ($encoding) {
    list ($name, $callback) = $encoding;
    header("Content-Encoding: " . $name);
    $data = call_user_func($callback, $data);
}

Хотя некоторые форматы сериализации данных могут показаться более эффективными до применения сжатия, это не обязательно означает, что это также будет сохраняться после применения сжатия. Например, MessagePack часто более компактен, чем JSON до сжатия, но менее компактен в большинстве случаев после сжатия. Таким образом, вы должны протестировать различные комбинации сериализации и сжатия на реальных данных, чтобы определить, какое решение оптимально для вашего приложения. Как общее практическое правило, используйте JSON и либо gzip, либо (предпочтительно) bzip2 в качестве основы для сравнения.

Отправка ответа

К этому моменту вы отправили код ответа и сериализацию данных и форматы сжатия. Еще один полезный заголовок, который вы можете отправить обратно, указывает на объем данных в теле ответа. Наконец, вы отправите необязательно сжатый поток сериализованных данных обратно.

 <?php
header("Content-Length: " . mb_strlen($data));
echo $data;

Распаковка ответа

Когда мы в последний раз выходили из DataModel.getData() Теперь, когда наш PHP REST сервис вернул ответ, нам нужно распаковать и десериализовать его, прежде чем мы сможем сделать что-то полезное с ним.

Точный код, необходимый для распаковки ответа, зависит от используемой схемы сжатия. gzip и deflate изначально поддерживаются в Android. Удивительно, но bzip2 является одновременно более эффективным из трех форматов декомпрессии и более сложным в использовании, поскольку изначально не поддерживается. Самый простой способ добавить поддержку — через библиотеку Apache Commons Compress. Вот как может выглядеть код для обработки каждой из этих схем сжатия:

 if (httpResponse.getStatusLine().getStatusCode() != 200) {
    // An error occurred, throw an exception and handle it on the calling end
}
String encoding = httpResponse.getFirstHeader("Content-Encoding").getValue();
java.io.InputStream inputStream = null;
if (encoding.equals("gzip")) {
    inputStream = AndroidHttpClient.getUngzippedContent(httpResponse.getEntity());
} else if (encoding.equals("deflate")) {
    inputStream = new java.util.zip.InflaterInputStream(httpResponse.getEntity().getContent(), new java.util.zip.Inflater(true));
} else if (encoding.equals("bzip2")) {
    inputStream = new org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream(httpResponse.getEntity().getContent());
}
String content;
try {
    content = new java.util.Scanner(inputStream).useDelimiter("\A").next();
} catch (java.util.NoSuchElementException e) {
    // An error occurred, throw an exception and handle it on the calling end
}

Если вы решите использовать bzip2, у вас есть два варианта использования библиотеки Apache Commons Compress. Первый — добавить JAR-файл библиотеки в ваш проект. Это менее утомительно и делает обновление столь же простым, как замена файла JAR и перестройка проекта, но в результате получается файл Android APK большего размера, поскольку он включает все содержимое JAR. Вот как использовать этот метод:

  1. Загрузите один из архивов Binaries для библиотеки и распакуйте его.
  2. В Eclipse щелкните меню «Проект» и выберите параметр «Свойства».
  3. В появившемся окне выберите «Путь сборки Java» из списка слева.
  4. На правой панели перейдите на вкладку Библиотеки.
  5. Нажмите кнопку Добавить внешние файлы JAR.
  6. Найдите и выберите файл commons-compress-#.jar# — текущая версия.

Второй вариант — получить отдельные исходные файлы, необходимые для сжатия bzip2, и скопировать их в свой проект вручную. Это более утомительный процесс, но размер вашего Android APK-файла будет значительно меньше. Вот как использовать этот метод:

  1. Загрузите исходный архив и распакуйте его.
  2. В Eclipse щелкните правой кнопкой мыши каталог src вашего проекта, выберите «Создать»> «Пакет» и введите имя пакета org.apache.commons.compress.compressors
  3. Повторите шаг 2, но на этот раз используйте имя пакета org.apache.commons.compress.compressors.bzip2
  4. Перейдите в каталог src/main/java
  5. Скопируйте эти файлы в ваш проект в рамках связанных пакетов:
  • org/apache/commons/compress/compressors/CompressorInputStream.java
  • org/apache/commons/compress/compressors/bzip2/BZip2Constants.java
  • org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStream.java
  • org/apache/commons/compress/compressors/bzip2/CRC.java
  • org/apache/commons/compress/compressors/bzip2/Rand.java

Десериализация ответа

После распаковки ответа вам необходимо десериализовать его в пригодный для использования объект данных. В случае JSON Android использует реализацию Java. Код для анализа массива данных JSON может выглядеть следующим образом:

 import org.json.*;

// ...
// String content = ...
JSONArray contacts = (JSONArray) new JSONTokener(content).nextValue();
JSONObject contact = contacts.getJSONObject(0);
String email = contact.getString("email");

Кэширование ответов

Основное внимание в этой статье уделяется минимизации объема данных, передаваемых во время запроса. В тех случаях, когда один и тот же ресурс может быть запрошен несколько раз, другой способ сделать связь более эффективным — это кэшировать ответы клиента (например, приложения Android) и затем указать веб-службе, к какой версии ресурса он обращался последним.

Есть два способа сделать это, оба описаны в подразделах RFC 2616 Раздел 14 . Первый метод основан на времени, когда сервер возвращает заголовок Last-ModifiedIf-Modified-Since Второй метод основан на хэше, где сервер отправляет значение хеша в своем ответе через заголовок ETagIf-None-Match В обоих случаях, если ресурс не был обновлен, он вернет статус ответа 304 (Не изменен).

Используя первый подход, вы просто добавили бы строку, подобную приведенной ниже, на стороне PHP, где $timestamp Во многих случаях метка времени UNIX может быть получена из форматированной строки даты с помощью функции strtotime()

 <?php
header("Last-Modified: " . date("D, d M Y H:i:s", $timestamp) . " GMT");

На стороне Android DataModel.getData()

 // Fetch the Last-Modified response header value from a previous request from persistent storage if 
// available
// String lastModified = ... 
// HttpGet httpRequest = ...
httpRequest.addHeader("If-Modified-Since", lastModified);
HttpResponse httpResponse = this.httpClient.execute(httpRequest);
switch (httpResponse.getStatusLine().getStatusCode()) {
    case 304:
        // Use the cached version of the response
        break;
    case 200:
        // Handle the response normally
        lastModified = httpResponse.getFirstHeader(
            "Last-Modified").getValue();
        // content = ...
        // Store both of the above variables in persistent storage
        break;
}

Обертывание-Up

Теперь вы знаете, как реализовать согласование содержимого для сериализации и сжатия, а также для кэширования ответов в PHP и Android. Я надеюсь, что эта статья оказалась полезной для вас, и что ваши мобильные приложения более эффективны для этого.

Изображение через Fotolia