Home > Mobile >  Asynchronous reading from boost::asio socket with timeout
Asynchronous reading from boost::asio socket with timeout

Time:08-13

I have following code:

class Connection
{
public:
    Connection(boost::asio::io_context& ctx) 
    : context(ctx),
      timer(ctx),
      socket(ctx)
    {
        buf.resize(10000);
    }

    void connect(const std::string& ip, uint16_t port)
    {
        boost::system::error_code code;
        boost::asio::ip::tcp::endpoint ep(ip::address::from_string(ip), port);
        socket.connect(ep, code);
        std::cout << "Connection status " << code.message() << std::endl;
    }

    void write()
    {
        std::string request;
        request  = "GET / HTTP/1.1\r\n";
        request  = "Host: hostname.com\r\n\r\n";

        std::cout << "Trying to write..." << std::endl;

        boost::asio::async_write(socket, boost::asio::buffer(request), transfer_all(),
        [=](const boost::system::error_code& e, std::size_t len)
        {
            std::cout << "request size is " << request.size() << std::endl;
            std::cout << "written size is " << len << std::endl;
        });
    }

    void read()
    {
        timer.expires_from_now(boost::posix_time::milliseconds(4000));
        timer.async_wait([this](const boost::system::error_code& e)
        {
            if (!e) {
                std::cout << "Socket closed by timer" << std::endl;
                socket.close();
            }
        });

        async_read(socket, boost::asio::buffer(buf), transfer_at_least(1), 
        [this](const boost::system::error_code& e, std::size_t len)
        {
            std::cout << "read callback" << std::endl;
            sleep(1);
            std::cout << buf.substr(0, len);
            if (!e)
            read();
        });
    }

private:
    std::string buf;
    boost::asio::io_context& context;
    boost::asio::deadline_timer timer;
    boost::asio::ip::tcp::socket socket;
};

int main(int argc, char** argv)
{
    boost::asio::io_context ctx;
    Connection connection(ctx);
    
    /*std::thread runThread([&ctx]()
    {
        
    });*/

    //sleep(2);
    connection.connect("ip_address", 80);
    connection.write();
    boost::shared_ptr<io_context::work> work(new io_context::work(ctx));
    std::cout << "Waiting for read..." << std::endl;
    connection.read();
    ctx.run();
}

I expect it to work like this: When the read function is called, we update the timer each time. If the timer expires before the data is available for reading, we will close the socket and thus cancel the asynchronous read operation. I read on the Internet that there may be a spurious wakeup on the timer, but I did not fully figure it out. How reliable is this code?

Socket and timer uses same io_context

CodePudding user response:

Your write function has big problems. You're initiating an async_write on a temporary (local) request (You even access request from the completion handler). This leads to undefined behaviour.

Your time callback looks fine, except it could be more specific about the error code. Most likely "spurious wake-up" would refer to completion with error::operation_aborted when the timer is canceled. This will also happen when you reset it (with expires_from_now).

I would probably write it more like

timer.expires_from_now(4000ms);
timer.async_wait([this](error_code ec) {
    if (ec != boost::asio::error::operation_aborted)
        return;

    std::cout << "Socket closed by timer" << std::endl;
    socket.cancel();
});

Note the cancel() which should suffice.

Here's a redo using some simplifications and good practices:

Live On Coliru

//#define BOOST_ASIO_ENABLE_HANDLER_TRACKING 1
#include <boost/asio.hpp>
#include <iostream>
using boost::asio::ip::tcp;
using boost::system::error_code;

using std::this_thread::sleep_for;
using namespace std::chrono_literals;

class Connection {
  public:
    template <typename Executor>
    Connection(Executor ex) : timer(ex)
                            , socket(ex) {
        buf.resize(10000);
    }

    void connect(const std::string& ip, uint16_t port) {
        std::cout << "Connecting" << std::endl;
        auto addr = boost::asio::ip::address::from_string(ip);
        socket.connect({addr, port});
        std::cout << "Connected to " << socket.remote_endpoint() << std::endl;
    }

