 | Уровень сложности: средний Дэвид Мерц, писатель, Gnosis Software, Inc.
25.03.2009 Следование принципам REST (REpresentational State Transfer – передача репрезентативного состояния) при создании Web-приложений, в частности, приложений Ajax – это хороший способ избежать многих проблем. Однако у архитектуры REST есть один недостаток, заключающийся в передаче лишних данных через схожие запросы XMLHttpRequest. В данной статье рассказывается, как использовать серверные объекты cookies для хранения минимально необходимого количества информации, существенно сокращая объем трафика между клиентом и сервером. В то же время при отключении cookies приложение остается работоспособным.
Введение
Тот простой факт, что HTTP – это протокол без передачи информации о состоянии (stateless-протокол), можно рассматривать как одно из его ключевых преимуществ, так и как один из наиболее существенных недостатков. Все запросы к серверным ресурсам через HTTP должны обладать свойством идемпотентности - т.е. один и тот же запрос должен возвращать одни и те же данные при каждом выполнении. Идемпотентность также является одним из центральных принципов REST: один и тот же запрос, возможно, включающий в себя информацию о клиенте, обязан всегда возвращать один и тот же результат.
В отличие от философии REST, в приложениях Ajax часто бывает важна передача состояния. Например, определенные атрибуты или области интерфейса Web-приложения могут отображать текущее состояние серверных данных, которое может периодически опрашиваться при помощи JavaScript (существуют способы уведомления клиентов об изменении состояния, но в контексте нашей статьи они не имеют решающего значения). При этом Web-приложение в какой-то степени ожидает, что сервер всегда знает, что он должен делать в ответ на каждый запрос. Другими словами, он должен хранить информацию о том, какие данные были (или не были) отправлены клиенту, каким образом происходило взаимодействие и так далее.
Один из стандартных способов реализации принципов REST в приложениях Ajax заключается в использовании глобально уникальных идентификаторов (URI) для каждого запроса, запрашивающего наиболее свежие данные. Идентификатор UUID в запрос можно добавить, либо закодировав дополнительный параметр в URL, либо при помощи скрытой переменной формы. Например, можно направить следующий GET-запрос XMLHttpRequest к ресурсу:
http://myserver.example.com?uuid=4b879324-8ec0-4120-bba6-890eb0aa3fc0
|
Даже если следующий запрос будет выполнен всего секундой позже, в нем будет использован другой URI.
Хитрости идемпотентности
Термин "тот же самый результат" на самом деле не так очевиден, как может показаться. Запросы по одному и тому же URI будут возвращать полностью идентичные данные разве что в игрушечном мире. Даже статические Web-страницы могут изменяться со временем (например, если были исправлены опечатки в ранее опубликованной статье). Главным смыслом идемпотентности является то, что различие в результатах не должно быть прямым следствием обработки самих GET-запросов. Поэтому вполне допустимо обращаться к постоянно меняющемуся ресурсу, например, такому:
http://myserver.example.com/latest_data/
|
Проблема заключается в том, что определение того, какие данные являются «последними» (latest_data), зависит от разных факторов, а не только от того, когда и откуда эти данные были получены. Сервер может вполне соответствовать принципам REST и тем не менее поддерживать картину состояния окружающего мира.
Получение последних данных
У меня и моего коллеги, Мики Тебека (Miki Tebeca) был опыт разработки Web-приложения, которое постоянно опрашивало сервер на предмет обновления данных. Запросы выполнялись через объект XMLHttpRequest() в JavaScript. Пример сервера, написанного на Python, который будет продемонстрирован ниже, по сути является упрощенной и доработанной версией внутреннего модуля, созданного Мики.
В данной ситуации перед нами встают две проблемы. Во-первых, необходимо избежать передачи значительного объема данных в случае, если не произошло никаких изменений со времени предыдущего запроса. Во-вторых, необходимо минимизировать нагрузку на базу данных и расходование других вычислительных ресурсов, связанные с дублированием данных.
Средства для решения первой проблемы предоставляются непосредственно протоколом HTTP, хотя этим полезным решением неоправданно пренебрегают. При отсутствии изменений сервер может (и должен) возвращать в HTTP-ответе код состояния 304. Далее Ajax-приложение должно проверять данный код и в случае его наличия не обновлять клиентское состояние, так как сервер не переслал новых данных.
Проблема, связанная с ресурсами сервера, может быть решена путем кэширования ранее полученных данных и их обновления по мере работы приложения. Этот вариант решения, как правило, подходит только в том случае, если «последние данные» представляют собой относительно небольшой дискретный набор записей, а не единый массив взаимосвязанных данных. Далее мы будем отслеживать кэшированное состояние сессии приложения-клиента при помощи клиентского объекта cookie, как показано в листинге 1.
Листинг 1. Код сервера с использованием сессий (server.cgi)
from datetime import datetime
session = ClientSession()
old_stuff = session.get("data", []) # Обращение к кэшу
last_query = session.get("last", None)
prune_data(old_stuff, last_query) # Отбрасывание устаревших данных
new_stuff = get_new_stuff() # Получение свежих данных
if not new_stuff:
print "Status: 304" # Код состояния "ничего не изменилось"
else
print session.cookie # Печать нового или ранее созданного объекта cookie
print "Content-Type: text/plain"
print
all_stuff = old_stuff + new_stuff
session["data"] = all_stuff
session["last"] = datetime.now().isoformat()
print encode_data(all_stuff) # XML, JSON или что-то еще...
session.save()
|
Определенную часть работы по управлению сессией берет на себя класс ClientSession, но он довольно прост. Фактически все, что требуется – это проверять каждого клиента на наличие объекта cookie, соответствующего данным, хранящимся в переменной old_stuff (листинг 2).
Листинг 2. Управление сессией
from os import environ
from Cookie import SimpleCookie
from random import shuffle
from string import letters
from cPickle import load, dump
COOKIE_NAME = "my.server.process"
class ClientSession(dict):
def __init__(self):
self.cookie = SimpleCookie()
self.cookie.load(environ.get("HTTP_COOKIE",""))
if COOKIE_NAME not in cookie:
# Real UUID would be better
lets = list(letters)
shuffle(lets)
self.cookie[COOKIE_NAME] = "".join(lets[:15])
self.id = self.cookie[COOKIE_NAME].value
try:
session = load(open("session."+self.id, "rb"))
self.update(session)
except: # Если кэш пуст, то не обновляем сессию
pass
def save(self):
fh = open("session."+self.id, "wb")
dump(self.copy(), fh, protocol=-1) # Сохранение сессии
fh.close()
|
Выполнение запроса через Ajax
Теперь, когда мы реализовали кэширование данных на стороне сервера, построение клиентского запроса на JavaScript не представляет особой сложности. Запрос будет выглядеть примерно так, как показано в листинге 3.
Листинг 3. Запрос к серверу на получение последних данных
var r = new XMLHttpRequest();
r.onreadystatechange=function() {
if (r.readyState==4) {
if (r.status==200) { // Статус "OK"
displayData(r.responseText);
}
else if (r.status==304) {
// Код "Ничего не изменилось", поэтому ничего не выводим
}
else {
alertProblem(r);
}
}
}
r.open("GET",'http://myserver.example.com/latest_data/',true)
r.send(null);
|
В данном примере мы опустим реализацию методов displayData() и alertProblem(). Вероятно, первый метод будет отвечать за обработку ответа сервера в соответствии с выбранным форматом передачи данных, например, JSON, XML и т.д., а также другими требованиями к приложению.
Кроме того, данный пример иллюстрирует выполнение только единичного запроса. В приложении, выполняющемся длительное время, запросы могут выполняться многократно, причем с использованием таких функций обратного вызова, как setTimeout() или setInterval(). В некоторых приложениях запросы могут также посылаться на сервер вследствие возникновения определенного события или выполнения действия.
Резюме
Примеры серверного кода в данной статье были написаны на Python, однако те же принципы применимы практически к любому языку, на котором можно создавать CGI или другие серверные приложения. Основная идея достаточно проста: используйте клиентский cookie (если он доступен) для получения доступа к кэшированным данным, а также код состояния 304 в случае, если данные не менялись с момента последнего запроса. Какой бы язык программирования вы ни использовали, код приложения будет выглядеть практически так же.
Хотя мы не уделяли много внимания обработке ошибок, дизайн приложения обеспечивает корректное поведение в случае отсутствия объектов cookie. Если данный клиент не содержит cookie (например, он не позволяет сохранять cookie или просто выполняет первый запрос в данной сессии), то содержимое переменной old_stuff будет представлять собой пустой список. В результате все данные для этого клиента будут новыми (будут присвоены переменной new_stuff). Приложение можно слегка усовершенствовать, добавив поддержку специального типа клиентских сообщений, в которых бы указывалось текущее состояние сессии. Это полезно для отладки приложения, так и для сброса состояния на сервере в случае, если клиент определил, что произошло рассогласование клиентского состояния и содержимого кэша. При этом все негативные последствия, связанные со сбросом кэша, сводятся к небольшому увеличению нагрузки на сервер и сеть передачи данных и не приводят к нарушению свойства идемпотентности.
Ресурсы
Об авторе  | |  | Дэвид Мерц (David Mertz) - большой знаток в области открытых стандартов и только умеренно пугает многословием. С Дэвидом можно связаться по mertz@gnosis.cx его жизнь описывается более подробно на http://gnosis.cx/dW/. Предложения и комментарии по этой, предыдущей или будущей статьям приветствуются. Можете также посмотреть книгу Дэвида Text Processing in Python. |
Выскажите мнение об этой странице
|  |