1

I am trying to write a Client with asio that does the following:

  1. Connects to a server.
  2. Tries to read back some data after it is connected to the server.

The problem I am finding is that operations do not seem to be executed in sequence, as I would expect. Here it is the code:

std::future<NetMessage> Cliente::asyncConnectTo(std::string const & hostname,
                                          int port,
                                          std::string const & name) {
    using namespace std::literals;
    boost::asio::spawn
        (strand_,
         [this, name, port, hostname](boost::asio::yield_context yield) mutable {
            i_->playerLog->trace() << name << " connecting to " << hostname << ':'
                                   << port;
            Guinyote::Utils::connectWith
                (*this, std::move(hostname), port,
                 std::move(name), yield);

            i_->playerLog->info() << "Connected to server.";
        });
    runthread_ = std::thread([&] {
            try {
                i_->playerLog->info() << "Starting...";
                this->service_.run();
            }
            catch (std::exception & e) {
                std::cout << e.what() << std::endl;
            }
        });
    return this->asyncReceiveMessage(); //This function spawns another coroutine through the same strand_ object.
}

The function this->asyncReceiveMessage() is expected to receive a message that the server sends back after connecting:

std::future<NetMessage> Cliente::asyncReceiveMessage() {
    namespace ba = boost::asio;

    std::promise<NetMessage> prom;
    std::future<NetMessage> message = prom.get_future();
    ba::spawn
        (strand_,
         [this, p = std::move(prom)](boost::asio::yield_context yield) mutable {
            i_->playerLog->trace("waiting to read message from server socket...");
            boost::system::error_code ec{};
            boost::int64_t messageSize{};
            ba::async_read(
                socket_,
                ba::buffer(&messageSize, sizeof(boost::int64_t)),
                yield);

            i_->playerLog->trace() << "Client: Received message of "
                                   << messageSize << " bytes. Reading message...";

            std::vector<char> serverMessageData(messageSize);
            ba::async_read
                (socket_,
                 ba::buffer(serverMessageData),
                 yield);

            using namespace boost::iostreams;
            basic_array_source<char> input_source(&serverMessageData[0], serverMessageData.size());
            stream<basic_array_source<char>> stream(input_source);
            boost::archive::binary_iarchive archive(stream);
            Utils::MensajeRed msg;

            archive >> msg;
            i_->playerLog->trace() << "NetMessage correctly read.";
            p.set_value(std::move(msg));

        });
    return message;
}

In my log file I am getting the following on client side:

[clientLog] [info] Client: Starting...
[clientLog] [trace] User1234 connecting to localhost:10004
[clientLog] [trace] waiting to read message from server socket...

But I would expect that the third line of that log would come after [clientLog] [info] Connected to server. So my expectation is the following:

[clientLog] [info] Client: Starting...
[clientLog] [trace] User1234 connecting to localhost:10004
[clientLog] [info] Connected to server.
[clientLog] [trace] waiting to read message from server socket...

Namely, "Connected to server" should always happen before "waiting to read message from server socket...".

Does anyone know what is going on? I thought strand_ would guarantee order of execution, but it seems I may have misunderstood something. What is the correct solution to get the effect I want?

Arunmu
  • 6,837
  • 1
  • 24
  • 46
Germán Diago
  • 7,473
  • 1
  • 36
  • 59
  • I see this is the same but I do not see a proper solution. It just says something like "handle by hand" actually: http://stackoverflow.com/questions/19946555/boostio-service-how-to-guarantee-handler-execution-sequence – Germán Diago Nov 27 '16 at 11:29

1 Answers1

1

So the problem here is that the tasks are going to be started in order, but subsequent boost::asio::spawn spawn invocations in chain does not mean the first task will finish before the second.

It will just happen that the first task is started before the second task, nothing else.

In order to keep the order, I just created a coroutine that is invoked inside the spawn in asyncConnectTo, instead of spawning two different coroutines. This way I make sure the first coroutine finishes before the second:

NetMessage Cliente::asyncReceiveMessageCoro(boost::asio::yield_context yield) {
    namespace ba = boost::asio;
    i_->playerLog->trace("waiting to read message from server socket...");
    boost::system::error_code ec{};
    boost::int64_t messageSize{};
    ba::async_read(
      socket_,
      ba::buffer(&messageSize, sizeof(boost::int64_t)),
      yield);

    i_->playerLog->trace() << "Client: Received message of "
                                   << messageSize << " bytes. Reading message...";

    std::vector<char> serverMessageData(messageSize);
    ba::async_read
       (socket_,
        ba::buffer(serverMessageData),
        yield);

    using namespace boost::iostreams;
    basic_array_source<char> input_source(&serverMessageData[0], serverMessageData.size());
    stream<basic_array_source<char>> stream(input_source);
    boost::archive::binary_iarchive archive(stream);
    Utils::MensajeRed msg;

    archive >> msg;
    i_->playerLog->trace() << "NetMessage correctly read.";
    return msg;
}

This coroutine can be chained at the end:

std::future<NetMessage> Cliente::asyncConnectTo(std::string const & hostname,
                                          int port,
                                          std::string const & name) {
    using namespace std::literals;

    std::promise<NetMessage> msgProm;
    auto msg = msgProm.get_future();
    boost::asio::spawn
        (strand_,
         [this, name, port, hostname, p = std::move(msgProm)](boost::asio::yield_context yield) mutable {
            i_->playerLog->trace() << name << " connecting to " << hostname << ':'
                                   << port;
            Guinyote::Utils::connectWith
                (*this, std::move(hostname), port,
                 std::move(name), yield);

            i_->playerLog->info() << "Connected to server.";
            p.set_value(this->asyncReceiveCoro(yield));
        });
    runthread_ = std::thread([&] {
            try {
                i_->playerLog->info() << "Starting...";
                this->service_.run();
            }
            catch (std::exception & e) {
                std::cout << e.what() << std::endl;
            }
        });
    return msg;
}

My old asyncReceiveMessage just becomes a combination of spawn + calling the asyncReceiveMessageCoro.

Germán Diago
  • 7,473
  • 1
  • 36
  • 59