Сокеты, C++ и boost asio

Пытаюсь разобраться с тем как работают сокеты. Использую библиотеку boost asio, и все никак не пойму до конца некоторых моментов:

Допустим, было успешно установлено TCP соединение, открыт сокет. Теперь я хочу "сделать запрос к серверу" и отправить некую порцию данных:

template<typename P>
void send_with_payload(Socket& socket, const P& payload)
{
    // Общий размер запроса
    const auto len = sizeof(Header) + sizeof(P);

    // Заголовок
    buffer_copy(buffer_->prepare(sizeof(Header)), boost::asio::buffer(&header_, sizeof(Header)));
    buffer_->commit(sizeof(Header));

    // Полезная нагрузка
    buffer_copy(buffer_->prepare(sizeof(P)), boost::asio::buffer(&payload, sizeof(P)));
    buffer_->commit(sizeof(P));

    // Писать в сокет (покуда не будет записано всё)
    boost::system::error_code error;
    write(socket, *buffer_, boost::asio::transfer_exactly(len), error);
    if(error.failed())
    {
        buffer_->consume(buffer_->size());
        throw std::runtime_error(error.message());
    }

    buffer_->consume(len);
}

И уже на этом моменте возникает вопрос:

Строка boost::asio::transfer_exactlyпо сути является условием прекращения. Это значит запись в сокет будет продолжаться покуда указанное кол-во байт не будет "записано". Но что вообще означает записано? Это значит на другой стороне это было прочитано? И покуда прочитано не было - поток блокируется? (именно это показывает практика, но я не уверен - корректное ли это поведение) Или же чтение на другой стороне вовсе не обязательна в этот же момент, и данные все равно отправятся, но будут находится "подвешенном состоянии" у сервера в сокете (порочесть их можно когда угодно, а клиент не ждет пока сервер их прочтет)? А может возможны оба варианта?

Второй момент (на стороне сервера). Насколько правильно "читать несколько раз", при том что запись была единожды? Например:

В начале читаем первую часть данных из сокета:

void receive(Socket& socket
    , const TypeBits& types
    , const Operation& op = Operation::O_UNKNOWN)
{
    // Читаем ТОЛЬКО заголовок (чтение прекращается когда получен его размер)
    boost::system::error_code error;
    read(socket, *buffer_, boost::asio::transfer_at_least(sizeof(Header)), error);

    auto* data = const_cast<void*>(buffer_->data().data());
    header_ = *static_cast<Header*>(data);

    buffer_->consume(sizeof(Header));
    header_received_ = true;
}

Второй раз читаем отсавшуюся часть (основываясь на полученных при первом чтении данных определяем размер оставшейся части)

template<typename P>
class RequestWithPayload final : public Request
{
public:
    explicit RequestWithPayload(Request* base)
    : base_(base)
    {}

    ~RequestWithPayload() override = default;

    void receive_payload(Socket& socket) override
    {
        // Читаем "полезную нагрузку" (чтение завершается когда получен размер полезной нагрузки)
        boost::system::error_code error;
        read(socket, *buffer_, boost::asio::transfer_exactly(sizeof(P)), error);
        
        auto* data = const_cast<void*>(buffer_->data().data());
        payload_ = *static_cast<P*>(data);
        buffer_->consume(sizeof(P));
    }

    const P& payload()
    {
        return payload_;
    }

protected:
    P payload_;
    Request* base_;
};

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

Буду рад развернутому ответу.

P.S. Благодяря комментариям нашел ошибку, из-за которой сервер повисал на втором чтении. Все дело было в transfer_at_least при чтении. Нужно было читать фиксированный и точный размер (transfer_exactly). Но в целом заданные вопросы все еще актуальны:

  • Когда поток блокируется - чтобы его разблокировать, обязательно ли участие второй стороны (чтение отправленных данных)?
  • Если да, то какое именно? (прочесть всё/прочесть что-то/попытаться прочесть)
  • Если нет - каков критерий разблокирования потока, в какой момент функция вернет управление?

Ответы (1 шт):

Автор решения: Serge3leo

Когда поток блокируется - чтобы его разблокировать, обязательно ли участие второй стороны (чтение отправленных данных)?

Строго говоря, не обязательно. А в некоторых случаях, получатель вообще ничего сделать не может, поскольку данные до его узла ещё не дошли (скажем, ограничитель трафика их поставил на небольшую паузу, или маршрутизаторы переключаются на резервную линию, а TCP ожидает повторную передачу пакета).

Если да, то какое именно? (прочесть всё/прочесть что-то/попытаться прочесть)

Не что-то прочесть, а прочесть достаточно много. Грубо говоря, если при посылке запроса поток был заблокирован на первом байте, то если вторая сторона прочитает данные достаточные только для заголовока, то Вы и не заметите, что, возможно, поток был разблокирован и снова заблокирован, а возможно, его и разблокировать не стали (зависит от реализации, плюс оговорки выше).

Если нет - каков критерий разблокирования потока, в какой момент функция вернет управление?

Если строго, то соответствующий вызов(ы) ОС (send()) успешно приняли все данные.

Это значит на другой стороне это было прочитано?

Нет. Для подтверждения другой стороной, что она что-то прочитала и/или поняла прочитанное, она должна прислать явное тому подтверждение.

Все дело было в transfer_at_least при чтении. Нужно было читать фиксированный и точный размер (transfer_exactly).

transfer_at_least() потенциально более эффективен, поскольку, в удачных случаях больших нагрузок, снижает количество вызовов ОС. Т.е. если ОС может сразу передать Вам в буфер не только заголовок, но вообще всё, то почему бы и нет? Только пользоваться этим надо правильно (может, ОС, что б дважны не вставать, 3,62 записи туда вернул). ?

→ Ссылка