Возможно, я плохо искал, но что-то мне не удалось найти для Erlang HTTP-клиента с поддержкой gzip . В этой заметке я продемонстрирую небольшую поделку, которая умеет получать gzip’ованные данные, используя библиотеки zlib и ibrowse.

Мы с вами уже использовали ibrowse, когда парсили выдачу Google на Erlang . Посылка запроса осуществлялась очень просто:

case ibrowse: send_req ( Url , [ ] , get ) of
{ ok , «200» , _Headers , Data } ->
parse_serp ( Data ) ;
Rslt ->
io : format ( «Request failed: ~p~n» , [ Rslt ] )
end .

Для того, чтобы принимать данные в gzip, достаточно добавить к запросу заголовок Accept-Encoding: gzip , а при получении ответа проверять, не пришел ли Content-Encoding: gzip и, если это так, распаковывать данные с помощью библиотеки zlib :

case ibrowse: send_req ( Url , [ { «Accept-Encoding» , «gzip» } ] , get ) of
{ ok , «200» , Headers , Data } ->
case proplists : get _ value ( «Content-Encoding» , Headers ) of
«gzip» ->
parse_serp ( zlib : gunzip ( Data ) ) ;
_ContentEncoding ->
parse_serp ( Data )
end ;
Rslt ->
io : format ( «Request failed: ~p~n» , [ Rslt ] )
end .

Жизнь прекрасна и удивительна, расходимся по домам. Правда, есть нюанс.

Что, если мы получим по HTTP 10 Гб данных? А если их окажется 10 Гб после распаковки? Придет OOM Killer и начнет наводить порядок. Для решения этой проблемы можно попросить ibrowse писать ответ сервера в файл с помощью опции save_response_to_file , но это не очень поможет в контексте текущей задачи (распаковать gzip).

К счастью, ibrowse позволяет обрабатывать данные частями, прямо по мере их получения. Как и библиотека zlib. Вот как примерно это выглядит:

module ( ibrowse_get_stream ) .
export ( [ main / 0 , main / 1 ] ) .

% copy-pasted from /usr/lib/erlang/lib/erts-5.9.1/src/zlib.erl
define ( MAX_WBITS , 15 ) .

main ( ) ->
io : format ( «Usage: get  ~n» ) .

main ( [ UrlAtom | [ OutFileAtom |_ ] ] ) ->
UrlString = atom_to_list ( UrlAtom ) ,
io : format ( «Fetching ~s …~n» , [ UrlString ] ) ,
OutFileString = atom_to_list ( OutFileAtom ) ,
{ ok , Fid } = file : open ( OutFileString , [ write ] ) ,
ibrowse: start ( ) ,
{ ibrowse_req_id , _RequestId } = ibrowse: send_req (
UrlString ,
[ { «Accept-Encoding» , «gzip» } ] , get , [ ] ,
[ { stream_to , self ( ) } , { response_format , binary } ] ,
infinity
) ,
receive_loop (
Fid ,
fun ( _Data ) -> io : format ( «Error: undefined processor!~n» ) end ,
fun ( ) -> io : format ( «Error: undefined finalizer!~n» ) end
) ,
file : close ( Fid ) ;

main ( _ ) ->
main ( ) .

receive_loop ( Fid , Processor , Finalizer ) ->
receive
{ ibrowse_async_headers , _RequestId , _Code , Headers } ->
{ NewProcessor , NewFinalizer } =
case proplists : get _ value ( «Content-Encoding» , Headers ) of
«gzip» ->
io : format ( «Gzipped data received~n» ) ,
ZlibStream = zlib : open ( ) ,
ok = zlib : inflateInit ( ZlibStream , 16 + ? MAX_WBITS ) ,
{
fun ( Data ) ->
Decompressed = zlib : inflate ( ZlibStream , Data ) ,
file : write ( Fid , Decompressed )
end ,
fun ( ) ->
ok = zlib : inflateEnd ( ZlibStream ) ,
zlib : close ( ZlibStream )
end
} ;
ContentEncoding ->
io : format (
«Plaintext data received (ContentEncoding = ~p)~n» ,
[ ContentEncoding ]
) ,
{ fun ( Data ) -> file : write ( Fid , Data ) end , fun ( ) -> ok end }
end ,
receive_loop ( Fid , NewProcessor , NewFinalizer ) ;

