Tarantool CE/EE Documentation portal logo
Помощь
Обновлена 15 сентября 2026 г. в 08:55

Подключение к Tarantool из C++

Чтобы упростить начало работы с коннектором Tarantool C++, воспользуемся примером приложения из репозитория коннектора. В этой статье мы разберём код приложения шаг за шагом и поясним, что делает каждая его часть.

В этом руководстве рассматриваются следующие основные темы:

Предварительные требования

Перед прохождением вводного руководство, выполните следующие шаги:

Шаг 1 - Клонирование исходного кода коннектора и проверка наличия Tarantool и стороннего ПО.

Шаг 2 - Запуск Tarantool и создание базы данных.

Шаг 3 - Настройка прав доступа.

Установка

Коннектор Tarantool C++ в настоящее время поддерживается только для Linux.

Сам коннектор представляет собой header-only библиотеку (то есть состоящую только из заголовочных файлов), поэтому не требует установки и сборки как таковых. Достаточно клонировать исходный код коннектора и встроить его в свой проект на C++.

Также убедитесь, что установлены Tarantool и другое необходимое программное обеспечение.

  1. Убедитесь, что установлено следующее стороннее ПО. Если каких-то компонентов не хватает, установите их:

    • Git, система контроля версий;
    • Утилита unzip;
    • Компилятор gcc, собранный с поддержкой стандарта C++17;
    • Инструменты cmake и make.
  2. Если в вашей ОС нет Tarantool, установите его одним из способов:

  3. Клонируйте репозиторий коннектора Tarantool C++.

    git clone git@github.com:tarantool/tntcxx.git

Запуск Tarantool и создание базы данных

Запустите Tarantool локально или в Docker и создайте спейс со следующей схемой и индексом:

box.cfg{listen = 3301}t = box.schema.space.create('t')t:format({         {name = 'id', type = 'unsigned'},         {name = 'a', type = 'string'},         {name = 'b', type = 'number'}         })t:create_index('primary', {         type = 'hash',         parts = {'id'}         })

Настройка прав доступа

Чтобы иметь возможность выполнять необходимые операции в Tarantool, нужно предоставить пользователю guest права на чтение и запись. Проще всего назначить пользователю роль super:

box.schema.user.grant('guest', 'super')

Подключение к Tarantool

Коннектор C++ состоит из трёх основных частей: буфера IO-zero-copy, кодировщика/декодировщика MessagePack и клиента, который обрабатывает запросы.

Чтобы настроить подключение к экземпляру Tarantool из приложения на C++, необходимо сделать следующее:

Встраивание коннектора

Встройте коннектор в своё приложение на C++, подключив основной заголовочный файл:

#include "../src/Client/Connector.hpp"

Создание объектов

Сначала нужно создать клиент коннектора. Он может асинхронно обслуживать множество соединений с экземплярами Tarantool. Чтобы создать экземпляр клиента, следует указать в качестве параметров шаблона конкретные реализации буфера и сетевого провайдера. Основной класс коннектора имеет следующую сигнатуру:

template<class BUFFER, class NetProvider = EpollNetProvider<BUFFER>>class Connector;

Буфер параметризуется аллокатором. Это означает, что пользователи могут выбрать, какой аллокатор будет выделять память под блоки буфера. Данные организованы в связный список блоков фиксированного размера, который задаётся как параметр шаблона буфера.

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

using Buf_t = tnt::Buffer<16 * 1024>;#include "../src/Client/LibevNetProvider.hpp"using Net_t = LibevNetProvider<Buf_t, DefaultStream>;
Connector<Buf_t, Net_t> client;

Чтобы использовать класс BUFFER, нужно также подключить заголовочный файл буфера:

#include "../src/Buffer/Buffer.hpp"

Одного клиента недостаточно для работы с экземплярами Tarantool — нужно также создать объекты соединения. Соединение тоже принимает буфер и сетевой провайдер в качестве параметров шаблона. Обратите внимание, что они должны совпадать с параметрами клиента:

Connection<Buf_t, Net_t> conn(client);

Подключение

Наш целевой экземпляр Tarantool принимает подключения на порту 3301 на localhost. Определим соответствующие переменные, а также переменную WAIT_TIMEOUT для времени ожидания подключения.

const char *address = "127.0.0.1";int port = 3301;int WAIT_TIMEOUT = 1000; // миллисекунды

Чтобы подключиться к экземпляру Tarantool, нужно вызвать метод Connector::connect() объекта клиента и передать три аргумента: экземпляр соединения, адрес и порт.

int rc = client.connect(conn, {.address = address,                   .service = std::to_string(port),                   /*.user = ...,*/                   /*.passwd = ...,*/                   /* .transport = STREAM_SSL, */});

Обработка ошибок

Реализация коннектора не использует исключения, поэтому мы полагаемся на коды возврата: в случае сбоя метод connect() возвращает rc < 0. Чтобы получить сообщение о последней ошибке, произошедшей при взаимодействии с экземпляром, можно вызвать метод Connection::getError().

if (rc != 0) {    //assert(conn.getError().saved_errno != 0);    std::cerr << conn.getError().msg << std::endl;    return -1;}

Чтобы сбросить соединение после ошибок, то есть очистить сообщение об ошибке и статус соединения, используется метод Connection::reset().

Работа с запросами

В этом разделе показано, как:

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

В нашем примере приложения на C++ выполняются следующие типы запросов:

  • ping;
  • replace;
  • select.

Каждый метод запроса возвращает идентификатор запроса — своего рода future. По этому идентификатору можно получить ответ, когда он будет готов. Запросы ставятся в очередь выходного буфера соединения до вызова метода Connector::wait().

Подготовка запросов

На этом шаге запросы кодируются в формате MessagePack и сохраняются в выходном буфере соединения. Они готовы к отправке, но само сетевое взаимодействие произойдёт позже.

Напомним, что для запросов, работающих с данными, используется спейс Tarantool t, созданный ранее, и этот спейс имеет следующий формат:

t:format({         {name = 'id', type = 'unsigned'},         {name = 'a', type = 'string'},         {name = 'b', type = 'number'}         })

ping

rid_t ping = conn.ping();

replace

Эквивалентно Lua-запросу <space_name>:replace(pk_value, "111", 1).

uint32_t space_id = 512;int pk_value = 666;std::tuple data = std::make_tuple(pk_value /* field 1*/, "111" /* field 2*/, 1.01 /* field 3*/);rid_t replace = conn.space[space_id].replace(data);

select

Эквивалентно Lua-запросу <space_name>.index[0]:select({pk_value}, {limit = 1}).

uint32_t index_id = 0;uint32_t limit = 1;uint32_t offset = 0;IteratorType iter = IteratorType::EQ;auto i = conn.space[space_id].index[index_id];rid_t select = i.select(std::make_tuple(pk_value), limit, offset, iter);

Отправка запросов

Чтобы отправить запросы на сторону сервера, вызовите метод client.wait().

client.wait(conn, ping, WAIT_TIMEOUT);

Метод wait() принимает в качестве параметров опрашиваемое соединение, идентификатор запроса и, если будет определено, время ожидания. Как только ответ на указанный запрос готов, wait() завершается. Метод также возвращает отрицательный код возврата при системных сбоях, например при разрыве соединения или истечении времени ожидания. Если wait() возвращает 0, то ответ получен и его можно разобрать.

Теперь отправим наши запросы экземпляру Tarantool. Функция futureIsReady() проверяет доступность future и возвращает true или false.

while (! conn.futureIsReady(ping)) {    /*     * wait() — основная функция, отвечающая за отправку и получение     * запросов и реализующая цикл обработки событий. Она может     * завершиться ошибкой по нескольким причинам:     *  - истекло время ожидания соединения;     *  - соединение разорвано (например, закрыто);     *  - ошибка epoll.     */    if (client.wait(conn, ping, WAIT_TIMEOUT) != 0) {        std::cerr << conn.getError().msg << std::endl;        conn.reset();    }}

Получение ответов

Чтобы получить ответ, когда он готов, используйте метод Connection::getResponse(). Он принимает идентификатор запроса и возвращает опциональный объект с ответом. Если ответ ещё не готов, метод возвращает std::nullopt. Обратите внимание, что для каждого future метод getResponse() можно вызвать только один раз: он удаляет идентификатор запроса из внутренней таблицы после возврата.

Ответ состоит из заголовка и тела (response.header и response.body). В зависимости от успешности выполнения запроса на стороне сервера тело может содержать либо ошибки времени выполнения, доступные через response.body.error_stack, либо данные (кортежи) — response.body.data. В свою очередь, данные — это вектор кортежей. Однако кортежи не декодированы и приходят в виде указателей на начало и конец MessagePack-данных. О том, как декодировать кортежи, см. раздел Декодирование и чтение данных.

При работе с одним соединением есть два варианта получения ответов: можно ждать один конкретный future либо все сразу. В нашем примере опробуем оба варианта. Для запроса ping воспользуемся первым вариантом.

std::optional<Response<Buf_t>> response = conn.getResponse(ping);/* * Поскольку conn.futureIsReady(ping) вернул <true>, ответ * должен быть готов. */assert(response != std::nullopt);/* * Если запрос успешно выполнен на стороне сервера, ответ * будет содержать данные (то есть кортеж, заменяемый в случае запроса :replace(), * или кортежи, удовлетворяющие условиям поиска в случае :select(); * ответы на ping не содержат ничего — пустую карту). * Чтобы отличить ответы с данными от ответов с ошибками, можно * опереться на код ответа из заголовка или проверить * члены Response->body.data и Response->body.error_stack. */printResponse<Buf_t>(*response);

Для запросов replace и select рассмотрим вариант с ожиданием обоих future сразу.

/* Дождёмся обоих future сразу. */std::vector<rid_t> futures(2);futures[0] = replace;futures[1] = select;/* Если время ожидания не задано, мы опрашиваем future до готовности. */client.waitAll(conn, futures);for (size_t i = 0; i < futures.size(); ++i) {    assert(conn.futureIsReady(futures[i]));    response = conn.getResponse(futures[i]);    assert(response != std::nullopt);    printResponse<Buf_t>(*response);}

Несколько соединений одновременно

Теперь рассмотрим случай, когда одновременно устанавливаются два соединения с экземпляром Tarantool.

/* Создадим ещё одно соединение. */Connection<Buf_t, Net_t> another(client);if (client.connect(another, {.address = address,                 .service = std::to_string(port),                 /* .transport = STREAM_SSL, */}) != 0) {    std::cerr << conn.getError().msg << std::endl;    return -1;}/* Одновременно выполним два запроса из разных соединений. */rid_t f1 = conn.ping();rid_t f2 = another.ping();/* * waitAny() возвращает первое соединение, получившее ответ. * Участвуют все соединения, зарегистрированные через вызов :connect(). */std::optional<Connection<Buf_t, Net_t>> conn_opt = client.waitAny(WAIT_TIMEOUT);Connection<Buf_t, Net_t> first = *conn_opt;if (first == conn) {    assert(conn.futureIsReady(f1));    (void) f1;} else {    assert(another.futureIsReady(f2));    (void) f2;}

Закрытие соединений

Наконец, за закрытие соединений отвечает пользователь.

client.close(conn);client.close(another);

Сборка и запуск приложения на C++

Теперь соберём наш пример приложения на C++ и запустим его, чтобы подключиться к экземпляру Tarantool и выполнить все заданные запросы.

Убедитесь, что вы находитесь в корневом каталоге клонированного репозитория коннектора C++. Чтобы собрать пример приложения:

cd examplescmake .make

Убедитесь, что запущенный ранее сеанс Tarantool работает. Запустите приложение:

./Simple

Как видно из журнала выполнения, все соединения с Tarantool, заданные в нашем приложении, установлены, и все запросы выполнены успешно.

Декодирование и чтение данных

Ответы от экземпляра Tarantool содержат сырые данные, то есть данные, закодированные в кортежи MessagePack. Чтобы декодировать данные клиента, пользователь должен написать собственные декодировщики (readers) на основе схемы базы данных и подключить их в своём приложении:

#include "Reader.hpp"

Чтобы показать логику декодирования ответа, воспользуемся декодировщиком из нашего примера.

Сначала определяется структура, соответствующая формату спейса из примера:

/** * Соответствует кортежам, хранящимся в спейсе пользователя: * box.execute("CREATE TABLE t (id UNSIGNED PRIMARY KEY, a TEXT, d DOUBLE);") */struct UserTuple {    uint64_t field1;    std::string field2;    double field3;    static constexpr auto mpp = std::make_tuple(        &UserTuple::field1, &UserTuple::field2, &UserTuple::field3);};

Прототип базового декодировщика

Прототип базового декодировщика приведён в src/mpp/Dec.hpp:

template <class BUFFER, Type TYPE>struct SimpleReaderBase : DefaultErrorHandler {   using BufferIterator_t = typename BUFFER::iterator;   /* Допустимый тип разбираемых значений. */   static constexpr Type VALID_TYPES = TYPE;   BufferIterator_t* StoreEndIterator() { return nullptr; }};

Каждый новый декодировщик должен наследоваться от него или напрямую от DefaultErrorHandler.

Разбор значений

Чтобы разобрать конкретное значение, нужно определить метод Value(). Первые два аргумента метода являются общими и, как правило, не используются, а третий задаёт разбираемое значение. В случае структур POD (Plain Old Data) достаточно побайтового копирования. Поскольку в нашей схеме есть поля трёх разных типов, определим соответствующие функции Value():

#pragma once/* * Copyright 2010-2020, Tarantool AUTHORS, please see AUTHORS file. * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following * conditions are met: * * 1. Redistributions of source code must retain the above *    copyright notice, this list of conditions and the *    following disclaimer. * * 2. Redistributions in binary form must reproduce the above *    copyright notice, this list of conditions and the following *    disclaimer in the documentation and/or other materials *    provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */#include "../src/Buffer/Buffer.hpp"#include "../src/mpp/Dec.hpp"/** * Соответствует кортежам, хранящимся в спейсе пользователя: * box.execute("CREATE TABLE t (id UNSIGNED PRIMARY KEY, a TEXT, d DOUBLE);") */struct UserTuple {    uint64_t field1;    std::string field2;    double field3;    static constexpr auto mpp = std::make_tuple(        &UserTuple::field1, &UserTuple::field2, &UserTuple::field3);};std::ostream&operator<<(std::ostream& strm, const UserTuple &t){    return strm << "Tuple: field1=" << t.field1 << " field2=" << t.field2 <<               " field3=" << t.field3;}

Разбор массива

Важно также понимать, что сам кортеж обёрнут в массив, поэтому фактически сначала нужно разобрать массив. Определим для этого ещё один декодировщик.

#pragma once/* * Copyright 2010-2020, Tarantool AUTHORS, please see AUTHORS file. * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following * conditions are met: * * 1. Redistributions of source code must retain the above *    copyright notice, this list of conditions and the *    following disclaimer. * * 2. Redistributions in binary form must reproduce the above *    copyright notice, this list of conditions and the following *    disclaimer in the documentation and/or other materials *    provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */#include "../src/Buffer/Buffer.hpp"#include "../src/mpp/Dec.hpp"/** * Соответствует кортежам, хранящимся в спейсе пользователя: * box.execute("CREATE TABLE t (id UNSIGNED PRIMARY KEY, a TEXT, d DOUBLE);") */struct UserTuple {    uint64_t field1;    std::string field2;    double field3;    static constexpr auto mpp = std::make_tuple(        &UserTuple::field1, &UserTuple::field2, &UserTuple::field3);};std::ostream&operator<<(std::ostream& strm, const UserTuple &t){    return strm << "Tuple: field1=" << t.field1 << " field2=" << t.field2 <<               " field3=" << t.field3;}

Установка декодировщика

Метод SetReader() задаёт декодировщик, который вызывается при разборе каждого элемента массива. Чтобы два определённых выше декодировщика заработали, нужно создать декодировщик, установить его итератор на позицию закодированного кортежа и вызвать метод Read() (блок кода ниже взят из примера приложения).

/* * Copyright 2010-2020, Tarantool AUTHORS, please see AUTHORS file. * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following * conditions are met: * * 1. Redistributions of source code must retain the above *    copyright notice, this list of conditions and the *    following disclaimer. * * 2. Redistributions in binary form must reproduce the above *    copyright notice, this list of conditions and the following *    disclaimer in the documentation and/or other materials *    provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. *//** * О сборке примера см. CMakeLists.txt или Makefile в текущем каталоге. * Предварительные требования для запуска теста: * 1. Запустите экземпляр Tarantool на localhost и задайте порт 3301 для приёма подключений; * 2. Создайте спейс с id = 512 и полями формата {integer, string, float} * 3. Предоставьте пользователю 'guest' права на чтение и запись (или просто box.schema.user.grant('guest', 'super')) * 4. Скомпилируйте и запустите ./Simple */#include "../src/Client/Connector.hpp"#include "../src/Buffer/Buffer.hpp"#include "Reader.hpp"const char *address = "127.0.0.1";int port = 3301;int WAIT_TIMEOUT = 1000; // миллисекундыusing Buf_t = tnt::Buffer<16 * 1024>;#include "../src/Client/LibevNetProvider.hpp"using Net_t = LibevNetProvider<Buf_t, DefaultStream>;template <class BUFFER>std::vector<UserTuple>decodeUserTuple(Data<BUFFER> &data){    std::vector<UserTuple> results;    bool ok = data.decode(results);    (void)ok;    assert(ok);    return results;}template<class BUFFER>voidprintResponse(Response<BUFFER> &response){    if (response.body.error_stack != std::nullopt) {        Error err = (*response.body.error_stack)[0];        std::cout << "RESPONSE ERROR: msg=" << err.msg <<              " line=" << err.file << " file=" << err.file <<              " errno=" << err.saved_errno <<              " type=" << err.type_name <<              " code=" << err.errcode << std::endl;    }    if (response.body.data != std::nullopt) {        Data<BUFFER>& data = *response.body.data;        std::vector<UserTuple> tuples = decodeUserTuple(data);        if (tuples.empty()) {            std::cout << "Empty result" << std::endl;            return;        }        for (auto const& t : tuples) {            std::cout << t << std::endl;        }    }}intmain(){    /*     * Создаём коннектор по умолчанию — он будет обрабатывать множество соединений     * асинхронно.     */    Connector<Buf_t, Net_t> client;    /*     * Создаём одно соединение. Конструктор принимает только ссылку на клиент.     */    Connection<Buf_t, Net_t> conn(client);    /*     * Пробуем подключиться к указанному адресу:порту. Текущая реализация     * не использует исключения, поэтому полагаемся только на коды возврата.     */    int rc = client.connect(conn, {.address = address,                       .service = std::to_string(port),                       /*.user = ...,*/                       /*.passwd = ...,*/                       /* .transport = STREAM_SSL, */});    if (rc != 0) {        //assert(conn.getError().saved_errno != 0);        std::cerr << conn.getError().msg << std::endl;        return -1;    }    /*     * Теперь выполним несколько запросов: ping, replace и select.     * Обратите внимание, что любой из методов :request() не может завершиться ошибкой; он всегда     * возвращает идентификатор запроса — future (число), который используется для получения     * ответа после его получения. Также обратите внимание, что на этом шаге     * запросы кодируются (в формат MessagePack) и сохраняются в     * выходном буфере соединения — они готовы к отправке.     * Но само сетевое взаимодействие произойдёт позже.     */    /* PING */    rid_t ping = conn.ping();    /* REPLACE — эквивалентно space:replace(pk_value, "111", 1) */    uint32_t space_id = 512;    int pk_value = 666;    std::tuple data = std::make_tuple(pk_value /* field 1*/, "111" /* field 2*/, 1.01 /* field 3*/);    rid_t replace = conn.space[space_id].replace(data);    /* SELECT — эквивалентно space.index[0]:select({pk_value}, {limit = 1}) */    uint32_t index_id = 0;    uint32_t limit = 1;    uint32_t offset = 0;    IteratorType iter = IteratorType::EQ;    auto i = conn.space[space_id].index[index_id];    rid_t select = i.select(std::make_tuple(pk_value), limit, offset, iter);    /*     * Теперь отправим наши запросы на сервер. Для одного соединения есть два варианта:     * можно ждать один конкретный future     * или все сразу. Опробуем оба варианта.     */    while (! conn.futureIsReady(ping)) {        /*         * wait() — основная функция, отвечающая за отправку и получение         * запросов и реализующая цикл обработки событий. Она может         * завершиться ошибкой по нескольким причинам:         *  - истекло время ожидания соединения;         *  - соединение разорвано (например, закрыто);         *  - ошибка epoll.         */        if (client.wait(conn, ping, WAIT_TIMEOUT) != 0) {            std::cerr << conn.getError().msg << std::endl;            conn.reset();        }    }    /* Теперь получим ответ с помощью future. */    std::optional<Response<Buf_t>> response = conn.getResponse(ping);    /*     * Поскольку conn.futureIsReady(ping) вернул <true>, ответ     * должен быть готов.     */    assert(response != std::nullopt);    /*     * Если запрос успешно выполнен на стороне сервера, ответ     * будет содержать данные (то есть кортеж, заменяемый в случае запроса :replace(),     * или кортежи, удовлетворяющие условиям поиска в случае :select();     * ответы на ping не содержат ничего — пустую карту).     * Чтобы отличить ответы с данными от ответов с ошибками, можно     * опереться на код ответа из заголовка или проверить     * члены Response->body.data и Response->body.error_stack.     */    printResponse<Buf_t>(*response);    /* Дождёмся обоих future сразу. */    std::vector<rid_t> futures(2);    futures[0] = replace;    futures[1] = select;    /* Если время ожидания не задано, мы опрашиваем future до готовности. */    client.waitAll(conn, futures);    for (size_t i = 0; i < futures.size(); ++i) {        assert(conn.futureIsReady(futures[i]));        response = conn.getResponse(futures[i]);        assert(response != std::nullopt);        printResponse<Buf_t>(*response);    }    /* Создадим ещё одно соединение. */    Connection<Buf_t, Net_t> another(client);    if (client.connect(another, {.address = address,                     .service = std::to_string(port),                     /* .transport = STREAM_SSL, */}) != 0) {        std::cerr << conn.getError().msg << std::endl;        return -1;    }    /* Одновременно выполним два запроса из разных соединений. */    rid_t f1 = conn.ping();    rid_t f2 = another.ping();    /*     * waitAny() возвращает первое соединение, получившее ответ.     * Участвуют все соединения, зарегистрированные через вызов :connect().     */    std::optional<Connection<Buf_t, Net_t>> conn_opt = client.waitAny(WAIT_TIMEOUT);    Connection<Buf_t, Net_t> first = *conn_opt;    if (first == conn) {        assert(conn.futureIsReady(f1));        (void) f1;    } else {        assert(another.futureIsReady(f2));        (void) f2;    }    /* Наконец, за закрытие соединений отвечает пользователь. */    client.close(conn);    client.close(another);    return 0;}