[Перевод] Создание RESTful HTTP-запросов на C++

yubzckm2rlvjmokxkajfae3vxye.png


В этой статье я покажу вам, как создавать HTTP-запросы к REST-серверу с помощью библиотеки C++ Request, написанной Ху Нгуйеном. При её написании мистер Нгуен во многом ориентировался на принципы проектирования из Python Requests, поэтому для тех, кто использовал или знаком с Python Requests, C++ Requests окажется вполне понятна.

Для демонстрации клиентского кода нам потребуется веб-сервер, к которому можно будет отправлять запрос, так что в данном случае мы реализуем CRUD API (Create, Read, Update, Delete) с помощью ASP.NET Web API 2. Сам сервер не представляет основную идею статьи, но структуру веб-API я всё равно поясню. Если же вас код сервера не интересует (вы не используете ASP.NET), тогда можете сразу переходить к разделу клиента.

▍ ASP.NET Web API


Я не стану подробно расписывать настройку проекта веб-API на ASP.NET. Заинтересованный читатель может обратиться к этому и этому руководствам от Microsoft.

Данный веб-API построен по схеме МVC (модель-представление-контроллер). Модель отвечает за данные, обычно это классы, которые определяют расположение данных в хранилище. Представление отвечает за, собственно, представление этих данных пользователю, а контроллер занимается бизнес-логикой.

Строго говоря, чистый веб-API сервер ASP.NET не предоставляет HTML-страницы, а значит, слоя представления не имеет. В нашем примере в качестве модели данных выступает класс Product.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Qty { get; set; }
    public decimal Price { get; set; }
}


В ProductsController класс Product хранится в непостоянном static Dictionary, то есть после выключения сервера API данные будут утрачиваться. Но такого решения будет достаточно для нашей демонстрации без участия БД.

public class ProductsController : ApiController
{
    static Dictionary products = new Dictionary()


Далее идёт метод Create, отвечающий за создание объекта Product для сохранения в словаре. Атрибут [FromBody] означает, что элемент Product будет заполнен содержимым из тела запроса.

[HttpPost]
public IHttpActionResult Create([FromBody] Product item)
{
    if (item == null)
    {
        return BadRequest();
    }

    products[item.Id] = item;

    return Ok();
}


Для проверки кода я использую команду curl. Если у вас установлен Postman, то можете использовать его. Я же старомодный парень, поэтому предпочитаю непосредственно curl.

curl -XPOST http://localhost:51654/api/products/create
-H 'Content-Type: application/json' -d'{"Id":1, "Name":"ElectricFan","Qty":14,"Price":20.90}'


  • -X указывает HTTP-глагол POST, соответствующий методу create или update;
  • вторым аргументом идёт URL, по которому должен отправиться запрос;
  • -H указывает HTTP-заголовки. Мы отправляем строку JSON, значит, устанавливаем 'Content-Type' на 'application/json';
  • -d указывает тело запроса. Здесь ясно видно, что ключи словаря JSON в точности соответствуют членам Product.


При успешном запросе POST возвращаемый curl вывод будет пуст. Чтобы увидеть созданный Product, нам потребуется метод извлечения, который вкратце описывается ниже.

Примечание: для извлечения используется HTTP-глагол GET.
 [HttpGet]
public List GetAll()
{
    List temp = new List();
    foreach(var item in products)
    {
        temp.Add(item.Value);
    }
    return temp;
}

[HttpGet]
public IHttpActionResult GetProduct(long id)
{
    try
    {
        return Ok(products[id]);
    }
    catch (System.Collections.Generic.KeyNotFoundException)
    {
        return NotFound();
    }
}


Соответствующие команды curl извлекают все либо один Product на основе переданного id (равен 1). Аргументы командной строки аналогичны описанным выше, так что их я пропущу.

curl -XGET http://localhost:51654/api/products'

curl -XGET http://localhost:51654/api/products/1'


Вывод:

 [{"Id":1,"Name":"ElectricFan","Qty":14,"Price":20.90}]

{"Id":1,"Name":"ElectricFan","Qty":14,"Price":20.90}


Первый результат заключён в [], потому что первая команда возвращает коллекцию объектов Product, но в нашем случае пока в ней всего один товар.

В завершение нужно обновить методы Update и Delete. Разница между HTTP-глаголами POST и PUT в том, что PUT выполняет исключительно обновление, а POST создаёт объект в случае его отсутствия, но при этом также может использоваться и для обновления.

