Продолжаем работу над книгой про библиотеку Synapse. И сегодня я выкладываю текст следующей главы, касающейся работы с HTTP. Надо сказать, что про эту сторону библиотеки в блоге уже рассказывалось, если не 100, то 21 раз точно :). Однако текст ниже — это не дословный пересказ той 21 статьи, хотя повторения, конечно, есть. Более того, некоторые примеры я решил не включать в эту главу. Так, например, я не стал уделять внимание работе с конкретно заданными ресурсами в Сети, например, как в статье «Synapse. Отправка данных на сервер на примере imagevenue.com.«, т.к. такие примеры могут быть хороши в виде отдельной статьи, но в книгу их включать довольно рискованно в плане того, что сайты имеют свойство исчезать из Сети или преобразовываться до неузнаваемости, особенно мелкие. Также я не стал упоминать про работу с API различных сервисов типа Google Drive, Яндекс.Диск и пр. Хотя, если большому количеству народа такая часть потребуется, то думаю, что можно будет рассмотреть какой-нибудь не большой API с авторизацией в качестве примера. И, наконец, третье — текст выкладывается так как он был написан изначально, т.е. пока без проверки соавтором и, вполне возможно, что в окончательном варианте этот текст претерпит кое-какие изменения.
Общие сведения о протоколе
Протокол передачи гипертекста HTTP (Hyper Text Transfer Protocol) – это базирующийся на TCP/IP протокол передачи гипертекста, обеспечивающий доступ к документам на web-узлах.
Основная задача протокола состоит в установлении связи с web-сервером и обеспечении доставки HTML-страниц web-браузеру клиента.
Протокол HTTP:
- определяет взаимодействие партнеров на прикладном уровне;
- предназначен для передачи сообщений, являющихся блоками гипертекста;
- используется в службе глобального соединения.
Транспортным протоколом для HTTP является протокол TCP, причем сервер HTTP (сервер Web) находится в состоянии ожидания соединения со стороны клиента (стандартно по порту 80 TCP), а клиент HTTP (браузер Web) является инициатором соединения.
Взаимодействие между клиентом и сервером Web осуществляется путем обмена сообщениями. Сообщения HTTP делятся на запросы клиента серверу и ответы сервера клиенту.
Каждое HTTP-сообщение состоит из трёх частей, которые передаются в указанном порядке:
- Стартовая строка (англ. Starting line) — определяет тип сообщения;
- Заголовки (англ. Headers) — характеризуют тело сообщения, параметры передачи и прочие сведения;
- Тело сообщения (англ. Message Body) — непосредственно данные сообщения. Обязательно должно отделяться от заголовков пустой строкой.
Заголовки и тело сообщения могут отсутствовать, но стартовая строка является обязательным элементом, так как указывает на тип запроса/ответа. Исключением является версия 0.9 протокола (в настоящее время наиболее часто используется версия 1.1), у которой сообщение запроса содержит только стартовую строку, а сообщения ответа только тело сообщения.
В самом общем случае запросы и ответы выглядят следующим образом:
стартовая строка заголовок 1 заголовок 2 ... заголовок N CR LF (пустая строка) тело сообщения.
Стартовая строка
Стартовые строки различаются для запроса и ответа.
Строка запроса выглядит так:
Метод URI HTTP/Версия
Здесь:
Метод — название запроса, одно слово заглавными буквами. В версии HTTP 0.9 использовался только метод GET, методы для версии 1.1 могут быть как GET, так и POST, PUT и т.д. О методах в HTTP мы поговорим ниже.
URI определяет путь к запрашиваемому документу.
Версия — пара разделённых точкой арабских цифр. Например: 1.0.
Например, чтобы запросить главную страницу какого-либо сайта клиент может передать такую стартовую строку:
GET /index.html HTTP/1.1
Стартовая строка ответа сервера имеет следующий формат:
HTTP/Версия КодСостояния Пояснение
Здесь:
Версия — пара разделённых точкой арабских цифр как в запросе.
КодСостояния (англ. Status Code) — три арабские цифры. По коду статуса определяется дальнейшее содержимое сообщения и поведение клиента. Более подробно о кодах состояния будет рассказано ниже.
Пояснение (англ. Reason Phrase) — текстовое короткое пояснение к коду ответа для пользователя. Никак не влияет на сообщение и является необязательным.
Например, на предыдущий наш запрос клиентом данной страницы сервер ответил строкой:
HTTP/1.0 200 OK
Методы HTTP
Метод HTTP (англ. HTTP Method) — последовательность из любых символов, кроме управляющих и разделителей, указывающая на основную операцию над ресурсом. Обычно метод представляет собой короткое английское слово, записанное заглавными буквами. Название метода чувствительно к регистру.
Каждый сервер обязан поддерживать как минимум методы GET и HEAD. Если сервер не распознал указанный клиентом метод, то он должен вернуть статус 501 (Not Implemented). Если серверу метод известен, но он неприменим к конкретному ресурсу, то возвращается сообщение с кодом 405 (Method Not Allowed). В обоих случаях серверу следует включить в сообщение ответа заголовок Allow со списком поддерживаемых методов.
Кроме методов GET и HEAD, часто применяется метод POST.
Коды состояния
Код состояния является частью первой строки ответа сервера. Он представляет собой целое число из трех арабских цифр.
Первая цифра указывает на класс состояния. За кодом ответа обычно следует отделённая пробелом поясняющая фраза на английском языке, которая разъясняет человеку причину именно такого ответа.
Примеры:
201 Created
403 Access allowed only for registered users
507 Insufficient Storage
Клиент узнаёт по коду ответа о результатах его запроса и определяет, какие действия ему предпринимать дальше. Набор кодов состояния является стандартом, и они описаны в соответствующих документах RFC. Клиент может не знать все коды состояния, но он обязан отреагировать в соответствии с классом кода.
В настоящее время выделено пять классов кодов состояния.
1xx Informational (Информационный)
В этот класс выделены коды, информирующие о процессе передачи. В HTTP/1.0 сообщения с такими кодами должны игнорироваться. В HTTP/1.1 клиент должен быть готов принять этот класс сообщений как обычный ответ, но ничего отправлять серверу не нужно. Сами сообщения от сервера содержат только стартовую строку ответа и, если требуется, несколько специфичных для ответа полей заголовка. Прокси-серверы подобные сообщения должны отправлять дальше от сервера к клиенту.
2xx Success (Успех)
Сообщения данного класса информируют о случаях успешного принятия и обработки запроса клиента. В зависимости от статуса сервер может ещё передать заголовки и тело сообщения.
3xx Redirection (Перенаправление)
Коды класса 3xx сообщают клиенту что для успешного выполнения операции необходимо сделать другой запрос (как правило по другому URI). Из данного класса пять кодов 301, 302, 303, 305 и 307 относятся непосредственно к перенаправлениям. Адрес, по которому клиенту следует произвести запрос, сервер указывает в заголовке Location. При этом допускается использование фрагментов в целевом URI.
4xx Client Error (Ошибка клиента)
Класс кодов 4xx предназначен для указания ошибок со стороны клиента. При использовании всех методов, кроме HEAD, сервер должен вернуть в теле сообщения гипертекстовое пояснение для пользователя.
5xx Server Error (Ошибка сервера)
Коды 5xx выделены под случаи неудачного выполнения операции по вине сервера. Для всех ситуаций, кроме использования метода HEAD, сервер должен включать в тело сообщения объяснение, которое клиент отобразит пользователю.
Заголовки HTTP (англ. HTTP Headers) — это строки в HTTP-сообщении, содержащие разделённую двоеточием пару параметр-значение. Формат заголовков соответствует общему формату заголовков текстовых сетевых сообщений ARPA (RFC 822). Заголовки должны отделяться от тела сообщения хотя бы одной пустой строкой.
Примеры заголовков:
Server: Apache/2.2.11 (Win32) PHP/5.3.0
Last-Modified: Sat, 16 Jan 2010 21:16:42 GMT
Content-Type: text/plain; charset=windows-1251
Content-Language: ru
В примере выше каждая строка представляет собой один заголовок. При этом то, что находится до первого двоеточия, называется именем (англ. name), а что после неё — значением (англ. value).
Все заголовки разделяются на четыре основных группы:
- General Headers (рус. Основные заголовки) — должны включаться в любое сообщение клиента и сервера.
- Request Headers (рус. Заголовки запроса) — используются только в запросах клиента.
- Response Headers (рус. Заголовки ответа) — только для ответов от сервера.
- Entity Headers (рус. Заголовки сущности) — сопровождают каждую сущность сообщения.
Именно в таком порядке рекомендуется посылать заголовки получателю.
Если вам не будет хватать существующих заголовков, то можете смело вводить свои. Традиционно к именам таких дополнительных заголовков добавляют префикс «X-» для избежания конфликта имён с возможно существующими. Например, как в заголовках X-Powered-By или X-Cache. Некоторые разработчики используют свои индивидуальные префиксы. Примерами таких заголовков могут служить Ms-Echo-Request и Ms-Echo-Reply, введённые корпорацией Microsoft для расширения WebDAV.
Тело HTTP сообщения (message-body), если оно присутствует, используется для передачи тела объекта, связанного с запросом или ответом. Тело сообщения (message-body) отличается от тела объекта (entity-body) только в том случае, когда применяется кодирование передачи, что указывается полем заголовка Transfer-Encoding.
Поле Transfer-Encoding должно использоваться для указания любого кодирования передачи, примененного приложением в целях гарантирования безопасной и правильной передачи сообщения. Поле Transfer-Encoding — это свойство сообщения, а не объекта, и, таким образом, может быть добавлено или удалено любым приложением в цепочке запросов/ответов.
Правила, устанавливающие допустимость тела сообщения в сообщении, отличны для запросов и ответов.
Присутствие тела сообщения в запросе отмечается добавлением к заголовкам запроса поля заголовка Content-Length или Transfer-Encoding. Тело сообщения может быть добавлено в запрос только когда метод запроса допускает тело объекта, например, при использовании методов POST и PUT .
Включается или не включается тело сообщения в сообщение ответа зависит как от метода запроса, так и от кода состояния ответа. Все ответы на запрос с методом HEAD не должны включать тело сообщения, даже если присутствуют поля заголовка объекта, заставляющие поверить в присутствие объекта. Никакие ответы с кодами состояния 1xx (Информационные), 204 (Нет содержимого, No Content), и 304 (Не модифицирован, Not Modified) не должны содержать тела сообщения. Все другие ответы содержат тело сообщения, даже если оно имеет нулевую длину.
Анализ сообщений клиента и сервера
Довольно часто при работе с различными ресурсами в Сети нам необходимо знать то, каким образом «общаются» между собой обычный web-браузер (например, Internet Explorer) и сервер, для того, чтобы на основании этих данных организовать работу своего собственного клиента для сайта/блога/форума.
Если Вы внимательно прочитали всё, что сказано выше про HTTP, то наверняка уже усвоили то, что даже не имея в распоряжении всего сообщения, оперируя только его стартовой строкой и заголовками, можно определить, какой запрос был отправлен на сервер, что ответил сервер на этот запрос и, при необходимости, определить причину того или иного ответа.
Для примера рассмотрим такой диалог клиента и сервера:
Запрос клиента:
GET /wiki/страница HTTP/1.1 Host: ru.wikipedia.org User-Agent: Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9b5) Gecko/2008050509 Firefox/3.0b5 Accept: text/html Connection: close (пустая строка)
Ответ сервера:
HTTP/1.1 200 OK Date: Wed, 11 Feb 2009 11:20:59 GMT Server: Apache X-Powered-By: PHP/5.2.4-2ubuntu5wm1 Last-Modified: Wed, 11 Feb 2009 11:20:59 GMT Content-Language: ru Content-Type: text/html; charset=utf-8 Content-Length: 1234 Connection: close
Проанализируем этот диалог по порядку, начиная с самой первой строки запроса клиента.
GET /wiki/страница HTTP/1.1
Host: ru.wikipedia.org
Здесь клиент запрашивает документ /wiki/страница, используя версию протокола HTTP 1.1 с ресурса ru.wikipedia.org.
User-Agent: Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9b5) Gecko/2008050509 Firefox/3.0b5
В этом заголовке клиент указывает свое программное обеспечение (имя и версию браузера и другую информацию).
Accept: text/html
Connection: close
Клиент указывает, что он «понимает» как простой plain-text так и HTML-код, а также указывает серверу на то, что после окончания передачи сеанс нужно закрыть.
Теперь разберем ответ сервера:
HTTP/1.1 200 OK
Date: Wed, 11 Feb 2009 11:20:59 GMT
Здесь сервер сообщает клиенту, что его запрос был успешно обработан в среду 11 февраля 2009 года в 11:20:59 по GMT.
Server: Apache
Указывает свое название
X-Powered-By: PHP/5.2.4-2ubuntu5wm1
В дополнительном заголовке сервер «рассказывает» клиенту о своем программном обеспечении
Last-Modified: Wed, 11 Feb 2009 11:20:59 GMT
Здесь сервер указывает время последнего изменения передаваемого нам документа.
Content-Language: ru
Язык документа — русский
Content-Type: text/html; charset=utf-8
Тип содержимого – простой текст или html-код в кодировке UTF-8
Content-Length: 1234
Объем документа (именно документа, а не всего сообщения) составил 1234 байта
Connection: close
Соединение будет закрыто.
Как видите, при анализе заголовков клиента и сервера мы смогли получить массу полезной (и не очень) для нас информации, включая даты последнего изменения документа, кодировку содержимого в ответе, размер содержимого, название и программное обеспечение сервера и т.д. От того на сколько правильно мы интерпретируем «общение» клиента и сервера зависит очень много – вплоть до работоспособности нашего программного обеспечения вообще.
Теперь, вооружившись этой минимально необходимой для дальнейшей работы информацией, мы можем приступать к рассмотрению работы библиотеки Synapse с протоколом HTTP.
Модуль HTTPSend.pas
Для работы с HTTP в Synapse предназначен модуль HTTPSend.pas. Этот модуль содержит класс THTTPSend, реализующего клиент HTTP и ряд вспомогательных функций для выполнения наиболее распространенных операций при работе с HTTP (функции для скачивания странички, файла и т.д.). Исследование модуля мы начнем с класса THTTPSend.
Класс THTTPSend
THTTPSend, как уже было сказано выше, представляет собой реализацию клиента для работы с HTTP. Все операции, которые Вы планируете выполнить с HTTP в Synapse так или иначе буду связаны с использованием именно этого класса. Рассмотрим, какие свойства и методы содержит этот класс.
Свойства класса:
property Headers: TStringList |
Заголовки. Перед выполнением HTTP запроса в этот список можно занести любые заголовки, за исключением следующих: ‘Expect: 100-continue’, ‘Content-Length’, ‘Content-Type’, ‘Connection’, ‘Authorization’, ‘Proxy-Authorization’ и ‘Host’ (эти заголовки заполняются самим классом непосредственно перед выполнением запроса). После выполнения запроса этот список будет содержать заголовки ответа сервера. |
property Cookies: TStringList |
Список, содержащий все куки запроса. Куки в списке представлены парами name-value. |
property Document: TMemoryStream |
Поток, содержащий тело запроса (перед его отправкой на сервер) или тело ответа (после выполнения запроса) |
property RangeStart: integer |
Определяет позицию в документе, начиная с которой, необходимо его получать с сервера. Если значение равно 0 (по умолчанию), то документ запрашивается с самого начала. |
property RangeEnd: integer |
Определяет позицию в документе до которой его необходимо скачивать. Если значение равно 0 (по умолчанию) то запрашивается весь документ. |
property MimeType: string |
Mime-тип данных, которые будут отправлены на сервер. По умолчанию ‘text/html’ |
property Protocol: string |
Версия HTTP. Допустимые значения: ‘0.9’,’1.0’, ‘1.1’. Значение по умолчанию ‘1.0’ |
property KeepAlive: Boolean |
Значение по умолчанию — True. Указывает, следует ли поддерживать постоянное соединение с сервером при обмене данными |
property KeepAliveTimeout: integer |
Значение интервала в секундах в течение которого необходимо поддерживать постоянное соединение |
property Status100: Boolean |
True означает, что перед отправкой данных запроса следует ожидать ответа сервера с кодом 100 Continue. |
property ProxyHost: string |
Доменное имя или IP-адрес прокси-сервера |
property ProxyPort: string |
Порт прокси-сервера |
property ProxyUser: string |
Если прокси-сервер требует авторизации пользователя, то это поле содержит логин |
property ProxyPass: string |
Если прокси-сервер требует авторизации пользователя, то это поле содержит пароль доступа к серверу |
property UserAgent: string |
Значение заголовка User-Agent. По умолчанию содержит ‘Mozilla/4.0 (compatible; Synapse)’ |
property ResultCode: Integer |
Код состояния после выполнения запроса |
property ResultString: string |
Описание кода состояния |
property DownloadSize: integer |
Содержит размер загруженных данных после выполнения запроса |
property UploadSize: integer |
Содержит размер отправленных на сервер данных после выполнения запроса. |
property Sock: TTCPBlockSocket |
TCP сокет, используемый для выполнения операций по протоколу HTTP |
property AddPortNumberToHost: Boolean |
True означает, что в заголовок ‘Host:’ будет также включаться и номер порта. Некоторые серверы могут не принимать заголовок, содержащий номер порта. Значение по умолчанию True. |
Методы класса:
procedure Clear; |
Очищает списки заголовков (Headers), куки (Cookies) и тело запроса (Document) |
procedure DecodeStatus(const Value: string); |
Разбирает стартовую строку ответа сервера и заполняет свойства ResultCode и ResultString |
function HTTPMethod(const Method, URL: string): Boolean; |
Отправляет запрос методом Method на адрес, определенный в параметре URL. Это единственный метод класса для отправки любых запросов на сервер. Если запрос выполнен успешно, то функция вернет True. |
procedure Abort; |
Прерывает работу TCP сокета. |
Вот и всё, что содержит класс THTTPSend. Немного, неправда ли? Однако, как мы увидим далее, этих свойств и методов вполне достаточно, чтобы реализовать практически любой обмен информацией по HTTP, включая и использование различных прокси и работа со сжатым содержимым (GZip) и т.д.
Кроме класса THTTPSend модуль HTTPSend.pas содержит также и ряд вспомогательных методов, которые облегчают работы с наиболее частыми операциями при работе с HTTP. Рассмотрим их.
Вспомогательные методы
function HttpGetText(const URL: string; const Response: TStrings): Boolean;
Эта функция получает текст страницы, расположенной по адресу URL и заносит его в список Response. Используется метод GET.
В случае, если запрос выполнен успешно (т.е. клиент смог получить ответ от сервера) результат выполнения метода будет равен True.
Следует отметить, что метод HttpGetText возвращает только тело документа, т.е. в Response будут отсутствовать заголовки ответа, куки и т.д.
function HttpGetBinary(const URL: string; const Response: TStream): Boolean;
Метод, аналогичный методу HttpGetText. Получает бинарные данные, расположенные по заданному URL и записывает эти данные в поток Response. В случае успеха метод возвращает True.
function HttpPostBinary(const URL: string; const Data: TStream): Boolean;
Метод отправляет документ, содержащийся в потоке Data методом POST на заданный в URL адрес. В случае успешного выполнения запроса, ответный документ также будет записан в поток Data.
function HttpPostURL(const URL, URLData: string; const Data: TStream): Boolean;
Метод удобно использовать для отправки данных web-форм на заданный URL. Для отправки данных используется метод POST. При этом поля формы предварительно должны быть записаны в URLData и закодированы методом EncodeURLElement. Данные в URLData должны представляться в той же последовательности, что и в web-форме и могут выглядеть следующим образом:
fileld1=hello&fileld2=world
В случае успешного выполнения запроса метод вернет True, а ответный документ будет записан в поток Data.
function HttpPostFile(const URL, FieldName, FileName: string; const Data: TStream; const ResultData: TStrings): Boolean;
Метод может быть использован для отправки любого файла методом POST на заданный URL. При этом функция имитирует отправку файла в формате multipart/form-data. Отправляемый файл должен содержаться в потоке Data, а имя файла – в параметре FileName. В свою очередь, параметр FileldName должен содержать имя поля формы в который должно быть записано имя файла. Если запрос выполнен успешно, то в ResultData будет записан ответный документ, а метод вернет True.
Примеры работы с HTTP в Synapse
В качестве примеров работы с HTTP в Synapse рассмотрим несколько небольших приложений, демонстрирующих различные возможности по использованию Synapse, которые впоследствии могут Вам пригодиться при разработке своих проектов.
Первый пример: скачиваем большие файлы с заполнением ProgressBar’а
Достаточно актуальный и важный вопрос при разработке приложений для работы с HTTP – как показать прогресс загрузки большого объема данных из Сети? Посмотрим как решается эта задача с использованием Synapse. Начнем с теории.
Итак, при отправке данных клиенту сервер может включить в список заголовков заголовок Content-Length, который будет содержать размер сущности (тела ответа) в байтах. Получив такой заголовок клиент может, например, организовать загрузку файла частями или, как в нашем случае, инициировать процесс загрузки с заполнением ProgressBar’а. Однако следует помнить о том, что сервер не обязан отправлять этот заголовок клиенту и клиент должен быть готов к такой ситуации, когда размер загружаемых данных заранее не известен.
Таким образом, алгоритм работы нашего приложение может быть следующим:
- Отправляем запрос методом HEAD на сервер и получаем список заголовков
- Ищем в заголовках Content-Length
- Если Content-Length найден, то получаем значение заголовка и присваиваем это значение свойству Max у ProgressBar’а
- Если заголовок Content-Length не найден, то скачиваем файл без заполнения ProgressBar’а
Теперь попробуем реализовать этот алгоритм в приложении. Создаем новый проект Delphi (назовем его syna_progress) и на главной форме приложения размещаем компоненты как показано на рисунке ниже:
На форме приложения расположены следующие компоненты:
- edURL: TEdit – поле для ввода URL с которого будет загружен файл
- btnGET: TButton – кнопка, клик по которой будет запускать процесс загрузки файла
- pbDownload: TProgressBar – индикатор процесса загрузки
- lbProgress: TLabel – метка для вывода информации о размере полученных от сервера данных.
Теперь приступим к написанию кода программы. Начнем с функции, которая будет возвращать нам размер данных для загрузки. Назовем её GetSize. Подключаем в uses модуль httpsend и пишем следующий код:
function TfMain.GetSize(const AURL : string): int64; var HTTPClient: THTTPSend; I: Integer; s: string; begin Result:=-1; HTTPClient:=THTTPSend.Create; try if HTTPClient.HTTPMethod('HEAD',AURL) then begin for I := 0 to HTTPClient.Headers.Count-1 do begin if pos('content-length',lowercase(HTTPClient.Headers[i]))>0 then begin s:= copy(HTTPClient.Headers[i], 16, Length(HTTPClient.Headers[i] )-15); Result:=StrToInt(s)+Length(HTTPClient.Headers.Text); break; end; end; end else raise Exception.Create('Ошибка: не удалось получить заголовки'); finally HTTPClient.Free end; end;
Здесь мы создаем объект типа THTTPSend и пробуем выполнить метод HEAD. Если сервер возвращает ответ, то пробуем найти в ответном сообщении заголовок Content-Length и получить его значение. При этом, обратите внимание на строку в которой мы получаем результат функции:
Result:=StrToInt(s)+Length(HTTPClient.Headers.Text);
Здесь мы к значению заголовка Content-Length также прибавляем и длину текста в списке Headers. Причина такого действия заключается в назначении самого заголовка Content-Length – он возвращает только размер тела документа и не учитывает длину заголовков, которые также будут получены при загрузке документа.
Теперь разберемся с тем, как будет происходить работа с ProgressBar. У THTTPSend, как мы уже знаем, есть свойство DownloadSize, которое содержит размер загруженных с сервера данных, однако это свойство будет иметь значение 0 до тех пор, пока все данные не будут получены. То есть, в нашем случае, это свойство оказывается практически бесполезным. Вместе с этим, у THTTPSend имеется другое свойство — Sock, используя которое мы можем легко получить доступ к сокету, который используется для работы с Сетью. И именно это свойство поможет нам в решении задачи.
В нашем случае, для реализации заполнения ProgressBar’а, нам потребуется определить обработчик события OnStatus сокета и в этом обработчике отслеживать все операции чтения данных из сокета. Реализуем всё сказанное в нашей программе.
Добавим в нашу программу две переменные:
type TfMain = class(TForm) [...] private downloaded: int64; size: int64; public { Public declarations } end;
Переменная size будет содержать размер документа, который мы будем скачивать с сервера, а downloaded – размер данных, полученных на текущий момент выполнения программы. Теперь напишем обработчик события OnStatus для сокета у THTTPSend. Для работы с сокетом нам потребуется модуль blcksock – подключаем его в uses модуля приложения и пишем такой обработчик:
procedure TfMain.OnSockStatus(Sender: TObject; Reason: THookSocketReason; const Value: String); const cProgress = '%d of %d bytes'; begin if Reason=HR_ReadCount then begin downloaded:=downloaded+StrToInt(Value); if size>0 then begin pbDownload.Position:=downloaded; lbProgress.Caption:=Format(cProgress,[downloaded,pbDownload.Max]); end else lbProgress.Caption:=IntToStr(downloaded)+' bytes'; Application.ProcessMessages; end; end;
Здесь мы отслеживаем каждый момент чтения данных из сокета (HR_ReadCount) и выполняем следующие операции:
- если переменная size содержит не нулевое значение (т.е. GetSize вернула нам размер документа), то заполняется ProgressBar и прогресс загрузки выводится в метку lbProgress
- если переменная size меньше или равна нулю, то выводим только информацию о том, сколько байт данных было загружено.
Теперь нам остается только написать код обработчика события OnClick кнопки «Download» и проверить работу программы. Пишем такой обработчик OnClick:
procedure TfMain.btnGETClick(Sender: TObject); var HTTPClient: THTTPSend; begin downloaded:=0; size:=GetSize(edURL.Text);//получаем размер файла для загрузки {определяем стиль у ProgressBar} if size>0 then begin pbDownload.Style:=pbstNormal; pbDownload.Max:=size;//указываем максимальное значение end else pbDownload.Style:=pbstMarquee; HTTPClient:=THTTPSend.Create; try //определяем обработчик события OnStatus HTTPClient.Sock.OnStatus:=OnSockStatus; {Пробуем скачать файл} if HTTPClient.HTTPMethod('GET',edURL.Text) then HTTPClient.Document.SaveToFile('file.pdf') else raise Exception.Create('Не удалось загрузить данные с сервера'); finally HTTPClient.Free; end; end;
Вот и все. Можете запустить программу и попробовать скачать какой-нибудь файл из Сети. Если Ваш сервер вернет заголовок Content-Length, то Вы увидите, что ProgressBar плавно заполняется при загрузке. Если же сервер не вернет необходимый нам заголовок, то весь процесс загрузки файла будет отображаться только в метке lbProgress.
Второй пример: пишем свой аналог свойства HandleRedirects для THTTPSend
Задача
Представленный выше пример работает, однако у программы есть один недостаток – вы не сможете скачать файл доступ к которому осуществляется через перенаправление. Например, в блоге webdelphi.ru есть ссылка на загрузку справочника по компонентам Ribbon Controls, которая выглядит следующим образом:
http://webdelphi.ru/wp-content/plugins/download-monitor/download.php?id=59
С использованием этого URL процесс загрузки с сайта выглядит следующим образом:
- Клиент отправляет запрос методом GET на заданный URL
- Сервер возвращает код статуса 302 и в заголовке Location указывает адрес по которому находится файл
- Клиент получает адрес из заголовка Location и начинает процесс загрузки.
Для владельца сайта такой процесс скачивания файлов может быть полезен, в случаях, когда перед отдачей клиенту запрашиваемого документа, необходимо выполнить какие-либо действия (например, нарастить значение счётчика количества скачиваний файла). Для нас же такой ход загрузки файла создает небольшую проблему так как прежде, чем мы начнем скачивать документ и заполнять ProgressBar, нам надо быть уверенными, что это необходимый нам документ, а не информация о расположении файла.
У класса THTTPSend нет такого замечательного свойства как HandleRedirects в Indy, которое позволяет забыть об отслеживании перенаправлений. Но ведь никто не запрещает нам самим немного расширить возможности класса и написать свой собственный механизм отслеживания всех перенаправлений и получения целевого URL.
Итак, в этом примере нам необходимо решить следующую задачу: разработать механизм обработки ответов сервера с кодом статуса 3xx и дописать пример работы с ProgressBar таким образом, чтобы файлы гарантированно скачивались с сервера (конечно, при условии, что файлы действительно присутствуют на сервере и доступ к ним не закрыт).
Решение
Для решения задачи воспользуемся одной из возможностей Delphi 2005 и выше – напишем свой помощник класса (class helper) THTTPSend. Для этого скопируем предыдущий пример работы с Synapse в новую директорию и переименуем проект из syna_progress в syna_HandleRedirects. Теперь определимся с тем, чего нам не хватает в классе для того, чтобы осуществить автоматическое перенаправление на URL, заданный в заголовке Location?
Во-первых, нам потребуется метод, возвращающий значение заголовка по его имени. С помощью этого методы мы будем узнавать значение в заголовке Location.
Во-вторых, необходим метод, который будет анализировать ответ сервера и перенаправлять нас на новый URL до тех пор, пока мы не получим целевой URL или количество попыток автоматического перенаправления не достигнет заранее определенного значения.
Теперь реализуем всё сказанное в коде нашей программы. Для этого добавим в проект новый модуль (назовем его HTTPSendHelper) и создадим в этом модуле такой помощник класса THTTPSend:
type THTTPSend_ = class helper for THTTPSend function HeaderByName(const AHeaderName: string): string; function GetTargetURL(const AStartURL: string; ARedirectMaximum: integer = 10): string; end;
Метод HeaderByName возвращает значение заголовка по его имени и выглядит следующим образом:
function THTTPSend_.HeaderByName(const AHeaderName: string): string; var I: integer; begin Result := EmptyStr; for I := 0 to Headers.Count - 1 do begin if StartsText(AHeaderName, Headers[I]) then begin Result := copy(Headers[I], Length(AHeaderName) + 3, Length(Headers[I]) - 1); break; end; end; end;
Метод GetTargetURL возвращает URL по которому находится запрашиваемый документ. При этом проверка перенаправлений начинается с адреса, заданного в параметре AStartURL и заканчивается в том случае, если сервер вернет код статуса отличный от 301/302 или, если количество перенаправлений превысит значение ARedirectMaximum. Код метода следующий:
function THTTPSend_.GetTargetURL(const AStartURL: string; ARedirectMaximum: integer = 10): string; var ARedirectCount: integer; ALocationURL: string; begin Result := AStartURL; repeat if HTTPMethod('HEAD', Result) then begin ALocationURL := HeaderByName('Location'); if Length(ALocationURL) > 0 then begin Result := ALocationURL; inc(ARedirectCount); end; end else exit; until ((ResultCode <> 301) and (ResultCode <> 302)) or (ARedirectCount >= ARedirectMaximum); end;
Теперь, имея в распоряжении оба этих метода мы можем написать новый метод получения данных от сервера, например, такой:
function THTTPSend_.HTTPMethodExt(const AMehtod, AURL: string; const AHandleRedirects: boolean; ARedirectMaximum: integer): boolean; var aTargetURL: string; begin if not AHandleRedirects then Result := HTTPMethod(AMehtod, AURL) else begin aTargetURL:=GetTargetURL(AURL,ARedirectMaximum); Result := HTTPMethod(AMehtod, aTargetURL); end; end;
Здесь кроме названия метода и URL мы также можем указать будет ли использоваться автоматическое перенаправление в случае получения кода статуса из группы 3хх (AHandleRedirects), а также определить количество попыток автоматического перенаправления (ARedirectMaximum).
Теперь воспользуемся помощников класса и допишем приложение из предыдущего примера, чтобы скачивать файлы даже при наличии перенаправлений. Подключим в uses главного модуля программы модуль HTTPSendHelper и перепишем обработчик OnClick кнопки «Download» следующим образом:
procedure TfMain.btnGETClick(Sender: TObject); var HTTPClient: THTTPSend; aTarget: string; begin downloaded := 0; HTTPClient := THTTPSend.Create; try //получаем целевой URL aTarget := HTTPClient.GetTargetURL(edURL.Text, 15); //запрашиваем размер документа size := GetSize(aTarget); //задаем стиль ProgressBar if size > 0 then begin pbDownload.Style := pbstNormal; pbDownload.Max := size; end else pbDownload.Style := pbstMarquee; //определяем обработчик события OnStatus сокета HTTPClient.Sock.OnStatus := OnSockStatus; //пробуем загрузить документ if HTTPClient.HTTPMethod('GET', aTarget) then HTTPClient.Document.SaveToFile('file.pdf') else raise Exception.Create('Не удалось загрузить данные с сервера'); finally HTTPClient.Free; end; end;
Теперь можете запустить приложение и задать, например, такой URL для загрузки файла:
http://webdelphi.ru/wp-content/plugins/download-monitor/download.php?id=59
и убедитесь, что несмотря на то, что результатом выполнения запроса GET на этот URL будет код статуса 302 без каких-либо ошибок с заполнением ProgressBar’а.
Как было сказано выше, предложенное решение работает в Delphi версии 2005 и выше. Поэтому, если Вы используете более раннюю версию Delphi, то вашим решением задачи может стать написание наследника THTTPSend в котором будут реализованы те же самые методы, что и в рассмотренном помощнике класса. Мы класс-наследник писать не будем, а перейдем к следующему примеру работы с HTTP в Synapse.
Третий пример: фильтруем считываемые данные.
Часто, при скачивании какой-либо странички с сайта, нам требуется получить не весь код страницы, а, например, только часть страницы, в которой расположены мета-теги. В этом случае, если количество запрашиваемых страниц сайта велико, то при отсутствии фильтрации полученных данных, возможны две взаимосвязанные проблемы:
- программа будет потреблять намного больше http-трафика, чем требуется для решения задачи
- запрос к серверу будет выполняться дольше, т.к. будет скачиваться вся страница, а не только необходимая нам часть.
Между тем Synapse позволяет нам фильтровать данные, считываемые из сокета. Для этого используется событие OnReadFilter. Рассмотрим работу с этим событием и напишем программу, которая будет получать с сервера только ту часть web-страницы, в которой содержатся необходимые нам данные. Создадим новый проект Delphi (назовем его «DataFilter») и на главной форме приложения разместим компоненты, как показано на рисунке ниже:
На форме расположены следующие компоненты:
- edURL: TEdit – поле ввода URL
- edStart: TEdit – поле для ввода строки с которой должна начинаться целевая часть страницы
- edEnd: TEdit – поле для ввода строки, которой должна заканчиваться целевая часть страницы
- btnGet: TButton – кнопка, клик по которой будет запускаться процесс загрузки данных с сервера
- memResult: TMemo – текстовый редактор для вывода результата работы программы.
Для решения задачи нам потребуется совсем не много действий, а именно написать обработчик события OnReadFilter сокета у THTTPSend и использовать его в программе. Так как мы в очередной раз будем использовать сокет, то подключаем в uses модули httpsend и blcksock.
Теперь зададим одну переменную в разделе private главной формы приложения и определим обработчик OnReadFilter:
type TForm1 = class(TForm) [..] private AllDocument: string; procedure Filter(Sender: TObject; var Value: AnsiString); public { Public declarations } end;
Здесь следует пояснить, зачем нам потребовалась переменная AllDocument. Причина в том, что в параметре Value обработчика события OnReadFilter возвращается только часть данных, которая была получена при последнем чтении данных из сокета и нет гарантии того, что эта часть данных будет содержать полностью искомую строку.
Сам обработчик события будет выглядеть следующим образом:
procedure TForm1.Filter(Sender: TObject; var Value: AnsiString); var IdxStart,IdxEnd: integer; begin //сохраняем полученые данные AllDocument:=AllDocument+Value; //ищем строку в полученных данных IdxStart:=pos(edStart.Text,AllDocument); IdxEnd:=pos(edEnd.Text,AllDocument); if (IdxStart>0) and (IdxEnd>0) then begin //строка найдена - выводим её в TMemo memResult.Lines.Add(copy(AllDocument,IdxStart, IdxEnd+Length(edEnd.Text)-IdxStart)); //прекращаем работу сокета TBlockSocket(Sender).AbortSocket; end; end;
Теперь напишем обработчик события OnClick кнопки:
procedure TForm1.btnGetClick(Sender: TObject); var HTTPClient: THTTPSend; begin //удаляем полученные ранее данные AllDocument:=EmptyStr; memResult.Lines.Clear; HTTPClient:=THTTPSend.Create; try //определяем обработчик события HTTPClient.Sock.OnReadFilter:=Filter; //документ был скачан полностью if HTTPClient.HTTPMethod('GET',edURL.Text)then memResult.Lines.Add(AllDocument) finally HTTPClient.Free; end; end;
В представленном выше методе следует обратить внимание на то, что вывод данных в TMemo осуществляется только в том случае, если HTTPMethod возвращает True, то есть работа сокета не была прервана в обработчике Filter и документ был скачан полностью.
Теперь можете проверить работу программы. Запустите приложение и задайте, например, исходные данные как показано на рисунке ниже:
Результатом работы будет следующая строка:
И пусть вас не смущают непонятные символы в полученной строке – Synapse вернул ровно то, что отдал нам сервер, т.е. строку в кодировке UTF-8. Примеры работы с различными кодировками текста в Synapse мы рассмотрим ниже. Если же Вы обратите внимание на значение переменной AllDocument, то увидите, что переменная содержит «сырые» данные, т.е. абсолютно всё, что было передано сервером, включая заголовки.
Подведем итоги по приведенному выше примеру. Событие OnReadFilter удобно использовать двух случаях:
- Когда необходимо провести фильтрацию присылаемых сервером данных до того как эти данные будут распределены по свойствам THTTPSend, а сами данные являются простым текстом, например, html-кодом страницы.
- Когда необходимо принудительно разорвать соединение с сервером после того как от сервера пришел необходимый объем данных.
Если же Вам необходимо скачать часть какого-то бинарного файла с сервера, то здесь OnReadFilter уже не подойдет и необходимо использовать свойства RangeStart и RangeEnd у THTTPSend. И именно этот пример мы и рассмотрим далее.
Четвертый пример: частичное скачивание файла с сервера. Частичный GET.
Теория
Что такое «частичный GET»? Варианты работы с байтовыми диапазонами.
Клиенты HTTP довольно часто сталкиваются с ситуацией, когда загрузка большого объема данных может быть прервана вследствие различных проблем, например, из-за разрыва соединения. В этом случае, когда клиент уже содержит часть документа, при следующем запросе желательно запрашивать не весь документ целиком, а лишь оставшуюся часть. Для решения этой задачи при использовании HTTP/1.1 в заголовках запроса возможно использование байтовых диапазонов. Подробную информацию о байтовых диапазонах в HTTP вы можете найти RFC 2616. Мы же в этом примере рассмотрим только основные моменты использования диапазонов в Synapse.
Если мы запрашиваем с сервера только часть документа, то такой запрос носит название частичный GET.
Диапазон запрашиваемых данных задается в заголовке Range и может представляться в нескольких вариантах.
Вариант 1. Запрос произвольного диапазона
В этом случае заголовок Range задается следующим образом:
Range: bytes=3000-5900
При этом запрашивается часть документа, начиная с 3000 байта по 5900 включительно.
Вариант 2. Запрос произвольного байта
Это частный случай первого варианты. При запросе одного байта значения начала и конца диапазона совпадают. Например, так мы можем запросить 3051 байт документа:
Range: bytes=3051—3051
Вариант 3. Запрос диапазона от конца документа
В этом случае заголовок задается в такой форме:
Range: bytes=-1000
При таком запросе сервер вернет последние 1000 байт документа (на рисунке – это байты от 5000 до 5999 включительно).
Вариант 4. Запрос диапазона от начала документа
В этом случае заголовок задается в следующей форме:
Range: bytes=5000-
При таком запросе сервер вернет первые 5000 байт документа (байты с 0 по 5000)
Вариант 5. Запрос нескольких фрагментов документа
В этом случае заголовок задается в следующей форме:
Range: bytes=100-1000,1000-
В этом случае здесь мы запросили два фрагмента файла.
Как проверить поддерживает ли сервер работы с байтовыми диапазонами?
Чтобы узнать поддерживает ли сервер работу с байтовыми диапазонами можно использовать два способа:
- Отправить на сервер запрос методом HEAD и проверить наличие заголовка “Accept—Ranges: bytes”, который указывает на то, что сервер поддерживает частичные GET.
- Отправить на сервер частичные GET, запросив произвольный фрагмент документа и проверить код статуса. Если код статуса равен 206, то сервер вернул нам запрошенную часть документа и, следовательно, поддерживает работу с байтовыми диапазонами.
Практика
Создаем новый проект Delphi (назовем его Ranges) и на главной форме приложения располагаем компоненты, как показано на рисунке ниже:
На форме расположены следующие компоненты:
- edURL: TEdit – поле ввода URL расположения документа
- btnCheck: TButton – кнопка «Проверить поддержку заголовков Ranges сервером»
- 2 TRadioButton для выбора способа запроса диапазона документа: с использованием свойств RangeStart и RangeEnd класса THTTPSend или же с использование собственного заголовка Range.
- edRangeStart, edRangeEnd: TEdit – поля ввода начала и конца запрашиваемого диапазона
- edRangeHeader: TEdit – поле ввода значения заголовка Range
- btnGet: TButton – кнопка для выполнения запроса к серверу
- memLog: TMemo – текстовый редактор для вывода лога работы программы.
Для начала, воспользуемся кодом примера №2 и перепишем метод GetTargetURL следующим образом:
function TForm2.GetTargetURL(const AStartURL: string; ARedirectMaximum: integer = 10): string; var ARedirectCount: integer; ALocationURL: string; HTTP:THTTPSend; function HeaderByName(const AHeaderName: string): string; var I: integer; begin Result := EmptyStr; for I := 0 to HTTP.Headers.Count - 1 do begin if StartsText(AHeaderName, HTTP.Headers[I]) then begin Result := copy(HTTP.Headers[I], Length(AHeaderName) + 3, Length(HTTP.Headers[I]) - 1); break; end; end; end; begin Result := AStartURL; HTTP:=THTTPSend.Create; try repeat if HTTP.HTTPMethod('HEAD', Result) then begin ALocationURL := HeaderByName('Location'); if Length(ALocationURL) > 0 then begin Result := ALocationURL; inc(ARedirectCount); end; end else exit; until ((HTTP.ResultCode <> 301) and (HTTP.ResultCode <> 302)) or (ARedirectCount >= ARedirectMaximum); finally HTTP.Free; end; end;
Теперь напишем функцию для проверки поддержки сервером работы с байтовыми диапазонами:
function TForm2.AcceptRanges: boolean; var Target: string; begin Result:=False; HTTPClient.Clear; Target:=GetTargetURL(edURL.Text); HTTPClient.Clear; HTTPClient.RangeStart:=1; HTTPClient.RangeEnd:=1; if HTTPClient.HTTPMethod('GET',Target) then Result:=HTTPClient.ResultCode=206; end;
Здесь мы проверяем поддержку сервером работы с диапазонами вторым способом, т.е. пробуем получить от сервера произвольный диапазон данных (в нашем примере запрашивается 1 байт). В случае, если сервер ответит нам с кодом статуса 206, то мы будем знать, что сервер гарантированно поддерживает работу с диапазонами.
Теперь, используя функцию AcceptRanges мы можем написать обработчик события OnClick кнопки btnCheck:
procedure TForm2.btnCheckClick(Sender: TObject); begin if AcceptRanges then ShowMessage('Есть поддержка Ranges') else ShowMessage('Ranges не поддерживается'); end;
И обработчик события OnClick у кнопки btnGet:
procedure TForm2.btnGetClick(Sender: TObject); var TargetURL: string; begin if AcceptRanges then begin HTTPClient.Clear; MemLog.Lines.Add('Есть поддержка Ranges. Пробуем выполнить запрос'); if rbProperties.Checked then begin HTTPClient.RangeStart:=StrToInt(edRangeStart.Text); HTTPClient.RangeEnd:=StrToInt(edRangeEnd.Text); end else HTTPClient.Headers.Add('Range: bytes='+edRangeHeader.Text); if HTTPClient.HTTPMethod('GET',GetTargetURL(edURL.Text)) then begin MemLog.Lines.Add(HTTPClient.Headers.Text); MemLog.Lines.Add(Format('С сервера загружено %d байт данных',[HTTPClient.Document.Size])); end else raise Exception.Create('Ошибка выполнения запроса') end end;
Здесь мы проверяем поддержку работы с байтовыми диапазонами у сервера и выполняем частичный GET в зависимости от того, какой вариант работы был выбран (с использованием свойств RangeStart/RangeEnd или с использованием собственных заголовков).
Теперь проверим работу нашей программы и посмотрим, что нам будет возвращать сервер.
Для начала, запросим с сервера произвольные 10 байт документа, используя свойства RangeStart/RangeEnd:
Теперь запросим последние данные со 101 байта и до конца документа:
Запрос произвольного 1 байта данных:
На этом возможности использования свойств RangeStart и RangeEnd заканчиваются. Остальные варианты работы с байтовыми диапазонами реализуются через собственные значения заголовка Range.
Запросим с сервера последние 100 байт документа:
И последний вариант – запрос нескольких произвольных фрагментов документа:
Если вы сравните все предыдущие ответы сервера с последним полученным, то увидите следующее «странное» поведение THTTPSend:
- Размер полученных с сервера данных явно не соответствует запрошенному. Так, последний ответ должен был содержать два фрагмента – на 100 и 2 байта соответственно, а сервер вернул 303 байта данных.
- В заголовках последнего ответа отсутствует заголовок Content-Range.
Последний результат работы программы также корректен, как и все предыдущие, просто в этом случае сервер вернул нам множественное содержимое, на что указывает заголовок ответа “Content-Type: multipart/byteranges”. Работу с множественным содержимым мы рассмотрим ниже.
Пятый пример: Использование GZip в Synapse.
Теория
Для того чтобы указать серверу перечень поддерживаемых клиентом способов кодирования тела запроса, в HTTP предусмотрен специальный заголовок для согласования содержимого – Accept-Encoding.
RFC 2616 определяет следующие возможные значения для этого заголовка:
- compress – кодирование с помощью Unix-утилиты «compress». Для кодирования используется алгоритм LZW
- gzip – кодирование с помощью Unix-утилиты «Gzip». Для кодирования используется алгоритм LZ77 (согласно RFC)
- deflate – для кодирования используется алгоритм deflate
- identity – данные передаются без сжатия.
В большинстве случаев, согласование содержимого происходит в два этапа:
1. Клиент включает в запрос заголовок Accept-Encoding, который содержит поддерживаемые способы кодирования. Например:
GET / HTTP/1.1 Host: www.webdelphi.ru Accept-Encoding: gzip, deflate
Здесь клиент сообщает серверу, что может принять данные сжатые с помощью gzip или deflate.
2. Если сервер поддерживает данные алгоритмы сжатия, он может вернуть ресурс закодированный одним из этих методов, а может и отказаться от кодирования, вернув ресурс в несжатом виде. При этом, если сервер возвращает сжатые данные, то в заголовки ответа включается заголовок Content-Encoding, указывающий способ кодирования. Например, ответ на указанный выше запрос может содержать следующий заголовок:
Content-Encoding: gzip
Использование в заголовке Content-Encoding значения identity не предусмотрено, т.е. если ответ сервера не содержит заголовок Content-Encoding, то это означает, что данные были переданы в несжатом виде.
При согласовании содержимого следует учитывать следующие возможные ситуации:
- Если в запрос не включен заголовок Accept-Encoding, то сервер может предполагать, что клиент воспримет любую кодировку информации и отправить клиенту данные сжатые, например, с помощью gzip
- Сервер может вернуть данные в несжатом виде даже в том случае, если в запрос включен заголовок Accept-Encoding.
- Сервер может вернуть данные в сжатом виде, даже, если Accept-Encoding содержит значение identity.
Первые две ситуации зависят от настроек сервера. Так, например, сервер может устанавливать минимальную версию HTTP, необходимую для сжатия ответа или установить минимальную длину ответа, необходимую для сжатия и т.д.
Третья ситуация относится скорее к недоработкам в программном обеспечении, конкретного ресурса в сети, чем к нормальной работе сервера. С подобной ситуацией автор столкнулся при работе с сайтом, использующем CMS WordPress. Включение заголовка
Accept—Encoding: identity
в запрос ни к чему не приводило – данные продолжали присылаться в сжатом виде. Однако стоило удалить на сайте плагин, отвечающий за сжатие данных перед их отправкой клиенту и настроить сжатие самостоятельно через файл .htaccess – проблема исчезла. В последствии оказалось, что только удаленный плагин обладал такой «особенностью» — сжимать данные даже, когда этого не требовалось.
В протоколе HTTP/1.1, так же как и в других заголовках согласования содержимого, в заголовке Accept-Encoding появилась возможность использовать параметр q — оценка качества (qvalue).
Оценка качества – это число от 0 до 1, определяющее степень предпочтения клиентом того или иного метода кодирования. Более предпочтительные методы помечаются более высокой оценкой качества. Если оценка качества отсутствует, то по умолчанию присваивается наивысшая оценка 1. Например, заголовок
Accept-Encoding: gzip;q=1.0, identity; q=0.5
Будет означать, что наиболее предпочтительным способом кодирования для клиента является gzip, если сервер не поддерживает gzip, то данные следует вернуть в несжатом виде.
Так же в HTTP/1.1 появилась возможность обобщать все методы кодирования с помощью символа звездочки «*» и согласование содержимого со стороны сервера.
Концепция согласование содержимого со стороны сервера заключается в следующем. Запрос клиента предшествует ответу сервера, поэтому целесообразно стратегию согласования содержимого начинать с предпочтений клиента (отправка в запросе заголовка Accept-Encoding). Если же сервер определяет, что он не может закодировать содержимое ресурса ни одним из указанных клиентом методов кодирования, он должен послать ему ответ с кодом состояния 406 «Not Acceptable». Если же на сервере содержимое ресурса может быть закодировано всего одним методом, то вместо ответа 406 «Not Acceptable» спецификация рекомендует вернуть ресурс, игнорируя заголовок Accept-Encoding.
Практика
Исходя из всего вышеизложенного, можно определить следующий алгоритм работы нашей программы:
- Если нам необходимо получить от сервера данные в сжатом виде, то включаем в запрос заголовок Accept-Encoding
- Т.к. сервер может установить минимальную версию HTTP, необходимую для сжатия, то также целесообразно указать версию протокола ‘1.1’ (на данный момент – это последняя версия HTTP)
- Отправляем запрос на сервер
- Получаем ответ и анализируем его заголовки:
- Если присутствует заголовок Content-Encoding, то данные пришли в сжатом виде – определяем метод сжатия, разархивируем данные и переходим к п.5.
- Если заголовок Content-Encoding отсутствует, то считаем, что данные пришли в несжатом виде и переходим к п.5.
- Показываем полученные данные
В этом простом алгоритме остается один вопрос – чем разархивировать данные?
В качестве «универсальной» библиотеки для работы с gzip, которая будет одинаково хорошо работать на любых версиях Delphi, начиная с Delphi 5, можно предложить библиотеку delphi zlib. Это бесплатная библиотека с открытым исходным кодом, которую вы можете скачать с сайта http://www.base2ti.com/?id=delphi.zlib.
Теперь реализуем рассмотренный выше алгоритм в виде небольшого приложения.
Создаем новый проект Delphi (назовем его «syna_gzip») и размещаем на форме приложения компоненты как показано на рисунке:
Теперь скачиваем библиотеку Delphi zlib (http://www.base2ti.com/?id=delphi.zlib), заходим в настройки проекта (Project —> Options —> Delphi Compiler) и указываем в поле «Search Path» путь к модулям библиотеки:
Подключаем в uses главного приложения следующие модули:
- HttpSend, SynaUtil из библиотеки Synapse
- ZLibExGZ из бибилотеки Delphi Zlib.
Теперь напишем обработчик кнопки «Get», в котором и реализуем наш алгоритм работы с Gzip в Synapse:
procedure Tfmain.btnGetClick(Sender: TObject); var Client: THTTPSend; OutStr: string; begin memData.Lines.Clear; Client:=THTTPSend.Create; try Client.Headers.Add('Accept-Encoding: gzip'); Client.Protocol:='1.1'; if Client.HTTPMethod('GET',edUrl.Text) then begin //привели заголовки к виду Name=Value HeadersToList(Client.Headers); //проверяем заголовок if Trim(Client.Headers.Values['Content-Encoding']) = 'gzip' then begin //считываем данные из потока OutStr:=ReadStrFromStream(Client.Document,Client.Document.Size); //разжимаем данные OutStr:=GZDecompressStr(OutStr); end else OutStr:=ReadStrFromStream(Client.Document,Client.Document.Size); //выводим текст в Memo memData.Lines.Add(OutStr); end; finally Client.Free end; end;
Теперь можно запустить приложение и проверить его работу:
Источники информации:
- http://ru.wikipedia.org/wiki/HTTP
- http://ru.wikipedia.org/wiki/%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_%D0%BA%D0%BE%D0%B4%D0%BE%D0%B2_%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D1%8F_HTTP
Вместо заключения
«За кадром» остались два приложения к главе, содержащие подробные описания методов и кодов состояния HTTP, а также небольшое описание по работе со Status 100. Как я сказал в начале этой статьи, при написании этой главы я старался избегать использования в примерах каких-либо определенных адресов сайтов (за исключением своего), может поэтому объем и содержание главы может показаться меньшим, чем ранее опубликованная большая статья на тему о работе THTTPSend. Также я не рассмотрел здесь и работу с HTTPS. Скажу лишь, что в дальнейшем, при написании книги, я ещё вернусь и к свойствам и методам THTTPSend где и постараюсь ликвидировать эти пробелы, но это уже будет совсем другая глава.
Книжная полка
Описание: Рассмотрены практические вопросы по разработке клиент-серверных приложений в среде Delphi 7 и Delphi 2005 с использованием СУБД MS SQL Server 2000, InterBase и Firebird. Приведена информация о теории построения реляционных баз данных и языке SQL. Освещены вопросы эксплуатации и администрирования СУБД.
|
||
Название: О чем не пишут в книгах по Delphi
Описание: Рассмотрены малоосвещенные вопросы программирования в Delphi. Описаны методы интеграции VCL и API. Показаны внутренние механизмы VCL и приведены примеры вмешательства в эти механизмы. Рассмотрено использование сокетов в Delphi: различные режимы их работы, особенности для протоколов TCP и UDP и др.
|
[…] Работа с HTTP-протоколом в Synapse […]
>воспользуемся одной из возможностей Delphi 2009 и выше
Маленькое уточнение. Class helpers появились в Delphi 2005.
блин :) Вот с этим вопросом как раз долго парился. Я то с Delphi 7 сразу на 2009 перешел. Спасибо, сейчас исправлю
Спасибо авторам за данный материал, понравилась ваша глава про сокеты.
Сейчас разрабатываю свою базовую библиотеку, смотрю реализацию Indy и Synapse.
Очень много идей просто появилось, ну и подумал нежели переписать synapse под свои замыслы, лучше напишу что-то свое :)
Спасибо!
Правда, как мне кажется, задумка с загрузкой PDF через сайт doc2pdf не слишком удачна. Результат уж больно странный на вид получился. Документ читать можно только в масштабе 200%…
JuryP, просто на тот момент (где-то год назад) это был единственный более менее вменяемый плагин для WordPress для конвертации статей в pdf. Конечно, после того как весь текст книги будет написан я соберу нормальный PDF и выложу в доступ.
Прочитал, спасибо за статью…
PDF версия этой главы будет выложена?
Сорри за задержку. Главу в PDF выложил в конце этой статьи. Скачивайте
Спасибо за статью.
Пару небольших замечаний:
1. В одном из примеров мы получаем с сайта текст в кодировке UTF-8. В статье вставлен поясняющий текст «Примеры работы с различными кодировками текста в Synapse мы рассмотрим ниже.», но ниже этот момент не рассматривается.
2. В конечной части документа приведён алгоритм взаимодействия клиента и сервера при использовании кодирования/сжатия информации. В данном алгоритме дважды встречается переход на п.5, но в самом алгоритме нет «п.5» (я так понимаю, по смыслу это пункт 4.3).
Спасибо, Дмитрий, за замечания. По п.1. пока могу только сказать, что работа с кодировками будет рассмотрена позже (не в главе по HTTP). Текс вставлялся копипастом без особых правок, поэтому отсыл на другую часть книги остался. А п.2 — да, тут ошибка вышла — подправлю :)
Ссылочка на pdf-ку этой главы померла. Если не затруднит, обновите, пожалуйста!
Обновил
pos(‘content-length’,lowercase(HTTPClient.Headers[i]))>0
>0
>0
в браузере пишет
& g t ; 0
Простой вопрос зачем писать книгу по Synapse если он сторонний и слишком отстал от родного Indy ?
Очень понравилась статья. Сделал скачивание файла и обработку редиректа по примере. С ссылками http всё работает хорошо но со ссылками https определяет правильный размер но качает только размер Headers.text
В uses добавил ssl_openssl и в папку проекта закинул libeay32.dll и ssleay32.dll.
Подскажите что ещё нужно чтоб нормально качало по https?
Также если можно напишите как проверить наличие уже скачанного файла и качать новый только тогда если он отличается от скачанного.
Подскажите что ещё нужно чтоб нормально качало по https? Пока не подскажу, т.к. с такой ситуацией сталкиваться не приходилось. Хотя качал по https и гиабайтный файлы. Если не секретно — дайте код — разберемся как проверить наличие уже скачанного файла if FileExist(AFileName) then //файл найден качать новый только тогда если он отличается от скачанного Всё зависит от того с чем имеете дело. Если с API — посмотрите выдает ли сервер дату последнего изменения файла и по ней решайте, что делать — качать или нет. Если просто сайт, то тут, наверное, придётся решать вопрос заменять старый файл вновь скачанным или нет.… Подробнее »
Исходник точно сделан по примерах 1 и 2 из этой статьи.
Вот ссылка на исходник https://yadi.sk/d/tOE9HMilccSoc
Я в TextEdit указал уже ссылку https по которой хочу скачать, в качестве примера я взял дистрибутив Mozilla Firefox.
Через снифер смотрел что обработка редиректа и определение размера файла проходит правильно а закачка обрывается на размере равному размеру Headers.text.
При этом ссылки http качает правильно.
Помогите пожалуйста решить проблему закачки по https!
Виталий, я посмотрю исходник и постараюсь в ближайшее время дать вам ответ
В обработчике OnClick() кнопки ПЕРЕД выполнением:
HTTPClient.HTTPMethodExt
надо почистить HTTPClient от результатов предыдущего запроса, т.е. вставить одну строку:
HTTPClient.Clear;
После чего дистрибутив нормально качается.
и ещё один момент из того же обработчика:
HTTPClient := THTTPSend.Create;
try
aTarget := HTTPClient.GetTargetURL(Edit1.Text, 15);
//тут что-то ещё делается
if HTTPClient.HTTPMethodExt('GET', aTarget, True, 15) then <-- Зачем тут True? Конечный URL и так уже на руках, не надо дёргать сервер без надобности //тут опять что-то делается finally HTTPClient.Free; end;
спасибо
Сделал как Вы написали но при скачивании по https исключение «Ошибка: не удалось получить заголовки» Проверил в Debug увидел что при https не проходит даже функция GetTargetURL не говоря уже о скачивании. Библиотеки SSL в папке с проектом присутствуют.
Буду очень благодарен если Вы выложите на Яндекс.Диск папку с моим подправленным проектом который скачивает по https с редиректами.
Разобрался сам, проблемы были в библиотеках.
Ещё раз большое спасибо за статью. Когда планируете писать продолжение книги?
не за что. Работа над книгой и не прекращалась, просто сейчас у меня практически не остается на неё времени и поэтому пишется глава очень медленно
Для того, чтобы проверить не изменился ли файл, достаточно отправить HEAD-запрос серверу и в полученном заголовке проверить значение «Last-Modified:». Большинство серверов возвращают данное поле.
Также считаю хорошим тоном, после завершения закачки устанавливать дату последнего изменения файла, полученную с сервера.
Как направить synapse на нужный сетевой интерфейс. Т.е. в компьютере несколько сетевых интерфейсов. Например два физических подключения к двум провайдерам или одно физическое, но на нем поднят еще и VPN.
Получаю Дату последнего запроса (Response Headers) от сервера:
Date: Wed, 18 Feb 2016 11:20:59 GMT
Как мне вывести (только Дату) в виде 18.02.2016 в компоненте TLabel?
В юните synautil есть function DecodeRfcDateTime(Value: string): TDateTime;