Best Practices для взаимодействия сервера и приложения

This post is also available in: Английский

iOS Best Practices

Сейчас, в эпоху роста и развития мобильных приложений, всё более актуальным становится правильное построение клиент-серверного взаимодействия. При взаимодействии мобильного приложения с сервером следует обратить внимание на следующие моменты: гибкость, расширяемость, документирование, поддержка старых версий API, кеширование. В этой статье мы рассмотрим наиболее важные аспекты разработки и проектирование сервисов, которые помогут достичь лучших результатов.

Гибкость и расширяемость

Для обеспечения гибкости и расширяемости клиент-серверного взаимодействия мы используем при разработке RESTful API. Хороший RESTful API — тот, который можно менять легко и просто. Сервер может считаться RESTful, если он соответствует принципам REST. Когда вы разрабатываете API, который будет в основном использоваться мобильными устройствами, понимание и следование трем наиважнейшим принципам может быть весьма полезным. Причем не только при разработке API, но и при его поддержке и развитии в дальнейшем.

Независимость от состояния

RESTful сервер не должен отслеживать, хранить и тем более использовать в работе текущую контекстную информацию о клиенте. Клиент должен взять эту задачу на себя. Другими словами: не заставляйте сервер помнить состояние мобильного устройства, использующего API.

Давайте представим, что мы разрабатываем социальное приложение. Хороший пример, где разработчик мог совершить ошибку это предоставление вызова API, который позволяет мобильному устройству установить последний прочитанный элемент в ленте. Вызов API, обычно возвращающий ленту (назовем его /feed), теперь будет возвращать элементы, которые новее установленного. Звучит умно, не правда ли? Вы «оптимизировали» обмен данными между клиентом и сервером? А вот и нет.

Что может пойти не так в приведенном случае, так это то, что если ваш пользователь использует сервис с двух или трех устройств, то, когда одно из них устанавливает последний прочитанный элемент, то остальные не смогут загрузить элементы ленты, прочитанные на других устройствах ранее.

Независимость от состояния означает, что данные, возвращаемые определенным вызовом API, не должны зависеть от вызовов, сделанных ранее.

Правильный способ оптимизации данного вызова – использование заголовка HTTP If-Modified-Since. Но обсудим это позже.

Клиент же со своей стороны, может (должен) помнить параметры, сгенерированные на сервере при обращении к нему, и использовать их для последующих вызовов API, если потребуется.

Кэшируемая и многоуровневая архитектура

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

Клиент – серверное разделение и единый интерфейс

RESTful сервер должен прятать от клиента как можно больше деталей своей реализации. Клиенту не следует знать о том, какая СУБД используется на сервере или сколько серверов в данный момент обрабатывают запросы и прочие подобные вещи. Организация правильного разделения функций важна для масштабирования, если ваш проект начнёт быстро набирать популярность.

Это, пожалуй три самых важных принципа, которым нужно следовать в ходе разработки RESTful сервера.

Документирование

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

Первым шагом мы рекомендуем подумать об основных, высокоуровневых структурах данных (моделях), которыми оперирует ваше приложение. Затем подумайте над действиями, которые можно произвести над этими компонентами. Документация по foursquare API — хороший пример, который стоит изучить перед тем как начать писать свою. Данное API имеет набор высокоуровневых объектов, таких как места, пользователи и тому подобное. Также у них есть набор действий, которые можно произвести над этими объектами. Поскольку вы знаете высокоуровневые объекты и действия над ними в вашем продукте, создание структуры вызовов API становится проще и понятней. Например, для добавления нового места логично будет вызвать метод наподобие /venues/add

Документируйте все высокоуровневые объекты. Затем документируйте запросы и ответы на них, используя эти высокоуровневые объекты вместо простых типов данных. Вместо того, чтобы писать “Этот вызов возвращает три строковых поля, первое содержит id, второе имя, а третье описание” пишите “Этот вызов возвращает структуру (модель), описывающую место”.

Поддержка старых версий API

До появления мобильных приложений, в эпоху Web 2.0 создание разных версий API не было проблемой. И клиент (JavaScript/AJAX front-end) и сервер разворачивались одновременно. Потребители (ваши клиенты) всегда использовали самую последнюю версию клиентского ПО для доступа к системе. Поскольку вы — единственная компания, разрабатывающая как клиентскую так и серверную часть, вы полностью контролируете то как используется ваш API и изменения в нем всегда сразу же применялись в клиентской части. К сожалению это невозможно с клиентскими приложениями, написанными под разные платформы. Вы можете развернуть API версии 2, считая что все будет отлично, однако это приведет к неработоспособности приложений под iOS, использующих старую версию. Поскольку ещё могут быть пользователи, использующие такие приложения несмотря на то, что вы выложили обновленную версию в App Store. Вам всегда надо быть готовым к разделению вашего API на версии и к прекращению поддержки некоторых из них как только это потребуется. Однако поддерживайте каждую версию своего API не менее трех месяцев.