{ ibrowse_async_response , _RequestId , Data } ->
io : format ( «Data received, time = ~p~n» , [ time ( ) ] ) ,
P rocessor ( Data ) ,
receive_loop ( Fid , Processor , Finalizer ) ;

{ ibrowse_async_response_end , _RequestId } ->
F inalizer ( )
end .

С помощью опции { stream_to, self() } мы говорим ibrowse присылать нам сообщения с данными по мере их получения. Затем эти сообщения принимаются в функции receive_loop/3 . При получении заголовков мы определяем, сжаты данные с помощью gzip или не сжаты, и в зависимости от этого определяем две функции — Processor и Finalizer. Первая отвечает за распаковку данных, если они запакованы, вторая — за освобождение ресурсов, если они были выделены. Лямбды и замыкания… ммм, красота!

Функция zlib:inflate/2 в действительности возвращает iolist() , а не binary() , как можно было бы ожидать. В приведенном коде после распаковки данные сразу передаются в функцию file:write/2 , поэтому все прекрасно работает. Однако если вы ожидаете получить именно binary() , придется воспользоваться функцией iolist_to_binary/1 .

Обратите внимание на последний аргумент функции ibrowse:send_req/6 , атом infinity . По умолчанию ibrowse ждет данные от сервера в течение 30-и секунд и, если после этого периода передача не завершится, тихо-мирно умирает. Функция receive_loop/3 не получит никакого сообщения, а в консоль ничего не будет выведено, даже если вы вызвали ibrowse:trace_on/0 . Вы разве что можете воспользоваться timer:send_after/2 . Передача infinity последним аргументом в ibrowse:send_req/6 меняет это поведение, данные будут получаться столько времени, сколько потребуется.

Опция { response_format, binary } говорит ibrowse присылать данные в виде binary() , а не списка байт. Согласно моим замерам, в этом случае передача осуществляется чуточку быстрее.

По умолчанию ibrowse отправляет сообщения асинхронно. Я обнаружил, что если синхронизировать отправку и получение сообщений с помощью опции { stream_to, { self(), once }} , а также вызовов ibrowse:stream_next/1 и ibrowse:stream_close/1 , передача осуществляется быстрее. Знающие люди говорят, что это может быть связано со следующим. По умолчанию перед отправкой данные буферизируются, что приводит к дополнительным накладным расходам. Об этом можно судить по тому, что в случае использования синхронного подхода сообщения от ibrowse приходят чаще и общее их количество больше. Синхронность хороша еще и тем, что она исключает возможность переполнения очереди сообщений.

Полную версию кода вы можете посмотреть в этом архиве . По скорости он не сильно уступает связке wget с gunzip. Во время его тестирования я обратил внимание на один интересный момент. Код прекрасно работает со всеми сайтами, на которых я его проверял, за исключением mail.ru. Расследование показало, что mail.ru отдает не совсем корректный gzip, в чем можно убедиться следующим образом:

wget —header ‘Accept-Encoding: gzip’ -S http: // mail.ru -O |
gunzip > mail.html

Утилита gunzip все успешно распаковывает, но в конце выводит:

gunzip: truncated input

Почему на Мейле так сделано и как должен быть написан код, чтобы обрабатывать подобную ситуацию, мне неизвестно.

Дополнение: HTTP-прокси на Erlang , позволяющий, помимо прочего, сжимать данные gzip’ом.

admin

Share
Published by
admin

Recent Posts

Консоль удаленного рабочего стола(rdp console)

Клиент удаленного рабочего стола (rdp) предоставляет нам возможность войти на сервер терминалов через консоль. Что…

1 месяц ago

Настройка сети в VMware Workstation

В VMware Workstation есть несколько способов настройки сети гостевой машины: 1) Bridged networking 2) Network…

1 месяц ago

Логи брандмауэра Windows

Встроенный брандмауэр Windows может не только остановить нежелательный трафик на вашем пороге, но и может…

1 месяц ago

Правильный способ отключения IPv6

Вопреки распространенному мнению, отключить IPv6 в Windows Vista и Server 2008 это не просто снять…

1 месяц ago

Ключи реестра Windows, отвечающие за параметры экранной заставки

Параметры экранной заставки для текущего пользователя можно править из системного реестра, для чего: Запустите редактор…

1 месяц ago

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

В этой статье расскажу про возможность просмотра журналов событий из командной строки. Эти возможности можно…

1 месяц ago