уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Продолжаем работу над книгой про библиотеку 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).

Все заголовки разделяются на четыре основных группы:

  1. General Headers (рус. Основные заголовки) — должны включаться в любое сообщение клиента и сервера.
  2. Request Headers (рус. Заголовки запроса) — используются только в запросах клиента.
  3. Response Headers (рус. Заголовки ответа) — только для ответов от сервера.
  4. 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

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’а. Однако следует помнить о том, что сервер не обязан отправлять этот заголовок клиенту и клиент должен быть готов к такой ситуации, когда размер загружаемых данных заранее не известен.

Таким образом, алгоритм работы нашего приложение может быть следующим:

  1. Отправляем запрос методом HEAD на сервер и получаем список заголовков
  2. Ищем в заголовках Content-Length
    1. Если Content-Length найден, то получаем значение заголовка и присваиваем это значение свойству Max у ProgressBar’а
    2. Если заголовок Content-Length не найден, то скачиваем файл без заполнения ProgressBar’а

Теперь попробуем реализовать этот алгоритм в приложении. Создаем новый проект Delphi (назовем его syna_progress) и на главной форме приложения размещаем компоненты как показано на рисунке ниже:

interface

На форме приложения расположены следующие компоненты:

  • 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 процесс загрузки с сайта выглядит следующим образом:

  1. Клиент отправляет запрос методом GET на заданный URL
  2. Сервер возвращает код статуса 302 и в заголовке Location указывает адрес по которому находится файл
  3. Клиент получает адрес из заголовка 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.

Третий пример: фильтруем считываемые данные.

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

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

Между тем Synapse позволяет нам фильтровать данные, считываемые из сокета. Для этого используется событие OnReadFilter. Рассмотрим работу с этим событием и напишем программу, которая будет получать с сервера только ту часть web-страницы, в которой содержатся необходимые нам данные. Создадим новый проект Delphi (назовем его «DataFilter») и на главной форме приложения разместим компоненты, как показано на рисунке ниже:

interface

На форме расположены следующие компоненты:

  • 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 и документ был скачан полностью.

Теперь можете проверить работу программы. Запустите приложение и задайте, например, исходные данные как показано на рисунке ниже:

test

Результатом работы будет следующая строка:

test2

И пусть вас не смущают непонятные символы в полученной строке – Synapse вернул ровно то, что отдал нам сервер, т.е. строку в кодировке UTF-8. Примеры работы с различными кодировками текста в Synapse мы рассмотрим ниже. Если же Вы обратите внимание на значение переменной AllDocument, то увидите, что переменная содержит «сырые» данные, т.е. абсолютно всё, что было передано сервером, включая заголовки.

Подведем итоги по приведенному выше примеру. Событие OnReadFilter удобно использовать двух случаях:

  1. Когда необходимо провести фильтрацию присылаемых сервером данных до того как эти данные будут распределены по свойствам THTTPSend, а сами данные являются простым текстом, например, html-кодом страницы.
  2. Когда необходимо принудительно разорвать соединение с сервером после того как от сервера пришел необходимый объем данных.

Если же Вам необходимо скачать часть какого-то бинарного файла с сервера, то здесь OnReadFilter уже не подойдет и необходимо использовать свойства RangeStart и RangeEnd у THTTPSend. И именно этот пример мы и рассмотрим далее.

Четвертый пример: частичное скачивание файла с сервера. Частичный GET.

Теория

Что такое «частичный GET»? Варианты работы с байтовыми диапазонами.

Клиенты HTTP довольно часто сталкиваются с ситуацией, когда загрузка большого объема данных может быть прервана вследствие различных проблем, например, из-за разрыва соединения. В этом случае, когда клиент уже содержит часть документа, при следующем запросе желательно запрашивать не весь документ целиком, а лишь оставшуюся часть. Для решения этой задачи при использовании HTTP/1.1 в заголовках запроса возможно использование байтовых диапазонов. Подробную информацию о байтовых диапазонах в HTTP вы можете найти RFC 2616. Мы же в этом примере рассмотрим только основные моменты использования диапазонов в Synapse.

Если мы запрашиваем с сервера только часть документа, то такой запрос носит название частичный GET.

Диапазон запрашиваемых данных задается в заголовке Range и может представляться в нескольких вариантах.

