[Перевод] Создание RESTful HTTP-запросов на C++
В этой статье я покажу вам, как создавать 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: первый релиз.