简体   繁体   中英

Boost asio::deadline_timer is resetting before timeout

I am using boost::asio::deadline_timer to add socket timeout option. I have implemented Asynchronous HTTP read, and I start the deadline_timer when I start connecting with the server and on every callback I reset the deadline_timer with function deadline_timer::expires_from_now. In the error handler of the deadline_timer I am clearly checking if the timeout was actual or operation_aborted. But almost always I receive actual timeout even before expected timeout. Please have a look at my given code. I don't understand in every callback I am resetting the timer then why I am getting this timeout error.

#define TCP_SOCKET_TIMEOUT 10

Http::AsyncDownload::AsyncDownload(boost::asio::io_service& io_service, 
                                   const std::string &protocol, 
                                   const std::string &serverip, 
                                   const std::string &port, 
                                   const std::string &path, 
                                   const std::string &filename,
                   const std::string &localFilePath,
                                   const  std::string &username,
                                   const std::string &password) :
resolver_(io_service),
socket_(io_service),
timer_(io_service, boost::posix_time::seconds(TCP_SOCKET_TIMEOUT)),
localFilePath_(localFilePath),
downloadFile_(filename),
protocol_(protocol)
{
     ........
     // Start TCP Socket Timer
 start_socket_timer();

// Start an asynchronous resolve to translate the server and service names
// into a list of endpoints.
tcp::resolver::query query(serverip, port);
resolver_.async_resolve(query, boost::bind(&AsyncDownload::resolve, this, 
                                               boost::asio::placeholders::error,
                                               boost::asio::placeholders::iterator)
                      );
}

void Http::AsyncDownload::resolve(const boost::system::error_code &err,
                                  tcp::resolver::iterator endpoint_iterator)
{
    // Ok, we have received one packet, so refresh the socket timer
    refresh_socket_timer();
        if ( !err ) {
            .........
            boost::asio::async_connect(ssocket_->lowest_layer(), endpoint_iterator, boost::bind(&AsyncDownload::connect, this, boost::asio::placeholders::error));
        } else {
            // Error handling here
        }
}

void Http::AsyncDownload::connect(const boost::system::error_code& err)
{
    // Ok, we have received one packet, so refresh the socket timer
    refresh_socket_timer();
        if ( !err ) {
            .........
        boost::asio::async_write(socket_, request_,
                                boost::bind(&AsyncDownload::write_request, this,     boost::asio::placeholders::error));

    }
        else {
             // Error handling here
        }
    }

void Http::AsyncDownload::hand_shake(const boost::system::error_code& err)
{
    // Ok, we have received one packet, so refresh the socket timer
    refresh_socket_timer();
        if ( !err ) {
            .........
            boost::asio::async_write(*ssocket_, request_,
                                 boost::bind(&AsyncDownload::write_request, this,
                                 boost::asio::placeholders::error));
        } else {
             // Error handling here.
        }
}

void Http::AsyncDownload::write_request(const boost::system::error_code& err)
{
    // Ok, we have received one packet, so refresh the socket timer
    refresh_socket_timer();
        if ( !err ) {
            .............
        boost::asio::async_read_until(*ssocket_, response_, "\r\n",
                         boost::bind(&AsyncDownload::read_status_line, this,
                         boost::asio::placeholders::error));
        } else {
           // Error handling here
        }

}
void Http::AsyncDownload::read_status_line(const boost::system::error_code& err)
{
    // Ok, we have received one packet, so refresh the socket timer
    refresh_socket_timer();
        if ( !err ) {
            ..........
        boost::asio::async_read_until(*ssocket_, response_, "\r\n\r\n",
                boost::bind(&AsyncDownload::read_headers, this,
                boost::asio::placeholders::error));
        } else {
             // Error handling here.
        }
}
void Http::AsyncDownload::read_headers(const boost::system::error_code& err)
{
    refresh_socket_timer();
        if ( !err ) {
            ..............
        boost::asio::async_read(*ssocket_, response_, 
                            boost::asio::transfer_at_least(1), 
                            boost::bind(&AsyncDownload::read_content, this,
                            boost::asio::placeholders::error)
            );
        } else {
            // Error handling here
        }
}
void Http::AsyncDownload::read_content(const boost::system::error_code& err)
{
    // Ok, we have received one packet, so refresh the socket timer
    refresh_socket_timer();
        if ( !err ) {
        boost::asio::async_read(*ssocket_, response_,
                            boost::asio::transfer_at_least(1),
                            boost::bind(&AsyncDownload::read_content, this,
                            boost::asio::placeholders::error)
        );
         } else if ( err != boost::asio::error::eof ) {
             // Error handling here.
         } else {
             // We have an EOF
         }
}

