Возможно, я плохо искал, но что-то мне не удалось найти для Erlang HTTP-клиента с поддержкой gzip . В этой заметке я продемонстрирую небольшую поделку, которая умеет получать gzip’ованные данные, используя библиотеки zlib и ibrowse.
Мы с вами уже использовали ibrowse, когда парсили выдачу Google на Erlang . Посылка запроса осуществлялась очень просто:
{ ok , «200» , _Headers , Data } ->
parse_serp ( Data ) ;
Rslt ->
io : format ( «Request failed: ~p~n» , [ Rslt ] )
end .
Для того, чтобы принимать данные в gzip, достаточно добавить к запросу заголовок Accept-Encoding: gzip
, а при получении ответа проверять, не пришел ли Content-Encoding: gzip
и, если это так, распаковывать данные с помощью библиотеки zlib :
{ 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. Вот как примерно это выглядит:
— 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, в чем можно убедиться следующим образом:
gunzip > mail.html
Утилита gunzip все успешно распаковывает, но в конце выводит:
Почему на Мейле так сделано и как должен быть написан код, чтобы обрабатывать подобную ситуацию, мне неизвестно.
Дополнение: HTTP-прокси на Erlang , позволяющий, помимо прочего, сжимать данные gzip’ом.