 [HttpPut]
public IHttpActionResult Update(long id, [FromBody] Product item)
{
    if (item == null || item.Id != id)
    {
        return BadRequest();
    }

    if(products.ContainsKey(id)==false)
    {
        return NotFound();
    }
    var product = products[id];

    product.Name = item.Name;
    product.Qty = item.Qty;
    product.Price = item.Price;

    return Ok();
}
[HttpDelete]
public IHttpActionResult Delete(long id)
{
    var product = products[id];
    if (product == null)
    {
        return NotFound();
    }

    products.Remove(id);

    return Ok();
}


Соответствующие команды curl обновляют и удаляют Product с id=1.

curl -XPUT http://localhost:51654/api/products/1
-H 'Content-Type: application/json' -d'{"Id":1, "Name":"ElectricFan","Qty":15,"Price":29.80}'

curl -XDELETE http://localhost:51654/api/products/1


Убедиться, что Product действительно был обновлён или удалён, можно с помощью приведённой выше команды curl для извлечения.

▍ Клиентский код C++


Наконец, мы добрались до основной сути статьи! Для того чтобы использовать C++ Request, вам нужно скопировать или скачать этот пакет отсюда и включить его заголовок cpr. В качестве альтернативы пользователи Visual C++ могут установить C++ Request через vcpkg. В vcpkg эта библиотека обозначена аббревиатурой cpr.

.\vcpkg install cpr
#include 


Для отправки запроса POST на создание Product мы помещаем его в cpr::Body в виде строкового литерала в формате JSON. Если не использовать строковый литерал, то придётся экранировать в строке JSON все двойные кавычки.

auto r = cpr::Post(cpr::Url{ "http://localhost:51654/api/products/create" },
    cpr::Body{ R"({"Id":1, "Name":"ElectricFan","Qty":14,"Price":20.90})" },
    cpr::Header{ { "Content-Type", "application/json" } });


Сравнивая этот C++ код с командой curl, мы видим, какая и куда идёт информация.

curl -XPOST http://localhost:51654/api/products/create
-H 'Content-Type: application/json' -d'{"Id":1, "Name":"ElectricFan","Qty":14,"Price":20.90}'


После создания товара мы пробуем его извлечь:

auto r = cpr::Get(cpr::Url{ "http://localhost:51654/api/products/1" });


Вывод будет такой же, как после команды curl, что вполне логично, поскольку в C++ Request для этого используется libcurl.

{"Id":1,"Name":"ElectricFan","Qty":14,"Price":20.90}


Весь код C++ для выполнения CRUD с помощью веб-API показан ниже вместе c выводом. Прежде чем его выполнять, убедитесь в работоспособности вашего веб-API.

int main()
{
    {
        std::cout << "Action: Create Product with Id = 1" << std::endl;
        auto r = cpr::Post(cpr::Url{ "http://localhost:51654/api/products/create" },
            cpr::Body{ R"({"Id":1,
            "Name":"ElectricFan","Qty":14,"Price":20.90})" },
            cpr::Header{ { "Content-Type", "application/json" } });
        std::cout << "Returned Status:" << r.status_code << std::endl;
    }
    {
        std::cout << "Action: Retrieve the product with id = 1" << std::endl;
        auto r = cpr::Get(cpr::Url{ "http://localhost:51654/api/products/1" });
        std::cout << "Returned Text:" << r.text << std::endl;
    }
    {
        std::cout << "Action: Update Product with Id = 1" << std::endl;
        auto r = cpr::Post(cpr::Url{ "http://localhost:51654/api/products/1" },
            cpr::Body{ R"({"Id":1,
            "Name":"ElectricFan","Qty":15,"Price":29.80})" },
            cpr::Header{ { "Content-Type", "application/json" } });
        std::cout << "Returned Status:" << r.status_code << std::endl;
    }
    {
        std::cout << "Action: Retrieve all products" << std::endl;
        auto r = cpr::Get(cpr::Url{ "http://localhost:51654/api/products" });
        std::cout << "Returned Text:" << r.text << std::endl;
    }
    {
        std::cout << "Action: Delete the product with id = 1" << std::endl;
        auto r = cpr::Delete(cpr::Url{ "http://localhost:51654/api/products/1" });
        std::cout << "Returned Status:" << r.status_code << std::endl;
    }
    {
        std::cout << "Action: Retrieve all products" << std::endl;
        auto r = cpr::Get(cpr::Url{ "http://localhost:51654/api/products" });
        std::cout << "Returned Text:" << r.text << std::endl;
    }

    return 0;
}


Ниже, как и говорилось, показан вывод. Возвращаемый текст отображается, только когда операция CRUD его подразумевает, в противном случае показывается только статус. Например, операции Create/Update/Delete не возвращают никакой текст, поэтому отображается только их статус. В данном случае мы видим код 200, который означает успех HTTP-запроса.

Action: Create Product with Id = 1
Returned Status:200

Action: Retrieve the product with id = 1
Returned Text:{"Id":1,"Name":"ElectricFan","Qty":14,"Price":20.90}

Action: Update Product with Id = 1
Returned Status:200

Action: Retrieve all products
Returned Text:[{"Id":1,"Name":"ElectricFan","Qty":15,"Price":29.80}]

Action: Delete the product with id = 1
Returned Status:200

Action: Retrieve all products
Returned Text:[]


Если вам нужно отправлять запросы с параметрами, как показано ниже, можете использовать cpr::Parameters.

http://www.example.com/products?quota=500&sold=true


Код C++ для вышеприведённого примера с URL:

auto r = cpr::Get(cpr::Url{ "http://www.example.com/products" },
    cpr::Parameters{{"quota", "500"}, {"sold", "true"}});


Исходный код для этой статьи доступен на GitHub и в архиве cpprestexample-1.0.1.zip.

▍ История


  • 28 июня, 2022: исправлена уязвимость Newtonsoft.Json в проекте RestWebApp.
  • 17 мая, 2018: первый релиз.


oug5kh6sjydt9llengsiebnp40w.png

© Habrahabr.ru