Доброго времени суток!

Хочу познакомить вас с библиотекой dlang-requests. Для чего она? Для D она хочет быть тем-же, чем python-requests является для python, то есть — удобным http-(и ftp) клиентом. Автор клялся, что при написании библиотеки его целями были:

  1. удобный, простой интерфейс
  2. производительность сопоставимая с libcurl
  3. сочетаемость со стандартной библиотекой D

Первая часть статьи будет состоять из примеров использования dlang-requests для наиболее часто встречающихся задач.

Начнем с запросов GET


Cоздадим самое простое приложение, использующее dlang-requests (делайте копипаст прямо в шелл если вы под юникс-подобной осью):

Создаем минимальное приложение c помощью dub
mkdir test-requests && cd test-requests && dub init . requests
cat >source/app.d<<EOF
import std.stdio;
import requests;

void main()
{
    writeln(getContent("http://httpbin.org"));
}
EOF
dub run

В результате выполнения команды dub run в консоли должен нарисоваться html, вытянутый из корня сайта httpbin.org.

Пока всё просто: в случае, если вам нужно только содержание документа, вы можете использовать getContent(url). Этот вызов просто вернет вам буфер с документом.

Почему буфер а не просто массив байт? Потому что в большинстве случаев в буфере будут находиться данные полученные непосредственно из сети, без лишних копирований и сборки строк из кусочков. Если вам нужно — буфер легко превращается в массив байт с помощью метода data():
преобразуем ответ в массив байт
import std.stdio;
import requests;

void main()
{
    auto content = getContent("http://httpbin.org");
    writeln(typeid(content.data()));
}


Но нужно ли вам это? Ведь буфер поддерживает множество примитивов Range, и вы можете использовать его напрямую для передачи в соответствующие алгоритмы. Например, нам не нужно преобразовывать контент в string для того, что-бы подсчитать число строк в ответе, поскольку
ответ может быть обработан алгоритмами стандарной библиотеки, работающими с range, (заодно смотрим какими свойствами обладает ответ)
import std.stdio;
import std.range.primitives;
import std.algorithm;
import requests;

void main()
{
    auto content = getContent("http://httpbin.org");
    writeln(content.splitter('\n').count);

    alias type = typeof(content);
    static assert(isInputRange!type);
    static assert(isForwardRange!type);
    static assert(hasLength!type);
    static assert(hasSlicing!type);
    static assert(isBidirectionalRange!type);
    static assert(isRandomAccessRange!type);
}

Eще пример: мы можем без дополнительных усилий распарсить полученный json:
парсим json из ответа
import std.stdio;
import std.json;
import requests;

void main()
{
    auto content = getContent("http://httpbin.org/get");
    auto json = parseJSON(content);
    writeln(json);
}

Перед тем как разобраться с post, посмотрим что делать если нам нужен get с параметрами.
запрашиваем get с параметрами
import std.stdio;
import std.json;
import requests;

void main()
{
    auto content = getContent("http://httpbin.org/get",
                             queryParams("name", "bob", "age", 101));
    auto json = parseJSON(content);
    writeln(json);
}



Переходим к запросам POST.


Первый и самый простой post это post в веб-форму, используя form-urlencoded. Такой post используется обычно для передачи небольших объемов данных и по виду вызова напоминает вызов get c параметрами:
POST в form-urlencoded
import std.stdio;
import std.json;
import requests;

void main()
{
    auto content = postContent("http://httpbin.org/post", queryParams("name", "bob", "age", 101));
    auto json = parseJSON(content);
    writeln(json);
}

Обратите внимание: вызов getContent превратился в postContent, больше ничего не изменилось.

Multipart form POST — служит для передачи на сервер бОльших обьемов данных, в том числе для аплоада файлов через веб-форму.
Пример отправки файла и параметров через MultipartForm
import std.stdio;
import requests;

void main()
{
    MultipartForm form;
    form.add(formData("content", "example for MultipartForm"));
    form.add(formData("file", File("source/app.d", "rb"), ["filename":"app.d", "Content-Type": "text/plain"]));
    auto content = postContent("http://httpbin.org/post", form);
    writeln(content);
}


Мы собираем поля для отправки в форму с помощью вызова form.add(formData(описание поля)) где первым параметром для formData будет имя поля, а вторым — либо массив с отправляемыми данными, либо открытый на чтение файл. Третим параметром может быть ассоциативный массив с дополнительными данными (в том числе Content-type для отправляемой части).

Наконец, последним вариантом вызова postContent, является отправка данных минуя всякие формы. Здесь возможны два варианта. Первый — отправка однорангового массива данных (например ubyte[]). В этом случае мы в момент вызова должны знать длину массива, поскольку при передаче используется заголовок Content-Length. Я приведу здесь только строку вызова:

auto content = postContent("http://httpbin.org/post", 
                        "ABCDEFGH", 
                        "application/binary");


В случае если мы по какой-либо причине не знаем длину массива в момент вызова (но знаем что она конечна) можно использовать передачу массива размерности 2, в этом случае каждый последовательный кусочек массива будет передан как очередной chunk в Transfer-Encoding: chunked:
отправка файла используя Transfer-Encoding: chunked
import std.stdio;
import requests;

void main()
{
     auto f = File("source/app.d", "rb");
     auto content = postContent("http://httpbin.org/post", f.byChunk(5), "application/binary");
     writeln(content);
}


На этом заканчивается обзор самого простого использования dlang-requests — когда вам не нужна отладка, не нужнен стриминг получаемого ответа, не нужны никакие методы кроме GET и POST. Всё, что не рассмотрели в части 1, рассмотрим в части 2.

Ссылка на репозиторий проекта на github.

Удачи!
Поделиться с друзьями
-->

Комментарии (24)


  1. vintage
    25.06.2016 11:08

    void main()
    {
        auto content = getContent("http://httpbin.org/get");
        auto json = parseJSON(content);
        writeln(json);
    }

    Так круче:


    void main()
    {
        writeln( "http://httpbin.org/get".getContent.parseJSON );
    }

    queryParams("name", "bob", "age", 101)

    Как-то это криво, лучше бы использовали словари:


    queryParams([ "name" :  "bob" , "age" : "101" ])

    Я так понимаю кроме POST и GET другие методы не поддерживаются?


    1. ik62
      25.06.2016 11:35

      Сорри, забыл ответить на вторй вопрос.
      Поддерживаются любые типы запросов, не только POST и GET, об этом подробно — во второй части.


  1. ik62
    25.06.2016 11:28

    Словарь тоже можно использовать, так сработает:

    getContent("http://httpbin.org/get", ["a":"hello", "b":"world"]);
    

    Но есть ситуации, когда имя параметра повторяется:
    getContent("http://httpbin.org/get", queryParams("name[]", "hello", "name[]", "world"));
    

    И значения в словаре должны иметь один тип. Такое без предварительного преобразования в строку нельзя использовать:
    getContent("http://httpbin.org/get", ["name":"bob", "age":42]);
    


    1. vintage
      25.06.2016 12:49

      Вместо повторяющихся параметров, можно сделать массивы в качестве значений:


      "http://httpbin.org/get".getContent([ "name" : [ "hello", "world" ] , "age" : [ 42.to!string ]  ]);

      А что типы надо преобразовывать, так это нормально для языка со строгой типизацией.


      1. ik62
        25.06.2016 13:12

        В качестве аргумнта для одного из вариантов getContent() принимается словарь типа string[string] куда можно сложить всё что угодно. В другом варианте можно использовать queryParams(), куда тоже можно сложить что угодно, используя другой синтаксис.

        Оба метода имеют сви плюсы и минусы, пользователь может выбрать то, что ему в данном случае подходит больше.


      1. guai
        29.06.2016 11:20

        Нет однозначно правильного способа конвертить такие массивчики в параметры урла. PHP умеет их назад в массив собирать, но это не обязательно будет делать каждый сервер.


        1. vintage
          29.06.2016 19:12

          Тут преобразование вполне однозначное:


          "http://httpbin.org/get".getContent([ "name[]" : [ "hello", "world" ] , "age" : [ 42.to!string ]  ]);

          http://httpbin.org/get?name[]=hello&name[]=world&age=42


          1. guai
            29.06.2016 19:51

            Я не имел в виду, что преобразование неоднозначное, а то, что оно не стандартное. Кто мне запретит представлять массивчики так ?name=[hello,world]&age=42 или так ?name=(hello,world)&age=42, да мало ли вариантов. И затачивать код в либе под ПХПшный вариант не вижу смысла.


            1. vintage
              30.06.2016 02:05

              Представляйте, никто вам не мешает:


              "http://httpbin.org/get".getContent([
                  "name" : [ "[" ~ [ "hello", "world" ].joiner(",") ~ "]" ] , 
                  "age" : [ 42.to!string ]
               ]);

              "http://httpbin.org/get".getContent([
                  "name" : [ "(" ~ [ "hello", "world" ].joiner(",") ~ ")" ] ,
                  "age" : [ 42.to!string ]
              ]);

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


  1. mwizard
    25.06.2016 11:56
    -1

    У этого всего есть один большой недостаток — оно синхронное и блокирующее.


    1. ik62
      25.06.2016 12:24
      +1

      Мне, например, чаще нужен удобный синхронный клиент. Кому нужен асинхронный клиент спокойно использует vibe.d


      1. mwizard
        25.06.2016 12:30
        -1

        Проблема в том, что в vibe.d используются коллбэки, а не промисы/async/await, поэтому асинхронный код в D громоздкий. Может быть, в будущем примут патчи для введения async/await в качестве ключевых слов самого языка.


        1. ik62
          25.06.2016 12:43

          Согласен на 100%! Для создания удобного неблокирующего клиента действительно требуется поддержка на более низком уровне, пусть даже не на уровне ключквых слов, но хотя-бы на уровне стандартной библиотеки.


        1. vintage
          25.06.2016 13:09
          +1

          Не нужны там никакие async/await-ы используйте fibers&streams.


          1. mwizard
            25.06.2016 13:19

            Конечно не нужны! Ведь нужно всего-то создать новый класс, унаследовать его от Fiber-а, прописать ему call, потом создать экземпляр, и можно пользоваться! Правда, даже авторы D2 потом, видимо, поняли, что как-то чуточку громоздковато получается, и дали возможность передавать делегат для call параметром конструктора — и все равно это слишком много работы для того, что должно занимать один кейворд, да и к тому же в результате получаются semi-coroutine, как в Lua, а не полноценные awaitable.

            Если бы в D был механизм декораторов, то async/await можно было бы реализовать и самостоятельно, но, увы, UDA не дают возможности менять определение или тело функции.


            1. ik62
              25.06.2016 13:26

              Одних fibers недостаточно, нужен еще event-loop (желательно из std) для неблокирующего io.

              А о каких strams идёт речь?


              1. mwizard
                25.06.2016 13:52

                нужен еще event-loop
                У нас есть живой пример того, как можно сделать это элегантно — asyncio из Python.

                Или еще лучше — сделайте наконец-то AST Macros из DIP-50! Тогда async/await можно будет просто реализовать без участия комитета одобрения D — и не только его. stm, linq, шаблонизаторы в стиле jinja или jade, aop… Возможности безграничны, и все это без жертв производительности. Однозначно нужен DIP-50.


            1. vintage
              25.06.2016 17:08

              А в чём проблема завернуть создание и запуск волокна в функцию?


              unittest
              {
                  import core.time;
                  import std.range;
                  import jin.go;
              
                  __gshared static string[] log;
              
                  static void saying( string message )
                  {
                      foreach( _ ; 3.iota ) {
                          sleep( 100.msecs );
                          log ~= message;
                      }
                  }
              
                  go!saying( "hello" );
                  sleep( 50.msecs );
                  saying( "world" );
              
                  log.assertEq([ "hello" , "world" , "hello" , "world" , "hello" , "world" ]);
              }

              Что такое "semi-coroutine"? Почему вы считаете stackless сопрограммы, требующие костылей в виде async/await, единственно верными?


              1. ik62
                25.06.2016 17:56

                Я не смотрел еще эту библиотеку. Каждая go! выполняктся в отдельном thread или в отдельном Fiber? Во вторм случае — что произойдкт если несколько процедур зависнут в ожидании данных из сокеты?


                1. vintage
                  25.06.2016 18:06

                  Тогда рекомендую почитать эту статью: https://habrahabr.ru/post/280378/


                  go! запускает функцию в отдельном волокне.


                  Очевидно, эти процедуры никогда не проснутся и так и останутся висеть заблокированными. Если сработает таймаут, то он приведёт к пробуждению процедуры с возникновением в ней исключения.


                  1. ik62
                    25.06.2016 18:32

                    Спасибо за статью, прочитал!

                    Если в отдельном волокне, то это хорошо, но не очень экономно. Мне больше нравится подход Fiber + неблокированный io, который, по идее, берёт лучшее от двух миров — «синхронный» код + скорость неблокированного io.


                    1. vintage
                      25.06.2016 19:13

                      Ну так vibe.d как раз реализует неблокирующий io.


                      1. ik62
                        25.06.2016 19:45

                        Я в курсе, но вот пишут что http-client он как-то неудобно реализует.


                        1. vintage
                          26.06.2016 00:29

                          Вот уж не знаю, что там такого не удобного.


                          shared static this()
                          {
                              runWorkerTask({
                                  logInfo( "Request IP..." );
                                  logInfo( "IP: %s" , "http://httpbin.org/ip".requestHTTP.readJson["origin"] );
                              });
                          
                              runWorkerTask({
                                  logInfo( "Request UA..." );
                                  logInfo( "UA: %s" , "http://httpbin.org/user-agent".requestHTTP.readJson["user-agent"] );
                              });
                          
                          }

                          Request IP...
                          Request UA...
                          IP: "5.144.98.1"
                          UA: "vibe.d/0.7.28 (HTTPClient, +http://vibed.org/)"