Парадигма разделения на версии через URL

Первое решение — это разделение с использованием URL.
api.example.com/v1/feeds будет использоваться версией 1 iOS приложения тогда как
api.example.com/v2/feeds будет использоваться версией 2.
Несмотря на то, что звучит это все неплохо, вы не сможете продолжать создание копий вашего серверного кода для каждого изменения в формате возвращаемых данных. Мы рекомендуем использование такого подхода только в случае глобальных изменений в API.

Парадигма разделения на версии через модель

Выше мы показали как документировать ваши структуры данных (модели). Рассматривайте эту документацию как контракт между разработчиками серверной и клиентской частей. Вам не следует вносить изменения в модели без изменения версии. Это значит, что в предыдущем случае должно быть две модели №1 и №2.
Поведение модели №1 остается таким же, как это было оговорено в документации.

Кэширование

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

Два главных принципа, которым мы вам рекомендуем следовать это:

  1. Не пытайтесь делать нестандартные схемы кэширования в клиентском приложении.
  2. Разберитесь с базовыми принципами кэширования, описанными в RFC спецификации HTTP 1.1. Там описаны две модели кеширования. Модель срока действия и модель действительности (валидности).

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

Модель срока действия

Давайте рассмотрим распространенную конфигурацию кэширования. Если вы используете nginx, у вас наверняка есть в конфиге нечто подобное:

location ~ \.(jpg|gif|png|ico|jpeg|css|swf)$ {
expires 7d;
}

nginx переводит эти настройки в соответствующий заголовок HTTP. В данном случае сервер отправляет поле “Expires” или “Cache-Control: max-age=n” в заголовке для всех изображений и рассчитывает на то, что клиент закэширует их на 7 дней. Это значит, что вам не нужно будет запрашивать эти же данные в течение 7-ми последующих дней. Каждый из распространенных браузеров (и промежуточных прокси) учитывает этот заголовок и работает как ожидается. К сожалению большинство Open Source фреймворков кэширования изображений для iOS, включая популярный SDWebImage, используют встроенный механизм кэширования, просто удаляющий изображения после n дней. Проблема заключается в том, что такие фреймворки не соответствуют модели валидности.

Модель валидности

И Facebook и Twitter решают проблему устаревших изображений в профиле (после того как было загружено новое изображение) используя модель валидности. В модели валидности сервер отправляет клиенту уникальный идентификатор ресурса и клиент кэширует и идентификатор и ответ. В терминах HTTP такой уникальный идентификатор называется ETag. Когда вы совершаете второй запрос к тому же ресурсу, вы должны отправить его ETag. Сервер использует этот идентификатор для проверки был ли изменен запрашиваемый вами ресурс с момента последнего обращения (помните, сервер — единственный достоверный источник). Если ресурс действительно менялся, сервер отправляет последнюю копию. В противном случае он шлет 304 Not Modified. Реализация модели валидности кэша требует дополнительных усилий как при разработке клиентской части, так и серверной.  Опишем обе эти модели далее.

Поддержка на стороне клиента

На самом деле под iOS, если вы используете MKNetworkKit, то он делает всю работу автоматически.

[[MKNetworkEngine sharedEngine] useCache];

Но для разработчиков под Android и Windows Phone мы распишем подробно, как это следует реализовывать.
Модель валидности кэша использует заголовки HTTP: ETag и Last-Modified. Реализация для клиентской части при этом проще, чем для серверной. Если вы получили ETag с ресурсом, когда вы делаете второй запрос на получение его же, отправьте ETag в поле “IF-NONE-MATCH” заголовка. Аналогично, если вы получили “Last-Modified” с ресурсом, отправьте его в поле “IF-MODIFIED-SINCE” заголовка в последующих запросах. Сервер же со своей стороны сам решит когда использовать “ETag”, а когда “Last-Modified”.

Реализация модели срока действия проста. Просто рассчитайте дату окончания срока действия на основе полей заголовка, “Expires” или “Cache-Control: max-age-n” и очистите кэш при наступлении этой даты.

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

Использование ETag