    void write() {
        std::cout << "Trying to write..." << std::endl;

        async_write( //
            socket, boost::asio::buffer(request),
            // boost::asio::transfer_all(),
            [this](error_code ec, size_t transferred) {
                std::cout << "request size is " << request.size() << "\n"
                          << "written size is " << transferred << " ("
                          << ec.message() << ")" << std::endl;
            });
    }

    void read() {
        timer.expires_from_now(4000ms);
        timer.async_wait([this](error_code ec) {
            if (ec != boost::asio::error::operation_aborted)
                return;

            socket.cancel();
        });

        async_read( //
            socket, boost::asio::buffer(buf), boost::asio::transfer_at_least(1),
            [this](error_code ec, size_t transferred) {
                if (ec == boost::asio::error::operation_aborted) {
                    std::cout << "Socket closed by timer" << std::endl;
                    return;
                }

                std::cout << "read callback (" << ec.message() << ", "
                          << transferred << " bytes)" << std::endl;
                sleep_for(1s);
                //std::cout << buf.substr(0, transferred);
                if (!ec)
                    read();
            });
    }

  private:
    std::string request = "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n";
    std::string buf;
    boost::asio::steady_timer timer;
    tcp::socket               socket;
};

int main() {
    boost::asio::io_context ctx;

    // strand not required unless multi-threading:
    Connection connection(make_strand(ctx.get_executor()));
    sleep_for(2s);

    connection.connect("93.184.216.34", 80);
    connection.write();
    connection.read();

    //auto work = make_work_guard(ctx); // not required
    std::cout << "Waiting for read..." << std::endl;
    ctx.run();
}

Prints

Connecting
Connected to 93.184.216.34:80
Trying to write...
Waiting for read...
request size is 41
written size is 41 (Success)
read callback (Success, 1591 bytes)
Socket closed by timer

Now, I wouldn't bother with transfer_at_least and friends. If you want, use async_read_until with a completion condition or boundary like "\r\n\r\n". I would probably also just receive into a dynamic buffer:

async_read_until( //
    socket, boost::asio::dynamic_buffer(buf), "\r\n\r\n",
    [this](error_code ec, size_t transferred) {
        if (ec == boost::asio::error::operation_aborted) {
            std::cout << "Socket closed by timer" << std::endl;
            return;
        }

        std::cout << "read callback (" << ec.message() << ", "
                  << transferred << " bytes)\n"
                  << "Headers only: "
                  << std::string_view(buf).substr(0, transferred)
                  << "Remaining in buffer (start of body): "
                  << buf.size() - transferred << std::endl;
    });

Now you can expect to see: Live

Connecting
Connected to 93.184.216.34:80
Trying to write...
Waiting for read...
request size is 41
written size is 41 (Success)
read callback (Success, 335 bytes)
Headers only: HTTP/1.1 200 OK
Age: 550281
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Sat, 13 Aug 2022 11:52:28 GMT
Etag: "3147526947 ident"
Expires: Sat, 20 Aug 2022 11:52:28 GMT
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Server: ECS (chb/0286)
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1256
Remaining in buffer (start of body): 177

Bonus: Use a Library!

In reality I'd be using Boost Beast: Live On Coliru

#include <boost/beast.hpp>
#include <iostream>
using boost::asio::ip::tcp;
using namespace std::chrono_literals;

namespace beast = boost::beast;
namespace http  = beast::http;

int main() {
    boost::asio::io_context ctx;
    beast::tcp_stream       conn(ctx.get_executor());
    conn.connect({boost::asio::ip::address::from_string("93.184.216.34"), 80});
    conn.expires_after(1000ms);

    auto req = http::request<http::empty_body>(http::verb::get, "/", 11);
    req.set(http::field::host, "www.example.com");
    write(conn, req);

    conn.expires_after(4000ms);

    http::response<http::string_body> res;
    beast::flat_buffer buf;
    read(conn, buf, res);

    std::cout << "Headers: "       << res.base()          << "\n";
    std::cout << "Body received: " << res.body().length() << "\n";
}

Prints

Headers: HTTP/1.1 200 OK
Age: 551081
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Sat, 13 Aug 2022 12:05:48 GMT
Etag: "3147526947 ident"
Expires: Sat, 20 Aug 2022 12:05:48 GMT
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Server: ECS (chb/0286)
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1256

Body received: 1256
  • Related