Вариант 1. Запрос произвольного диапазона

В этом случае заголовок Range задается следующим образом:

range_1_thumb

Range: bytes=3000-5900

При этом запрашивается часть документа, начиная с 3000 байта по 5900 включительно.

Вариант 2. Запрос произвольного байта

Это частный случай первого варианты. При запросе одного байта значения начала и конца диапазона совпадают. Например, так мы можем запросить 3051 байт документа:

range_2_thumb

Range: bytes=30513051

Вариант 3. Запрос диапазона от конца документа

В этом случае заголовок задается в такой форме:range_4_thumb

Range: bytes=-1000

 

При таком запросе сервер вернет последние 1000 байт документа (на рисунке – это байты от 5000 до 5999 включительно).

Вариант 4. Запрос диапазона от начала документа

В этом случае заголовок задается в следующей форме:

range_3_thumb

Range: bytes=5000-

При таком запросе сервер вернет первые 5000 байт документа (байты с 0 по 5000)

Вариант 5. Запрос нескольких фрагментов документа

В этом случае заголовок задается в следующей форме:

range_5_thumb

Range: bytes=100-1000,1000-

В этом случае здесь мы запросили два фрагмента файла.

Как проверить поддерживает ли сервер работы с байтовыми диапазонами?

Чтобы узнать поддерживает ли сервер работу с байтовыми диапазонами можно использовать два способа:

  1. Отправить на сервер запрос методом HEAD и проверить наличие заголовка “AcceptRanges: bytes”, который указывает на то, что сервер поддерживает частичные GET.
  2. Отправить на сервер частичные GET, запросив произвольный фрагмент документа и проверить код статуса. Если код статуса равен 206, то сервер вернул нам запрошенную часть документа и, следовательно, поддерживает работу с байтовыми диапазонами.

Практика

Создаем новый проект Delphi (назовем его Ranges) и на главной форме приложения располагаем компоненты, как показано на рисунке ниже:

ranges_1

На форме расположены следующие компоненты:

  • 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:

ranges_2

 Теперь запросим последние данные со 101 байта и до конца документа:

ranges_3

Запрос произвольного 1 байта данных:

ranges_4

На этом возможности использования свойств RangeStart и RangeEnd заканчиваются. Остальные варианты работы с байтовыми диапазонами реализуются через собственные значения заголовка Range.

Запросим с сервера последние 100 байт документа:

ranges_5

И последний вариант – запрос нескольких произвольных фрагментов документа:

ranges_6

Если вы сравните все предыдущие ответы сервера с последним полученным, то увидите следующее «странное» поведение THTTPSend:

  1. Размер полученных с сервера данных явно не соответствует запрошенному. Так, последний ответ должен был содержать два фрагмента – на 100 и 2 байта соответственно, а сервер вернул 303 байта данных.
  2. В заголовках последнего ответа отсутствует заголовок 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 – данные передаются без сжатия.

Примечание: могут использоваться и другие значения, указывающие способ кодирования содержимого. Так, например, браузер Google Chrome может использовать в заголовке AcceptEncoding значение SDCH.

В большинстве случаев, согласование содержимого происходит в два этапа:

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, то это означает, что данные были переданы в несжатом виде.

При согласовании содержимого следует учитывать следующие возможные ситуации:

  1. Если в запрос не включен заголовок Accept-Encoding, то сервер может предполагать, что клиент воспримет любую кодировку информации и отправить клиенту данные сжатые, например, с помощью gzip
  2. Сервер может вернуть данные в несжатом виде даже в том случае, если в запрос включен заголовок Accept-Encoding.
  3. Сервер может вернуть данные в сжатом виде, даже, если Accept-Encoding содержит значение identity.

 

Первые две ситуации зависят от настроек сервера. Так, например, сервер может устанавливать минимальную версию HTTP, необходимую для сжатия ответа или установить минимальную длину ответа, необходимую для сжатия и т.д.

Третья ситуация относится скорее к недоработкам в программном обеспечении, конкретного ресурса в сети, чем к нормальной работе сервера. С подобной ситуацией автор столкнулся при работе с сайтом, использующем CMS WordPress. Включение заголовка

AcceptEncoding: 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.

Практика

