Делаем Shrimp еще полезнее: добавляем перекодирование картинок в другие форматы



    С начала 2017-го года наша небольшая команда разрабатывает OpenSource-библиотеку RESTinio для встраивания HTTP-сервера в C++ приложения. К своему большому удивлению мы время от времени получаем вопросы из категории «А для чего может потребоваться встраиваемый HTTP-сервер на C++?» К сожалению, на простые вопросы отвечать сложнее всего. Иногда лучшим ответом является пример кода.

    Пару месяцев назад мы затеяли небольшой демо-проект Shrimp, который наглядно демонстрирует типичный сценарий, под который «затачивается» наша библиотека. Демо-проект представляет из себя простой Web-сервис, который получает запросы на масштабирование хранящихся на сервере картинок и который отдает в ответ картинку нужного пользователю размера.

    Этот демо-проект хорош тем, что в нем, во-первых, требуется интеграция с давным-давно написанным и исправно работающим кодом на C или C++ (в данном случае это ImageMagick). Поэтому должно быть понятно, почему имеет смысл встраивать HTTP-сервер в C++ приложение.

    И, во-вторых, в данном случае требуется асинхронная обработка запросов, дабы HTTP-сервер не блокировался пока выполняется масштабирование картинки (а это может занимать сотни миллисекунд или даже секунды). А разработку RESTinio мы затеяли именно потому, что не смогли найти вменяемый C++ный встраиваемый сервер, ориентированный именно на асинхронную обработку запросов.

    Работу на Shrimp-ом мы построили итеративным путем: сперва была сделана и описана самая простая версия, которая только масштабировала картинки. Затем мы устранили ряд недочетов первой версии и описали это во второй статье. Наконец дошли руки расширить функциональность Shrimp-а еще раз: добавилось преобразование картинок из одного формата в другой. О том, как это было сделано и пойдет речь в данной статье.

    Поддержка target-format


    Итак, в очередной версии Shrimp-а мы добавили возможность отдать отмасштабированную картинку в другом формате. Так, если выдать Shrimp-у запрос вида:

    curl "http://localhost:8080/my_picture.jpg?op=resize&max=1920"

    то Shrimp отдаст картинку в том же самом формате JPG, в котором была исходная картинка.

    Но если добавить в URL параметр target-format, то Shrimp преобразует картинку в указанный целевой формат. Например:

    curl "http://localhost:8080/my_picture.jpg?op=resize&max=1920&target-format=webp"

    В этом случае Shrimp отдаст картинку в формате webp.

    Обновленный Shrimp поддерживает пять форматов изображений: jpg, png, gif, webp и heic (так же известный как HEIF). Поэкспериментировать с различными форматами можно на специальной web-страничке:



    (на этой странице нет возможности выбрать формат heic, т.к. обычные десктопные браузеры данный формат по умолчанию не поддерживают).

    Для того, чтобы поддержать target-format в Shrimp-е потребовалось немного доработать код Shrimp-а (чему мы сами были удивлены, т.к. изменений действительно оказалось немного). Но зато пришлось пошаманить со сборкой ImageMagick-а, чему мы были удивлены еще больше, т.к. раньше нам с этой кухней сталкиваться, по счастливом стечению обстоятельств, не приходилось. Но давайте поговорим обо всем по порядку.

    ImageMagick должен понимать разные форматы


    ImageMagick для кодирования/декодирования изображений использует внешние библиотеки: libjpeg, libpng, libgif и т.д. Эти библиотеки должны быть установлены в системе до того, как ImageMagick будет сконфигурирован и собран.

    Тоже самое должно произойти и для того, чтобы ImageMagick поддерживал форматы webp и heic: сперва нужно собрать и установить libwebp и libheif, потом уже конфигурировать и устанавливать ImageMagick. И если с libwebp все просто, то вот вокруг libheif пришлось поплясать с бубном. Хотя по прошествии времени, после того, как все в итоге собралось и заработало, уже и непонятно: а почему же пришлось прибегать к бубну, вроде бы все тривиально? ;)

    В общем, если кто-то хочет подружить heic и ImageMagick, то придется установить:


    Именно в таком порядке (возможно, придется еще поставить nasm, дабы x265 работал с максимальной скоростью). После чего при выдаче команды ./configure ImageMagick сможет найти все, что ему нужно для поддержки .heic-файлов.

    Поддержка target-format в query string входящих запросов


    После того, как мы подружили ImageMagick с форматами webp и heic, пришло время модифицировать код Shrimp-а. Прежде всего, нам нужно научиться распознавать аргумент target-format во входящих HTTP-запросах.

    С точки зрения RESTinio это вообще не проблема. Ну появился в query string еще один аргумент, ну и что? А вот с точки зрения Shrimp-а ситуация оказалась несколько сложнее, поэтому усложнился и код функции, которая отвечала за разбор HTTP-запроса.

    Дело в том, что раньше нужно было различать всего две ситуации:

    • пришел запрос вида "/filename.ext" без каких-либо других параметров. Значит нужно просто отдать файл «filename.ext» как он есть;
    • пришел запрос вида "/filename.ext?op=resize&...". В этом случае нужно отмасштабировать картинку из файла «filename.ext».

    Но после добавления target-format нам нужно различать уже четыре ситуации:

    • пришел запрос вида "/filename.ext" без каких-либо других параметров. Значит нужно просто отдать файл «filename.ext» как он есть, без масштабирования и без перекодирования в другой формат;
    • пришел запрос вида "/filename.ext?target-format=fmt" без каких-либо других параметров. Значит взять изображение из файла «filename.ext» и перекодировать его в формат «fmt» с сохранением оригинальных размеров;
    • пришел запрос вида "/filename.ext?op=resize&..." но без target-format. В этом случае нужно отмасштабировать картинку из файла «filename.ext» и отдать ее в оригинальном формате;
    • пришел запрос вида "/filename.ext?op=resize&...&target-format=fmt". В этом случае нужно выполнить масштабирование, а затем перекодировать результат в формат «fmt».

    В итоге функция для определения параметров запроса приняла следующий вид:

    void
    add_transform_op_handler(
       const app_params_t & app_params,
       http_req_router_t & router,
       so_5::mbox_t req_handler_mbox )
    {
       router.http_get(
          R"(/:path(.*)\.:ext(.{3,4}))",
             restinio::path2regex::options_t{}.strict( true ),
             [req_handler_mbox, &app_params]( auto req, auto params )
             {
                if( has_illegal_path_components( req->header().path() ) )
                {
                   // Задан недопустимый путь к файлу.
                   return do_400_response( std::move( req ) );
                }
    
                // Разбираем параметры запроса.
                const auto qp = restinio::parse_query( req->header().query() );
                const auto target_format = qp.get_param( "target-format"sv );
    
                // Пытаемся определить в каком формате отдать итоговое
                // изображение. Если задан target-format, то именно этот
                // аргумент определяет формат. Если же target-format не
                // задан, то используется исходный формат, заданный
                // расширением файла с изображением.
                const auto image_format = try_detect_target_image_format(
                      params[ "ext" ],
                      target_format );
                if( !image_format )
                {
                   // Не смогли разобраться с форматом. Запрос не обслуживаем.
                   return do_400_response( std::move( req ) );
                }
    
                if( !qp.size() )
                {
                   // Нет никаких дополнительных аргументов, отдаем картинку как есть.
                   return serve_as_regular_file(
                         app_params.m_storage.m_root_dir,
                         std::move( req ),
                         *image_format );
                }
    
                const auto operation = qp.get_param( "op"sv );
                if( operation && "resize"sv != *operation )
                {
                   // Задана операция над изображением, но эта операция не resize.
                   return do_400_response( std::move( req ) );
                }
    
                if( !operation && !target_format )
                {
                   // В запросе должно быть либо op=resize,
                   // либо же target-format=something.
                   return do_400_response( std::move( req ) );
                }
    
                handle_resize_op_request(
                      req_handler_mbox,
                      *image_format,
                      qp,
                      std::move( req ) );
    
                return restinio::request_accepted();
       } );
    }

    В предыдущей версии Shrimp-а, где не нужно было перекодировать изображение, работа с параметрами запроса выглядела несколько проще.

    Очередь запросов и кэш картинок с учетом target-format


    Следующим моментом в реализации поддержки target-format стала работа к очередью ждущих запросов и кэшем готовых картинок в агенте a_transform_manager. Подробнее про эти штуки речь шла в предыдущей статье, но слегка напомним о чем речь.

    Когда приходит запрос на преобразование картинки, то может оказаться, что готовая картинка с такими параметрами уже есть в кэше. В этом случае ничего делать не нужно, достаточно отослать в ответ изображение из кэша. Если же картинку нужно преобразовывать, то может оказаться, что свободных воркеров на данный момент нет и нужно подождать, пока таковой появится. Для этого информацию о запросе нужно поставить в очередь ожидания. Но при этом нужно проверять уникальность запросов — если у нас ждут обработки три одинаковых запроса (т.е. нужно одним и тем же образом преобразовать одну и ту же картинку), то следует всего лишь один раз обработать картинку, а результат обработки отдать в ответ на эти три запроса. Т.е. в очереди ожидания одинаковые запросы нужно группировать.

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

    • во-первых, целевой формат изображения (т.е. исходная картинка может быть в jpg, а результирующая — в png);
    • во-вторых, тот факт, что масштабирование картинки может быть не нужно. Это происходит в ситуации, когда клиент заказывает только преобразование картинки из одного формата в другой, но с сохранением оригинального размера изображения.

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

    Зачем такое двойное кэширование могло бы понадобиться? Дело в том, что при трансформации картинок двумя самыми дорогими по времени операциями являются ресайзинг и сериализация картинки в целевой формат. Поэтому, если бы мы получили запрос на масштабирование картинки example.jpg до размера 1920 по ширине и трансформации ее в формат webp, то мы могли бы сохранить в памяти два изображения: example_1920px_width.jpg и example_1920px_width.webp. Картинку example_1920px_width.webp мы бы отдавали при получении повторного запроса. А вот картинка example_1920px_width.jpg могла бы использоваться при получении запросов на масштабирование example.jpg до размера 1920 по ширине и трансформации ее в формат heic. Мы бы могли пропустить операцию ресайзинга и сделать только преобразование формата (т.е. уже готовое изображение example_1920px_width.jpg перекодировалось бы в формат heic).

    Еще одна потенциальная возможность: когда приходит запрос на перекодирование изображение в другой формат без ресайзинга, то можно определить реальный размер картинки и использовать этот размер внутри составного ключа. Например, пусть example.jpg имеет размер 3000x2000 пикселей. Если мы следом получаем запрос на масштабирование example.jpg до 2000px по высоте, то мы можем сразу же определить, что картинка в таком размере у нас уже есть.

    В теории все эти рассуждения заслуживают внимания. Но вот с практической точки зрения непонятно, насколько высока вероятность такого развития событий. Т.е. как часто мы будем получать запрос на масштабирование example.jpg до 1920px с конвертацией в webp, а затем запрос на такое же масштабирование этой же картинки, но с конвертацией в png? Не имея реальной статистики сложно сказать. Поэтому мы решили не усложнять себе жизнь в своем демо-проекте, а пойти сперва по самому простому пути. С расчетом на то, что если кому-то потребуются более продвинутые схемы кэширования, то это можно будет добавить впоследствии, отталкиваясь от реальных, а не вымышленных сценариев использования Shrimp-а.

    В итоге в обновленной версии Shrimp-а мы слегка расширили ключ, добавив в него еще и такой параметр, как целевой формат:

    class resize_request_key_t
    {
       std::string m_path;
       image_format_t m_format;
       resize_params_t m_params;
    
    public:
       resize_request_key_t(
          std::string path,
          image_format_t format,
          resize_params_t params )
          :  m_path{ std::move(path) }
          ,  m_format{ format }
          ,  m_params{ params }
       {}
    
       [[nodiscard]] bool
       operator<(const resize_request_key_t & o ) const noexcept
       {
          return std::tie( m_path, m_format, m_params )
                < std::tie( o.m_path, o.m_format, o.m_params );
       }
    
       [[nodiscard]] const std::string &
       path() const noexcept
       {
          return m_path;
       }
    
       [[nodiscard]] image_format_t
       format() const noexcept
       {
          return m_format;
       }
    
       [[nodiscard]] resize_params_t
       params() const noexcept
       {
          return m_params;
       }
    };

    Т.е. запрос на ресайзинг example.jpg до размера 1920px с конвертацией в png отличается от такого же ресайзинга, но с конвертацией в webp или heic.

    Но главный фокус прячется в новой реализации класса resize_params_t, который и определяет новые размеры отмасштабированной картинки. Ранее этот класс поддерживал три варианта: задана только ширина, задана только высота или задана длинная сторона (высота или ширина выясняется по актуальному размеру картинки). Соответственно, метод resize_params_t::value() всегда возвращал какое-то реальное значение (что это за значение определялось методом resize_params_t::mode()).

    А вот в новом Shrimp-е добавился еще один режим — keep_original, который означает, что масштабирование не выполняется и картинка отдается в своем исходном размере. Для поддержки этого режима в resize_params_t пришлось внести некоторые изменения. Во-первых, теперь метод resize_params_t::make() определяет, используется ли режим keep_original (считается, что этот режим используется если не определен ни один из параметров width, height и max в query string входящего запроса). Это позволило не переписывать функцию handle_resize_op_request(), которая и толкает на исполнение запрос на масштабирование картинки.

    Во-вторых, метод resize_params_t::value() теперь можно вызывать не всегда, а только когда режим масштабирования отличается от keep_original.

    Но самое важное, что resize_params_t::operator<() продолжил работать так, как это и задумывалось.

    Благодаря всем этим изменениям в a_transform_manager и кэш отмасштабированных изображений, и очередь ждущих запросов, остались теми же самыми. Но зато сейчас в этих структурах данных хранится информация о разнообразных запросах. Так, ключ {«example.jpg», «jpg», keep_original} будет отличаться и от ключа {«example.jpg», «png», keep_original}, и от ключа {«example.jpg», «jpg», width=1920px}.

    Получилось, что пошаманив немного с определением таких простых структур данных, как resize_params_t и resize_params_key_t, мы избежали переделки более сложных структур, таких как кэш результирующих изображений и очередь ждущих запросов.

    Поддержка target-format в a_transformer


    Ну и финальный шаг в поддержке target-format — это расширение логики работы агента a_transformer таким образом, чтобы картинка, возможно уже отмасштабированная, затем преобразовывалась к целевому формату.

    Сделать это оказалось проще всего, достаточно было лишь расширить код метода a_transform_t::handle_resize_request():

    [[nodiscard]]
    a_transform_manager_t::resize_result_t::result_t
    a_transformer_t::handle_resize_request(
       const transform::resize_request_key_t & key )
    {
       try
       {
          m_logger->trace( "transformation started; request_key={}", key );
    
          auto image = load_image( key.path() );
    
          const auto resize_duration = measure_duration( [&]{
                // Реальный ресайзинг требуется только если режим
                // масштабирования отличается от keep_original.
                if( transform::resize_params_t::mode_t::keep_original !=
                      key.params().mode() )
                {
                   transform::resize(
                         key.params(),
                         total_pixel_count,
                         image );
                }
             } );
          m_logger->debug( "resize finished; request_key={}, time={}ms",
                key,
                std::chrono::duration_cast<std::chrono::milliseconds>(
                      resize_duration).count() );
    
          image.magick( magick_from_image_format( key.format() ) );
    
          datasizable_blob_shared_ptr_t blob;
          const auto serialize_duration = measure_duration( [&] {
                   blob = make_blob( image );
                } );
          m_logger->debug( "serialization finished; request_key={}, time={}ms",
                key,
                std::chrono::duration_cast<std::chrono::milliseconds>(
                      serialize_duration).count() );
    
          return a_transform_manager_t::successful_resize_t{
                std::move(blob),
                std::chrono::duration_cast<std::chrono::microseconds>(
                      resize_duration),
                std::chrono::duration_cast<std::chrono::microseconds>(
                      serialize_duration) };
       }
       catch( const std::exception & x )
       {
          return a_transform_manager_t::failed_resize_t{ x.what() };
       }
    }

    По сравнению с предыдущей версией здесь есть два принципиальных дополнения.

    Во-первых, вызов по-настоящему магического метода image.magick() после выполнения ресайзинга. Этот метод указывает ImageMagick-у результирующий формат изображения. При этом представление картинки в памяти не меняется — ImageMagick продолжает ее хранить так, как ему это удобно. Но зато заданное методом magick() значение будет учтено при последующем вызове Image::write().

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

    В остальном же агент a_transformer_t не претерпел никаких изменений.

    Распараллеливание работы ImageMagick-а


    По умолчанию ImageMagic собирается с поддержкой OpenMP. Т.е. есть возможность распараллелить операции над изображениями, которые выполняет сам ImageMagick. Управлять количеством рабочих потоков, которые задействует в этом случае ImageMagick, можно посредством переменной окружения MAGICK_THREAD_LIMIT.

    Например, на своей тестовой машине со значением MAGICK_THREAD_LIMIT=1 (т.е. без реального распараллеливания) я получаю следующие результаты:

    curl "http://localhost:8080/DSC08084.jpg?op=resize&max=2400" -v > /dev/null
    > GET /DSC08084.jpg?op=resize&max=2400 HTTP/1.1
    > Host: localhost:8080
    > User-Agent: curl/7.47.0
    > Accept: */*
    > 
    < HTTP/1.1 200 OK
    < Connection: keep-alive
    < Content-Length: 2043917
    < Server: Shrimp draft server
    < Date: Wed, 15 Aug 2018 11:51:24 GMT
    < Last-Modified: Wed, 15 Aug 2018 11:51:24 GMT
    < Access-Control-Allow-Origin: *
    < Access-Control-Expose-Headers: Shrimp-Processing-Time, Shrimp-Resize-Time, Shrimp-Encoding-Time, Shrimp-Image-Src
    < Content-Type: image/jpeg
    < Shrimp-Image-Src: transform
    < Shrimp-Processing-Time: 1323
    < Shrimp-Resize-Time: 1086.72
    < Shrimp-Encoding-Time: 236.276
    

    Время, затраченное на ресайзинг указывается в заголовке Shrimp-Resize-Time. В данном случае это 1086.72ms.

    А вот если на этой же машине задать MAGICK_THREAD_LIMIT=3 и запустить Shrimp, то получаем уже другие значения:

    curl "http://localhost:8080/DSC08084.jpg?op=resize&max=2400" -v > /dev/null
    > GET /DSC08084.jpg?op=resize&max=2400 HTTP/1.1
    > Host: localhost:8080
    > User-Agent: curl/7.47.0
    > Accept: */*
    > 
    < HTTP/1.1 200 OK
    < Connection: keep-alive
    < Content-Length: 2043917
    < Server: Shrimp draft server
    < Date: Wed, 15 Aug 2018 11:53:49 GMT
    < Last-Modified: Wed, 15 Aug 2018 11:53:49 GMT
    < Access-Control-Allow-Origin: *
    < Access-Control-Expose-Headers: Shrimp-Processing-Time, Shrimp-Resize-Time, Shrimp-Encoding-Time, Shrimp-Image-Src
    < Content-Type: image/jpeg
    < Shrimp-Image-Src: transform
    < Shrimp-Processing-Time: 779.901
    < Shrimp-Resize-Time: 558.246
    < Shrimp-Encoding-Time: 221.655

    Т.е. время ресайзинга сократилось до 558.25ms.

    Соответственно, раз уж ImageMagick предоставляет возможность распараллеливать вычисления, то можно этой возможностью пользоваться. Но при этом желательно иметь возможность управлять тем, сколько рабочих нитей под себя заберет Shrimp. В предшествующих версиях Shrimp-а воздействовать на то, сколько рабочих потоков создает Shrimp, было нельзя. А в обновленной версии Shrimp-а это можно делать. Либо через переменные среды, например:

    SHRIMP_IO_THREADS=1 \
    SHRIMP_WORKER_THREADS=3 \
    MAGICK_THREAD_LIMIT=4 \
    shrimp.app -p 8080 -i ...

    Либо через аргументы командной строки, например:

    MAGICK_THREAD_LIMIT=4 \
    shrimp.app -p 8080 -i ... --io-threads 1 --worker-threads 4

    Значения, заданные через командную строку имеют более высокий приоритет.

    Нужно подчеркнуть, что MAGICK_THREAD_LIMIT влияет только на те операции, которые ImageMagick выполняет сам. Например, ресайзинг выполняется ImageMagick-ом. А вот преобразования из одного формата в другой ImageMagick делегирует внешним библиотекам. И как распараллеливаются операции в этих внешних библиотеках — это отдельный вопрос, с которым мы не разбирались.

    Заключение


    Пожалуй, в этой версии Shrimp-а мы довели свой демо-проект до приемлемого состояния. Желающие посмотреть и поэкспериментировать могут найти исходные тексты Shrimp-а на BitBucket-е или GitHub-е. Там же можно найти и Dockerfile, чтобы собрать Shrimp для своих экспериментов.

    В общем-то, своих целей, которые мы ставили перед собой начиная этот демо-проект, мы достигли. Появился ряд идей для дальнейшего развития как RESTinio, так и SObjectizer-а, причем часть из них уже нашла свое воплощение. Поэтому будет ли развиваться Shrimp куда-то дальше полностью зависит от вопросов и пожеланий. Если таковые будут, то и Shrimp может расширяться. Если нет, то Shrimp так и останется демо-проектом и полигоном для экспериментов с новыми версиями RESTinio и SObjectizer-а.

    В заключение хочется выразить отдельную благодарность aensidhe за помощь и советы, без которых наши пляски с бубном были бы гораздо более длительными и печальными.
    Поделиться публикацией

    Комментарии 9

      0
      А чем вам boost::beast не угодил?
        +1
        Boost.Beast слишком низкоуровневый для того, чтобы делать что-то прикладное прямо на нем.

        Кроме того, когда мы начали делать RESTinio, Boost.Beast-а еще не было, был какой-то Beast, который еще даже не проходил ревью на включение в Boost. Серьезное внимание и известность Beast получил именно после того, как стал Boost.Beast-ом.
          0
            +1
            Интересно, гляну обязательно.
              0
              Если сможете оставить какой-нибудь фидбек, пусть даже абсолютно субъективным, то для нас это будет весьма полезно. Возможно, это поможет сделать RESTinio лучше в будущем. Ну или мы сами лучше поймем, где преимущества у Boost.Beast, а где у нас.
                +1
                Посмотрел ваше изделие, оно действительно гораздо высокоуровневее, чем бист.
                Код выглядит прилично, чист и аккуратен, даже понять можно, что делает, не особо сломав голову.
                Единственно, не отметил практически совсем использования R-value и семантики перемещений.

                К примеру, impl/header_helpers.hpp:
                ...
                
                //
                // create_header_string()
                //
                
                //! Creates a string for http response header.
                inline std::string
                create_header_string(
                	const http_response_header_t & h,
                	content_length_field_presence_t content_length_field_presence =
                		content_length_field_presence_t::add_content_length,
                	std::size_t buffer_size = 0 )
                {
                std::string result;
                ...
                
                for( const auto & f : h )
                	{
                		result += f.name();
                		result.append( header_field_sep, ct_string_len( header_field_sep ) );
                		result += f.value();
                		result.append( header_rn, ct_string_len( header_rn ) );
                	}
                	result.append( header_rn, ct_string_len( header_rn ) );
                
                	return result;
                }
                


                куча копирований, никакого перемещения, что, возможно, весьма ускорило бы работу в высоконагруженном приложении. А ведь это делается на каждый HTTP-ответ. Хотя работу по оптимизации проводили, это видно, предварительное резервирование для результирующей строки имеется, что говорит о высоком качестве проработки.

                Наверное, я придираюсь. Реально с вашей библиотекой проще. Молодцы!
                  +1
                  Дело в том что заголовок нужно иметь в виде непрерывной последовательности байт. Вариант отправки множества мелких кусочков не рассматриваем, т.к. scatter/gather io для коротких буферов работает не очень быстро. Вообще быстрее всего работает отправка одного длинного буфера, но в реальности это чаще всего и неудобно и не оптимально.

                  И выход такой, что хоть сами данные заголовка хранятся в виде string-ов, мы их всех копируем в результирующую строку, размер которой заранее резервируем. Это становится первым буфером в рамках заданного response.

                  В ситуациях, когда ответ (и его заголовок) создаются на одной нити, а RESTinio c ASIO крутится на другой, то от одной нити к другой в счет заголовка подйет всего один инстанс std::string (память под который выделялась на другой нити, ибо редко какой заголовок влезает в SSO) и для менеджеров памяти для которых освобождать память лучше на той же нити на которой она выделялась, это такая хорошая добавка.

                  А вообще, да, мувать мы любим и надеюсь умеем ;)
                    0
                    «Мувать» тоже увидел :)
                      0
                      ngrodzitski основной разработчик RESTinio, подавляющее большинство кода за его авторством, так что с ним технические детали обсуждать лучше, чем с кем бы то ни было.

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

        Самое читаемое