Возможно, я плохо искал, но что-то мне не удалось найти для 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’ом.

EnglishRussianUkrainian