3

I am writing a c++ websocket server with boost beast 1.70 and mysql 8 C connector. The server will have several clients simultaneously connected. The particularity is that each client will perform like 100 websocket requests in a row to the server. Each request is "cpu light" for my server but the server perform a "time heavy" sql request for each request.

I have started my server with the websocket_server_coro.cpp example. The server steps are :

1) a websocket read

2) a sql request

3) a websocket write

The problem is that for a given user, the server is "locked" at the step 2 and cannot read until this step and the step 3 are finished. Thus, the 100 requests are solved sequentially. This is too slow for my use case.

I have read that non blocking read/write are not possible with boost beast. However, what I am trying to do now is to execute async_read and async_write in a coroutine.

void ServerCoro::accept(websocket::stream<beast::tcp_stream> &ws) {
    beast::error_code ec;

    ws.set_option(websocket::stream_base::timeout::suggested(beast::role_type::server));

    ws.set_option(websocket::stream_base::decorator([](websocket::response_type &res) {
                res.set(http::field::server, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-Server-coro");
            }));

    ws.async_accept(yield[ec]);
    if (ec) return fail(ec, "accept");

    while (!_bStop) {
        beast::flat_buffer buffer;
        ws.async_read(buffer, yield[ec]);

        if (ec == websocket::error::closed) {
            std::cout << "=> get closed" << std::endl;
            return;
        }

        if (ec) return fail(ec, "read");

        auto buffer_str = new std::string(boost::beast::buffers_to_string(buffer.cdata()));
        net::post([&, buffer_str] {

            // sql async request such as :
            // while (status == (mysql_real_query_nonblocking(this->con, sqlRequest.c_str(), sqlRequest.size()))) {
            //    ioc.poll_one(ec);
            // }
            // more sql ...

            ws.async_write(net::buffer(worker->getResponse()), yield[ec]); // this line is throwing void boost::coroutines::detail::pull_coroutine_impl<void>::pull(): Assertion `! is_running()' failed.
            if (ec) return fail(ec, "write");

        });
    }
}

The problem is that the line with async_write throw an error :

void boost::coroutines::detail::pull_coroutine_impl::pull(): Assertion `! is_running()' failed.

If a replace this line with a sync_write, it works but the server remains sequential for a given user. I have tried to execute this code on a single threaded server. I have also tried to use the same strand for async_read and async_write. Still have the assertion error.

Is such server impossible with boost beast for websocket ? Thank you.

Julien
  • 51
  • 7
  • 1
    Have you tried dispatching the SQL work to a separate thread pool, and then posting the results back to the web socket connection's strand? This should allow you to perform the SQL operations concurrently. You will need to implement an outgoing message queue, code can be found in the "websocket chat" Beast example program. – Vinnie Falco May 21 '19 at 02:46
  • 1
    It works and it works very well. I used the exemple of "websocket chat" and "async server". I have put the sql code in a net::post without a strand and the websocket writes/reads on a strand (with a queue for the async_write). Thank you Vinnie Falco for your response and for your work on boost beast. – Julien May 22 '19 at 15:28

1 Answers1

2

By following the suggestion of Vinnie Falco, I rewrite the code by using "websocket chat" and "async server" as exemple. Here is the final working result of the code :

void Session::on_read(beast::error_code ec, std::size_t bytes_transferred)
{
    boost::ignore_unused(bytes_transferred);

    if(ec == websocket::error::closed) return;  // This indicates that the Session was closed
    if(ec) return fail(ec, "read");

    net::post([&, that = shared_from_this(), ss = std::make_shared<std::string const>(std::move(boost::beast::buffers_to_string(_buffer.cdata())))] {
        /* Sql things that call ioc.poll_one(ec) HERE, for me the sql response go inside worker.getResponse() used below */

        net::dispatch(_wsStrand, [&, that = shared_from_this(), sss = std::make_shared < std::string const>(worker.getResponse())] {
            async_write(sss);
        });
    });
    _buffer.consume(_buffer.size()); // we remove from the buffer what we just read
    do_read(); // go for another read
}

void Session::async_write(const std::shared_ptr<std::string const> &message) {
    _writeMessages.push_back(message);

    if (_writeMessages.size() > 1) {
        BOOST_LOG_TRIVIAL(warning) << "WRITE IS LOCKED";
    } else {
        _ws.text(_ws.got_text());
            _ws.async_write(net::buffer(*_writeMessages.front()), boost::asio::bind_executor(_wsStrand, beast::bind_front_handler(
                    &Session::on_write, this)));
    }
}

void Session::on_write(beast::error_code ec, std::size_t)
{
    // Handle the error, if any
    if(ec) return fail(ec, "write");

    // Remove the string from the queue
    _writeMessages.erase(_writeMessages.begin());

    // Send the next message if any
    if(!_writeMessages.empty())
        _ws.async_write(net::buffer(*_writeMessages.front()), boost::asio::bind_executor(_wsStrand, beast::bind_front_handler(
                        &Session::on_write, this)));
}

Thank you.

Julien
  • 51
  • 7
  • 2
    This looks great, although it can be improved still. Since you are using Boost 1.70, you can put the strand on the NextLayer object, and initialize it once upon construction of the websocket. Then, all asynchronous websocket operations will use the strand automatically, without the need to use bind_executor at call sites. This is covered in the documentation. – Vinnie Falco May 22 '19 at 17:22