Исходя из всего вышеизложенного, можно определить следующий алгоритм работы нашей программы:

  1. Если нам необходимо получить от сервера данные в сжатом виде, то включаем в запрос заголовок Accept-Encoding
  2. Т.к. сервер может установить минимальную версию HTTP, необходимую для сжатия, то также целесообразно указать версию протокола ‘1.1’ (на данный момент – это последняя версия HTTP)
  3. Отправляем запрос на сервер
  4. Получаем ответ и анализируем его заголовки:
    1. Если присутствует заголовок Content-Encoding, то данные пришли в сжатом виде – определяем метод сжатия, разархивируем данные и переходим к п.5.
    2. Если заголовок Content-Encoding отсутствует, то считаем, что данные пришли в несжатом виде и переходим к п.5.
    3. Показываем полученные данные

В этом простом алгоритме остается один вопрос – чем разархивировать данные?

В качестве «универсальной» библиотеки для работы с gzip, которая будет одинаково хорошо работать на любых версиях Delphi, начиная с Delphi 5, можно предложить библиотеку delphi zlib. Это бесплатная библиотека с открытым исходным кодом, которую вы можете скачать с сайта http://www.base2ti.com/?id=delphi.zlib.

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

Создаем новый проект Delphi (назовем его «syna_gzip») и размещаем на форме приложения компоненты как показано на рисунке:

Gzip_1

Теперь скачиваем библиотеку Delphi zlib (http://www.base2ti.com/?id=delphi.zlib), заходим в настройки проекта (Project —> Options —> Delphi Compiler) и указываем в поле «Search Path» путь к модулям библиотеки:

Gzip_2

Подключаем в 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;

Теперь можно запустить приложение и проверить его работу:

Gzip_3


Источники информации:


Вместо заключения

«За кадром» остались два приложения к главе, содержащие подробные описания методов и кодов состояния HTTP, а также небольшое описание по работе со Status 100. Как я сказал в начале этой статьи, при написании этой главы я старался избегать использования в примерах каких-либо определенных адресов сайтов (за исключением своего), может поэтому объем и содержание главы может показаться меньшим, чем ранее опубликованная большая статья на тему о работе THTTPSend. Также я не рассмотрел здесь и работу с HTTPS. Скажу лишь, что в дальнейшем, при написании книги, я ещё вернусь и к свойствам и методам THTTPSend где и постараюсь ликвидировать эти пробелы, но это уже будет совсем другая глава.

Следующая глава книги «Глава 2 «Работа с FTP в Synapse»«
Предыдущая глава книги «Глава 0 «Сокеты»«

 Скачать главу можно здесь

Книжная полка

Описание: Рассмотрены практические вопросы по разработке клиент-серверных приложений в среде Delphi 7 и Delphi 2005 с использованием СУБД MS SQL Server 2000, InterBase и Firebird. Приведена информация о теории построения реляционных баз данных и языке SQL. Освещены вопросы эксплуатации и администрирования СУБД.
купить книгу delphi на ЛитРес
Описание: Рассмотрены малоосвещенные вопросы программирования в Delphi. Описаны методы интеграции VCL и API. Показаны внутренние механизмы VCL и приведены примеры вмешательства в эти механизмы. Рассмотрено использование сокетов в Delphi: различные режимы их работы, особенности для протоколов TCP и UDP и др.
купить книгу delphi на ЛитРес
5 1 голос
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
29 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
trackback

[…] Работа с HTTP-протоколом в Synapse […]

Kazantsev Alexey
11/01/2014 14:55

>воспользуемся одной из возможностей Delphi 2009 и выше

Маленькое уточнение. Class helpers появились в Delphi 2005.

Алексей
Алексей
12/01/2014 04:54

Спасибо авторам за данный материал, понравилась ваша глава про сокеты.
Сейчас разрабатываю свою базовую библиотеку, смотрю реализацию Indy и Synapse.
Очень много идей просто появилось, ну и подумал нежели переписать synapse под свои замыслы, лучше напишу что-то свое :)

JuryP
JuryP
12/01/2014 12:52

Спасибо!
Правда, как мне кажется, задумка с загрузкой PDF через сайт doc2pdf не слишком удачна. Результат уж больно странный на вид получился. Документ читать можно только в масштабе 200%…

Ural
Ural
16/01/2014 06:35

Прочитал, спасибо за статью…

