Boost::asio - ошибка использования сокета

Помогите разобраться, в асинхронном сервере с использованием библиотеки boost.

Имеем:

  1. win 10 x64;
  2. boost 1.88;
  3. компилятор MSYS2 MINGW64;

Что непонятно:

  1. зачем повсеместно используют "shared_ptr" ? пишут о продлении жизни класса\объекта до окончания отработки кода в функции обратного вызова. Так ведь я создал объект класса "Сервер" и он и так будет жить пока выполняется код в теле "main"... к тому же в классе "Сервер" в одном из методов запускается какая-то магия "io_context.run()", которая вызывает(?)\запускает(?) код выполнения функций обратного вызова. То же относится и к "enable_shared_from_this", то же не ясно.
  2. При запуске кода проги(приложен вконце) появляется ошибка: "Исключение: bind: Обычно разрешается только одно использование адреса сокета (протокол/сетевой адрес/порт) [system:10048 at C:\boost_1_88_0\include\boost-1_88/boost/asio/detail/win_iocp_socket_service.hpp:243 in function 'bind']" Не пойму где я еще в коде использую сокет...

Что делал:

  1. Читал учебники(большая часть старых версий) и статьи;
  2. Асинхронный клиент-сервер разобрался как написать;
  3. Начитался, решил написать код без "shared_ptr" и лямбда функций;
  4. Написал что-то не то, т.к. появляется ошибка(см. выше) изз недопонимания.

Код:

#include <iostream>
#include <string>
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>

#include <thread>

class Server {
    public:
        boost::asio::ip::tcp::endpoint m_endpoint;
        boost::asio::io_context m_ioContext;
        boost::asio::ip::tcp::acceptor m_acceptor;
        boost::asio::ip::tcp::socket m_socket;
        boost::asio::streambuf m_streambuf;

    public:
        Server (boost::asio::ip::tcp::endpoint aEndpoint)
                : m_endpoint (aEndpoint),
                    m_acceptor (m_ioContext, aEndpoint),
                    m_socket (m_ioContext, m_endpoint) {
            // ...
        }

    public:
        void readFromSocket () {
            boost::asio::async_read (m_socket, m_streambuf);

            std::istream istream1 (&m_streambuf);
            std::string string1;
            std::getline (istream1, string1);
            std::cout << "thread [" << std::this_thread::get_id() << "]:: ";
            std::cout << "Server::readFromSocket:: 2:: client message = " << string1 << std::endl;
        }





        void processTheConnection (const boost::system::error_code& aErrorCode) {
            std::cout << "thread [" << std::this_thread::get_id() << "]:: ";
            std::cout << "Server::processTheConnection" << std::endl;
            
            if (aErrorCode.value() == 0) {
                this->readFromSocket();
            }
        }




        void startAccept () {
            std::cout << "thread [" << std::this_thread::get_id() << "]:: ";
            std::cout << "Server::startAccept" << std::endl;
            boost::system::error_code errorCode1;
            auto bindedProcessTheConnection = std::bind (&Server::processTheConnection, this,
                                            errorCode1);
            m_acceptor.async_accept (m_ioContext, m_endpoint, bindedProcessTheConnection);

            m_ioContext.run();
        }
};







int main() {
    SetConsoleCP (1251);
    SetConsoleOutputCP (1251);
    std::cout << "--------------" << std::endl;
    std::cout << "Curren code page: ";
    system ("chcp");
    std::cout << "--------------" << std::endl;
    std::cout << std::endl;

    std::cout << "------ server ------" << std::endl;
    std::cout << std::endl;

    try {
        boost::asio::ip::tcp::endpoint endpoint1 (boost::asio::ip::tcp::v4(), 3333);
        Server Server1 (endpoint1);
        Server1.startAccept ();
    }
    catch (std::exception& e) {
        std::cout << "thread [" << std::this_thread::get_id() << "]:: ";
        std::cerr << "Исключение: " << e.what() << std::endl;
    }


    system ("pause");
    return 0;
}

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

Автор решения: user7860670
  1. shared_ptr используют, когда люди не хотят или не имеют необходимости контролировать время жизни объекта с данамическим временем хранения. Объект будет удален только после уничтожения последнего экземпляра sharad_ptr, разделяющего владение им. То бишь простой способ продлить время жизни на неопределенный срок.

  2. В конструкторе сервера сокет будущего входящего соединения инициализируется m_socket (m_ioContext, m_endpoint), причем использованием ендпоинта, по которому собирается слушать сервер. То бишь порт 3333 занят, потому и вылетает исключение.

Вместо этого стоило создать сокет без указания ендпоинта, m_socket{m_ioContext} и использовать перегрузку async_accept, принимающую функцию обратного вызова в которую будет передавать новый сокет с сигнатурой

void
processTheConnection
(
    boost::system::error_code const & ec
,   boost::asio::ip::tcp::socket peer
)
{
    if (ec)
    {
        // handle error...
    }
    else
    {
        m_socket = ::std::move(peer);
    }
}
→ Ссылка
Автор решения: Serge3leo

Читал учебники(большая часть старых версий) и статьи;

Надо повторить, до просветления. Сервера TCP используют два вида сокетов:

  • сокеты для приёма соединений, отличаются наличием только собственного порта и, возможно, собственного адреса;

  • сокеты для обмена данными, у них уже есть все адреса и порт корреспондента.

В boost, грубо говоря, первые реализованы как acceptor, вторые как socket.

Для вашего сервера, который асинхронно обрабатывает несколько соединений, требуется один acceptor и несколько socket.

boost::asio::ip::tcp::socket m_socket;
boost::asio::streambuf m_streambuf;

Это ошибка, несовместимо с async_read (m_socket, m_streambuf), который сразу возвращает управление без ожидания завершения операции.

Ну и вообще, это противоречит вашему первому тезису об "асинхронном сервере с использованием библиотеки boost".

boost::asio::async_read (m_socket, m_streambuf);
std::istream istream1 (&m_streambuf);

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

void processTheConnection (const boost::system::error_code& aErrorCode)

Ненужные сложности. Для простого примера лучше использовать функцию завершения с двумя аргументами: ec и peer (код ошибки и сокет для обмена данными).

Кроме того processTheConnection() завершается без вызова m_acceptor.async_accept(), прямого или косвенного. Таким образом, текущий код обрабатывает только одно соединение, после чего io_context.run() завершается (с завершением сервера).

При запуске кода проги(приложен вконце) появляется ошибка:

m_acceptor (m_ioContext, aEndpoint),
m_socket (m_ioContext, m_endpoint)

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

Ошибка возникает из-за того, что фактически Вы создаёте два сокета приёма соединений на одном TCP порту.

одном из методов запускается какая-то магия "io_context.run()",

Это основной цикл обработки асинхронных функции завершения. Грубо говоря, внутри цикл пока есть функции завершения, выполнить pool(), select() или иные специфические для ОС функции, что бы дождаться события по одному из каналов, потом вызвать функцию завершения.

зачем повсеместно используют "shared_ptr" ?

Это один из достаточно простых способов контроля за данными соединений. В вашем случае, сервер должен асинхронно работать, как минимум, с множеством пар m_socket, m_streambuf.

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

P.S.

Альтернативно, если частично отказаться от асинхронности, можно заменить async_read() на синхронное чтение.

→ Ссылка