ETag обычно рассчитывается на сервере с использованием алгоритмов хэширования. (Большинство серверных языков высокого уровня, таких как Java/C#/Scala обладают средствами хэширования объектов). Перед формированием ответа сервер должен рассчитать хэш объекта и добавить его в поле заголовка ETag. Теперь, если клиент действительно отправил IF-NONE-MATCH в запросе и данный ETag равен тому, что вы рассчитали, отправьте 304 Not Modified. Иначе сформируйте ответ и отправьте его с новым ETag.

Использование Last-Modified

Реализация использования Last-Modified не совсем проста. Давайте представим что в нашем API есть вызов, возвращающий список друзей.

http://api.socialnetwork.com/friends/

Когда вы используете ETag, вы вычисляете хэш массива друзей. При использовании Last-Modified вы должны отправлять дату последнего изменения этого ресурса. Поскольку этот ресурс представляет собой список, эта дата должна являть собой дату когда вы последний раз добавили нового друга. Это требует от разработчика организации хранения даты последнего изменения данных для каждого пользователя в базе. Немного сложнее чем ETag, но дает большое преимущество в плане производительности.
Когда клиент запрашивает ресурс первый раз, вы отправляете полный список друзей. Последующие запросы от клиента теперь будут иметь поле “IF-MODIFIED-SINCE” в заголовке. Ваш серверный код должен отправлять только список друзей, добавленных после указанной даты. Код обращения к базе до модификации был примерно таким:

SELECT * FROM Friends;

после модификации стал таким:

SELECT * FROM Friends WHERE friendedDate > IF-MODIFIED-SINCE;

Если запрос не вернет записей, отправляем 304 Not Modified. Таким образом, если у пользователя 300 друзей и только двое из них были добавлены недавно, то ответ будет содержать только две записи. Время обработки запроса сервером и затрачиваемые при этом ресурсы снижаются значительно.
Конечно это сильно упрощенный код. Разработчику добавится головной боли когда вы решите сделать поддержку удаления или блокирования друзей. Сервер должен быть способным отправлять подсказки, используя которые у клиента будет возможность сказать какие друзья были добавлены, а какие удалены. Эта техника требует дополнительных усилий при разработке серверной части.

Выбор модели кэширования

Итак. Это была непростая тема. Теперь мы попробуем подвести итоги и вывести базовые правила использования той или иной модели кэширования.

  1. Все статические изображения должны обслуживаться по модели срока действия.
  2. Все данные, формируемые динамически, должны кэшироваться по модели валидности.
  3. Если ваш, динамически формируемый, ресурс является списком, вам следует использовать модель валидности, основанную на Last-Modified. (Например /friends). В остальных случаях следует использовать модель валидности, основанную на ETag. (Например /friends/firstname.lastname).
  4. Изображения или любые другие ресурсы, которые могут быть изменены пользователем (такие как аватар) должны также кэшироваться по модели валидности с использованием ETag. Несмотря на то, что это изображения, они не постоянны как например логотип компании. Кроме того вы просто не сможете точно рассчитать срок действия таких ресурсов.

Другой способ (более простой в реализации, но немного хакерский), это использование “ошибки URL”. Когда в ответе есть URL аватара, надо сделать часть его динамичной. Так вместо представления URL как

http://images.socialnetwork.com/person/firstname.lastname/avatar

сделать

http://images.socialnetwork.com/person/firstname.lastname/avatar/<хэш>

Хэш должен меняться в случае когда пользователь меняет аватар. Вызов, отправляющий список друзей, теперь отправит модифицированные URL-ы для пользователей, сменивших свои аватары. Таким образом изменения в изображениях профиля будут распространяться практически моментально!
Если ваши серверные и клиентские приложения будут соответствовать практически устоявшимся стандартам кэширования, ваше iOS приложение и ваш продукт вообще будут просто “летать”.

В этой статье мы дали простое объяснение таких стандартов, которых подавляющее большинство разработчиков не придерживаются.

Ссылки:

  1. http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
  2. http://habrahabr.ru/post/144011/#API_documentation
  3. http://habrahabr.ru/post/144259/
  4. http://shop.oreilly.com/product/0636920021575.do
  5. http://www.cocoanetics.com/2011/10/avoiding-image-decompression-sickness/
  6. http://mobile.tutsplus.com/tutorials/iphone/advanced-restkit-development_iphone-sdk/

2 thoughts on “Best Practices для взаимодействия сервера и приложения

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Пожалуйста напечатайте буквы/цифры изображенные на картинке

Please type the characters of this captcha image in the input box