never
never
03/02/2014 13:08

PDF версия этой главы будет выложена?

Дмитрий Кудрявцев
Дмитрий Кудрявцев
05/02/2014 17:17

Спасибо за статью.
Пару небольших замечаний:
1. В одном из примеров мы получаем с сайта текст в кодировке UTF-8. В статье вставлен поясняющий текст «Примеры работы с различными кодировками текста в Synapse мы рассмотрим ниже.», но ниже этот момент не рассматривается.
2. В конечной части документа приведён алгоритм взаимодействия клиента и сервера при использовании кодирования/сжатия информации. В данном алгоритме дважды встречается переход на п.5, но в самом алгоритме нет «п.5» (я так понимаю, по смыслу это пункт 4.3).

Сергей
Сергей
06/07/2014 14:57

Ссылочка на pdf-ку этой главы померла. Если не затруднит, обновите, пожалуйста!

johnfx
johnfx
15/09/2014 09:10

pos(‘content-length’,lowercase(HTTPClient.Headers[i]))>0

>0

>0

johnfx
johnfx
15/09/2014 09:11

в браузере пишет
& g t ; 0

Avazart
Avazart
07/10/2014 02:02

Простой вопрос зачем писать книгу по Synapse если он сторонний и слишком отстал от родного Indy ?

Виталий
Виталий
10/11/2014 04:14

Очень понравилась статья. Сделал скачивание файла и обработку редиректа по примере. С ссылками http всё работает хорошо но со ссылками https определяет правильный размер но качает только размер Headers.text
В uses добавил ssl_openssl и в папку проекта закинул libeay32.dll и ssleay32.dll.

Подскажите что ещё нужно чтоб нормально качало по https?

Также если можно напишите как проверить наличие уже скачанного файла и качать новый только тогда если он отличается от скачанного.

Виталий
Виталий
10/11/2014 18:21
Ответить на  Vlad

Исходник точно сделан по примерах 1 и 2 из этой статьи.
Вот ссылка на исходник https://yadi.sk/d/tOE9HMilccSoc

Я в TextEdit указал уже ссылку https по которой хочу скачать, в качестве примера я взял дистрибутив Mozilla Firefox.

Через снифер смотрел что обработка редиректа и определение размера файла проходит правильно а закачка обрывается на размере равному размеру Headers.text.

При этом ссылки http качает правильно.
Помогите пожалуйста решить проблему закачки по https!

Виталий
Виталий
15/11/2014 02:14
Ответить на  Vlad

спасибо

Виталий
Виталий
16/11/2014 03:12
Ответить на  Vlad

Сделал как Вы написали но при скачивании по https исключение «Ошибка: не удалось получить заголовки» Проверил в Debug увидел что при https не проходит даже функция GetTargetURL не говоря уже о скачивании. Библиотеки SSL в папке с проектом присутствуют.

Буду очень благодарен если Вы выложите на Яндекс.Диск папку с моим подправленным проектом который скачивает по https с редиректами.

Виталий
Виталий
16/11/2014 03:27
Ответить на  Vlad

Разобрался сам, проблемы были в библиотеках.
Ещё раз большое спасибо за статью. Когда планируете писать продолжение книги?

Александр
Александр
17/09/2016 19:44
Ответить на  Vlad

Для того, чтобы проверить не изменился ли файл, достаточно отправить HEAD-запрос серверу и в полученном заголовке проверить значение «Last-Modified:». Большинство серверов возвращают данное поле.
Также считаю хорошим тоном, после завершения закачки устанавливать дату последнего изменения файла, полученную с сервера.

Евгений
Евгений
21/01/2015 18:05

Как направить synapse на нужный сетевой интерфейс. Т.е. в компьютере несколько сетевых интерфейсов. Например два физических подключения к двум провайдерам или одно физическое, но на нем поднят еще и VPN.

Любопытный
Любопытный
20/03/2016 23:52

Получаю Дату последнего запроса (Response Headers) от сервера:

Date: Wed, 18 Feb 2016 11:20:59 GMT

Как мне вывести (только Дату) в виде 18.02.2016 в компоненте TLabel?

Павел Яремчук
30/01/2017 16:30
Ответить на  Любопытный

В юните synautil есть function DecodeRfcDateTime(Value: string): TDateTime;