void Http::AsyncDownload::start_socket_timer()
{
    timer_.async_wait(boost::bind(&Http::AsyncDownload::socket_timeout, this,
                           boost::asio::placeholders::error));
}
void Http::AsyncDownload::refresh_socket_timer()
{
    timer_.expires_from_now(boost::posix_time::seconds(TCP_SOCKET_TIMEOUT));
    timer_.async_wait(boost::bind(&Http::AsyncDownload::socket_timeout, this, boost::asio::placeholders::error));
}
void Http::AsyncDownload::socket_timeout(const boost::system::error_code &error_)
{
    // operation_aborted error is thrown whenever we cancel the timer or
    // we reset the timer using expires_from_now function,
    if ( error_ != boost::asio::error::operation_aborted ) {
        csputils::Utils::DEBUG_MSG(downloadFile_, __LINE__, " ------------> TCP Connection Timeout. Broken Connection Found. Abort Operation ");
        // Ok, our TCP connection is broken, we will cancel all asynchronous
        // operations of our sockets.
                    ssocket_->shutdown(); // For Secure Socket & socket.close(); for  normal socket.
    } else {
            // Ok, we have reset the timer, please continue...
        }
}

Ok. In the above code you will notice I am starting the timer in the constructor, and once I receive one packet I am refreshing the timer with expries_from_now function call. This call will call error handler (socket_timeout) with the error code operation_aborted, but for every actual timeout this function will be called without operation_aborted and you can see I am checking operation_aborted explicitly, but still as per my expectation I am receiving timeout early, though I am refreshing the timer on every packet I receive but I am sure it is being expired before 10 seconds. I also tried with timeout value = 60 but same effect. Any thoughts.

UPDATE Updated my code with error handling I have used in my actual code. I have trimmed down my actual code for the sake of simplicity. You can notice in timer timeout handler, I am checking if the timeout was not explicit (ie operation_aborted), then close the socket. Once the socket will be closed I will get an exception in my socket data handler (mostly read_content function). In that function when I receive exception my socket will exit calling the AsyncDownload destructor where I am doing some more cleaning. My download code works perfect if I remove deadline_timer. I have added it here to detect unforeseen TCP connection drops. I am running this code on embedded Linux on ARM architecture, and my sockets are secure but as I have mentioned my code works perfect without timer so I don't think the problem has something to do with sockets.

Okay, so, I've played with your example hands on.

I can see an infinite loop happening when the timeout has expired, because there is a lack of error handling, and the read_content loop simply continues despite the fact that the timeout had been reached.

Note:

  1. the first read_until_async suggests that it will only read the statusline. This is simply not the case! See boost read_until does not stop at delimiter .

    It will read the first "swoop" of data that includes a minimum of \\r\\n once. In practice, many servers send the headers in the same packet. (In fact, effective behaviour might depend on proxies and possibly hardware). So, it's not safe to assume that you should always require the second read_until_async to read the headers. And since another "\\r\\n\\r\\n" might never happen, it's easy to get into failure mode (when eg end-of-stream is reached).

  2. If you don't carefully watch the flow, it's possible to conclude that "the timer fires too early" (because you land in refresh_socket_timer , for example). However, what is happening in my error situation is that read_until_async simply immediately returns, which is erronously treated as if a packet was returned.

So I propose something like

void read_content(const boost::system::error_code& err)
{
    if (err && socket_.is_open())
    {
        DEBUG_TRACE();
        boost::asio::async_read(*ssocket_, response_,
                boost::asio::transfer_at_least(1),
                boost::bind(&AsyncDownload::read_content, this,
                    boost::asio::placeholders::error)
                );
        refresh_socket_timer();
    }
    else
        std::cerr << "Error '" << err.message() << "'\n";
}

So we avoid the infinite loop on connection dropped.

void refresh_socket_timer()
{
    if (socket_.is_open())
    {
        DEBUG_TRACE();
        timer_.expires_from_now(boost::posix_time::seconds(TCP_SOCKET_TIMEOUT));
        timer_.async_wait(boost::bind(&AsyncDownload::socket_timeout, this, boost::asio::placeholders::error));
    }
}

So we avoid refreshing the timer after the socket has been closed.

void socket_timeout(const boost::system::error_code &error_)
{
    // operation_aborted error is thrown whenever we cancel the timer or
    // we reset the timer using expires_from_now function,
    if ( error_ != boost::asio::error::operation_aborted ) {
        DEBUG_TRACE();
        std::cout << " ------------> TCP Connection Timeout. Broken Connection Found. Abort Operation\n";
        // Ok, our TCP connection is broken, we will cancel all asynchronous
        // operations of our sockets.
        socket_.close();
    }
}

So we actively close the socket on timeout.


If you comment the if(...) conditionals out in the above, you'd see the failure mode I was describing.

Here's the full example I used to test:

#include <boost/asio.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/bind.hpp>

using tcp = boost::asio::ip::tcp;

#define TCP_SOCKET_TIMEOUT 2
#define DEBUG_TRACE() do { std::cout << __FILE__ << ':' << __LINE__ << "\t" << __FUNCTION__ << "\n"; } while(false)

struct AsyncDownload
{
    tcp::resolver               resolver_;
    tcp::socket                 socket_;
    tcp::socket*                ssocket_ = &socket_;
    boost::asio::deadline_timer timer_;
    std::string                 localFilePath_;
    boost::asio::streambuf      response_;

    AsyncDownload(boost::asio::io_service& io_service, 
            const std::string &protocol, 
            const std::string &serverip, 
            const std::string &port) :
        resolver_(io_service),
        socket_(io_service),
        timer_(io_service, boost::posix_time::seconds(TCP_SOCKET_TIMEOUT))
    {
        DEBUG_TRACE();
        // Start TCP Socket Timer
        start_socket_timer();

        // Start an asynchronous resolve to translate the server and service names
        // into a list of endpoints.
        tcp::resolver::query query(serverip, port);
        resolver_.async_resolve(query, boost::bind(&AsyncDownload::resolve, this, 
                    boost::asio::placeholders::error,
                    boost::asio::placeholders::iterator)
                );
    }

    void resolve(const boost::system::error_code &err, tcp::resolver::iterator endpoint_iterator)
    {
        DEBUG_TRACE();
        boost::asio::async_connect(ssocket_->lowest_layer(), endpoint_iterator, boost::bind(&AsyncDownload::connect, this, boost::asio::placeholders::error));
        refresh_socket_timer();
    }

    void connect(const boost::system::error_code& err)
    {
        DEBUG_TRACE();

        std::string const request_ = "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n";
        boost::asio::async_write(socket_, boost::asio::buffer(request_),
                boost::bind(&AsyncDownload::write_request, this, boost::asio::placeholders::error));
        refresh_socket_timer();
    }

    void write_request(const boost::system::error_code& err)
    {
        DEBUG_TRACE();
        boost::asio::async_read_until(*ssocket_, response_, "\r\n",
                boost::bind(&AsyncDownload::read_status_line, this,
                    boost::asio::placeholders::error));
        refresh_socket_timer();
    }
    void read_status_line(const boost::system::error_code& err)
    {
        DEBUG_TRACE();
        std::cout << "read_status_line: " << &response_ << "\n";
        boost::asio::async_read_until(*ssocket_, response_, "\r\n\r\n",
                boost::bind(&AsyncDownload::read_headers, this,
                    boost::asio::placeholders::error));
        refresh_socket_timer();
    }
    void read_headers(const boost::system::error_code& err)
    {
        DEBUG_TRACE();
        // ..............
        boost::asio::async_read(*ssocket_, response_, 
                boost::asio::transfer_at_least(1), 
                boost::bind(&AsyncDownload::read_content, this,
                    boost::asio::placeholders::error)
                );
        refresh_socket_timer();
    }
    void read_content(const boost::system::error_code& err)
    {
        if (err && socket_.is_open())
        {
            DEBUG_TRACE();
            boost::asio::async_read(*ssocket_, response_,
                    boost::asio::transfer_at_least(1),
                    boost::bind(&AsyncDownload::read_content, this,
                        boost::asio::placeholders::error)
                    );
            refresh_socket_timer();
        }
        else
            std::cerr << "Error '" << err.message() << "'\n";
    }

    void start_socket_timer()
    {
        DEBUG_TRACE();
        timer_.async_wait(boost::bind(&AsyncDownload::socket_timeout, this, boost::asio::placeholders::error));
    }
    void refresh_socket_timer()
    {
        if (socket_.is_open())
        {
            DEBUG_TRACE();
            timer_.expires_from_now(boost::posix_time::seconds(TCP_SOCKET_TIMEOUT));
            timer_.async_wait(boost::bind(&AsyncDownload::socket_timeout, this, boost::asio::placeholders::error));
        }
    }
    void socket_timeout(const boost::system::error_code &error_)
    {
        // operation_aborted error is thrown whenever we cancel the timer or
        // we reset the timer using expires_from_now function,
        if ( error_ != boost::asio::error::operation_aborted ) {
            DEBUG_TRACE();
            std::cout << " ------------> TCP Connection Timeout. Broken Connection Found. Abort Operation\n";
            // Ok, our TCP connection is broken, we will cancel all asynchronous
            // operations of our sockets.
            socket_.close();
        }
    }
};

int main()
{
    DEBUG_TRACE();
    boost::asio::io_service io_service;
    AsyncDownload download(
            io_service,
            "http",
            "www.google.com",
            "80");

    io_service.run();
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM