Search code examples
c++tcpboost-asio

asio (standalone, non-boost) async_write handler not called by io_context.run() or io_context.poll()


(I know the tag is boost-asio, but I'm using standalone asio) I am debugging my tcp client application, and it boils down to this:

// checkpoint 1
asio::async_write(socket, asio::buffer(input), [](const asio::error_code &ec, std::size_t bytes_transferred) {
    // checkpoint 2
}
// checkpoint 3
io_context.run()
// checkpoint 4

So pretty much checkpoint 1, 3, 4 get called (i just std::cout something to console), but checkpoint 2 does not. The actual write operation DOES WORK though, because my backend server receives the write. The application is completely single-threaded, the io_context and socket are valid, socket.is_open() returns true. I would appreciate more clues as to how I can debug this, as I don't have much experience with asio.

EDIT: this is a not so boiled down version:
(print() is a template function that std::cout's everything)
network.h:

class Network
{
public:
    Network();
    void ConnectToServer();
    void Update();
    void Test(std::string input);

private:
    asio::io_context io_context;
    asio::ip::tcp::socket socket;
    asio::ip::basic_resolver_results<asio::ip::tcp> endpoints;
}  

network.cpp:

Network::Network() : socket(io_context)
{
    asio::ip::tcp::resolver resolver(io_context);
    endpoints = resolver.resolve(serverIP, serverPort); // ip and port are defined static variables
}
void Network::ConnectToServer()
{
    asio::connect(socket, endpoints);

    std::string initMessage = "I just connected";
    asio::write(socket, asio::buffer(initMessage));

    asio::streambuf receiveBuffer;
    asio::read_until(socket, receiveBuffer, "\n");
    std::istream responseStream(&receiveBuffer);

    std::string response;
    std::getline(responseStream, response);
    print("server response: " + response);
}
void Network::Update()
{
    print("about to poll");
    io_context.poll();
    print("just polled");
}
void Network::Test(std::string input)
{
    print("about to async_write");
    asio::async_write(socket, asio::buffer(input), [](const asio::error_code &ec, std::size_t bytes_transferred)
                      { 
                        try{
                            print("async_write completed no error");
                        }
                        catch(const std::exception& e)
                        {
                            print("async_write ERROR");
                        } });
    print("after async_write call");
}

main.cpp:

Network *network;

int main(void)
{
    // other single-threaded stuff
    network = new Network();
    network->ConnectToServer();
    network->Test("hello");
    // other single-threaded stuff
    print("main loop starting");
    while(true) // !glfwWindowShouldClose(window) - it's a glfw aplication
    {
        // other single-threaded stuff
        network->Update();
        if(some player input)
        {
            print("player input");
            network->Test("in main loop");
        }
        // other single-threaded stuff
    }
}

and this is what I get:

about to connect to server
connected to server
server response: You connected successfully
about to async_write
after async_write call
main loop starting
about to poll
async_write completed no error
just polled
about to poll
just polled
about to poll
just polled
|
|
about to poll
just polled
player input
about to async_write
after async_write call
about to poll
just polled
about to poll
just polled
|
|

My server receives a correct write from both calls to Test() - the one before the main loop and the one inside the main loop, but the completion handlers for those asyncs don't get called by the poll(). (note: Network *network is in global scope because i use it as an extern from other cpp files). And I don't make any other calls to network's functions.


Solution

  • Please include the self-contained code. The code shown is obviously ok, and there will be another difference that you're not showing here.

    Start from e.g. this Live Example

    #include <boost/asio.hpp>
    #include <iostream>
    namespace asio = boost::asio;
    using asio::ip::tcp;
    using boost::system::error_code;
    
    #define checkpoint(x) do { std::clog << "checkpoint " << x << std::endl; } while(0)
    
    int main() {
        asio::io_context ioc;
        std::string      input = "hello world\n";
    
        tcp::socket socket(ioc);
        socket.connect({{}, 8989});
    
        checkpoint(1);
    
        asio::async_write(socket, asio::buffer(input), [](error_code ec, size_t bytes_transferred) {
            checkpoint(2);
            std::cerr << ec.message() << ": " << bytes_transferred << "\n";
        });
    
        checkpoint(3);
        
        ioc.run();
        checkpoint(4);
    }
    

    Which works online, and interactively:

    enter image description here

    What could be happening potentially is that you start run() before the async operation(s) and the services runs out of work before the completion ever gets registered. Obviously, that is not the case in your question code.

    UPDATE TO EDITS

    The most glaring problem is using async_write with a stack buffer. That invokes UB. Next up, you need to protect against overlapping writes.

    Next up, polling isn't great for composed operations. I'd poll in a loop to make operation a bit more reliable. Also, restart() when the context ran out of work (see Boost::Asio : io_service.run() vs poll() or how do I integrate boost::asio in mainloop)

    Lastly, read_until may read more than the initial response, so you might want to keep the unused part of the response buffer.

    With those out of the way, things look like they work ok:

    Live On Coliru

    
    #include <boost/asio.hpp>
    #include <deque>
    #include <iomanip>
    #include <iostream>
    namespace asio = boost::asio;
    using namespace std::chrono_literals;
    using asio::ip::tcp;
    using boost::system::error_code;
    
    static std::string const serverIP   = "127.0.0.1";
    static std::string const serverPort = "7878";
    static inline void print(auto const&... args) { (std::cout << ... << args) << std::endl; }
    
    struct Network {
        void ConnectToServer();
        void Update();
        void Test(std::string input);
    
      private:
        std::deque<std::string> outbox;
    
        void send_loop() {
            if (outbox.empty())
                return;
            async_write(socket, asio::buffer(outbox.front()), [this](error_code ec, size_t bytes_transferred) {
                print("async_write completed: ", ec.message(), ", ", bytes_transferred);
                if (!ec) {
                    outbox.pop_front();
                    send_loop();
                } else
                    throw std::runtime_error("Network error");
            });
        }
    
        asio::io_context            io_context;
        tcp::socket                 socket{io_context};
        tcp::resolver::results_type endpoints = tcp::resolver(io_context).resolve(serverIP, serverPort);
    };
    
    void Network::ConnectToServer() {
        connect(socket, endpoints);
        write(socket, asio::buffer(std::string("I just connected\n")));
    
        std::string response;
        auto n = read_until(socket, asio::dynamic_buffer(response), "\n");
    
        print("server response: ", quoted(response.substr(0, n - 1)));
    }
    
    void Network::Update() {
        if (io_context.stopped())
            io_context.restart();
        while (io_context.poll())
            ;
    }
    
    void Network::Test(std::string input) {
        outbox.push_back(std::move(input));
        if (outbox.size() == 1)
            send_loop();
    }
    
    int main() {
        // other single-threaded stuff
        Network network;
        network.ConnectToServer();
        network.Test("hello\n");
    
        // other single-threaded stuff
        print("main loop starting");
    
        for (unsigned i = 0; /*!glfwWindowShouldClose(window)*/; std::this_thread::sleep_for(50ms)) {
            // other single-threaded stuff
            network.Update();
    
            if (rand() % 4) {
                std::cout << "." << std::flush;
            } else {
                print("player input");
                network.Test("in main loop " + std::to_string(++i) + "\n");
            }
            // other single-threaded stuff
        }
    }
    

    enter image description here