1

I'm trying to write server with boost::asio but I want that the boost::asio::async_read operation to be with time out if no data is coming, but i can figure how to do it. this is my code so far

void do_read_header() {
    auto self(shared_from_this());
    std::cout << "do_read_header\n";
    boost::asio::async_read(
        socket_, boost::asio::buffer(res.data(), res.header_length),
        [this, self](boost::system::error_code ec,
                     std::size_t length) {
            if (!ec && res.decode_header()) {
                do_read_body();
            }
        });
    do_write();
}

void do_read_body() {
    auto self(shared_from_this());
    Message msg;
    std::cout << "do_read_body\n";
    boost::asio::async_read(
        socket_, boost::asio::buffer(res.body(), res.body_length()),
        [this, self](boost::system::error_code ec,
                     std::size_t length) {
            if (!length) {
                return;
            }
            if (!ec) {
                try {
                    std::cout << "read " << res.body() << "\n";
                    request_queue_.send(res.body(), res.body_length(),
                                        0);
                } catch (const std::exception& ex) {
                    std::cout << ex.what() << "\n";
                }
            } else {
                if (ec) {
                    std::cerr << "read error:" << ec.value()
                              << " message: " << ec.message() << "\n";
                }
                socket_.close();
            }
            do_read_header();
        });
}

void start() {
    post(strand_, [this, self = shared_from_this()] {
        do_read_header();
        do_write();
    });
}

class Server {
  public:
    Server(boost::asio::io_service& io_service, short port)
        : acceptor_(io_service, tcp::endpoint(tcp::v4(), port)),
          socket_(io_service) {
        do_accept();
    }

  private:
    void do_accept() {
        acceptor_.async_accept(
            socket_, [this](boost::system::error_code ec) {
                if (!ec) {
                    std::cout << "accept connection\n";

                    std::make_shared<Session>(std::move(socket_))
                        ->start();
                }

                do_accept();
            });
    }

    tcp::acceptor acceptor_;
    tcp::socket socket_;
};
sehe
  • 328,274
  • 43
  • 416
  • 565
mosh
  • 35
  • 5
  • Depends on what you want to do after a timeout. Do you want to close the socket? – rustyx Mar 10 '21 at 14:32
  • no, i want to move to the `do_write()` function – mosh Mar 10 '21 at 14:51
  • Sounds like you want a [deadline timer and `async_wait`](https://www.boost.org/doc/libs/1_75_0/doc/html/boost_asio/reference/basic_deadline_timer/async_wait.html). Then just take appropriate action depending on which handler -- timer or read op. -- is invoked first. – G.M. Mar 10 '21 at 15:00
  • You should probably just post self-contained code. I'm getting the very distinct feeling I've seen this code before, yet I spent a lot of time just making it complete, again. If I can do it just by guessing bits, probably you should be able to do it :) – sehe Mar 10 '21 at 17:55

2 Answers2

4

You can add a deadline timer that cancels the IO operation. You can observe the cancellation because the completion will be called with error::operation_aborted.

deadline_.expires_from_now(1s);
deadline_.async_wait([self, this] (error_code ec) {
    if (!ec) socket_.cancel();
});

I spent about 45 minutes making the rest of your code self-contained:

  • in this example I'll assume that we
    • want to wait for max 5s for a new header to arrive (so after a new session was started or until the next request arrives on the same session)
    • after which the fullbody must be received within 1s

Note also that we avoid closing the socket - that's done in the session's destructor. It's better to shutdown gracefully.

Live Demo

#include <boost/asio.hpp>
#include <boost/endian/arithmetic.hpp>
#include <boost/interprocess/ipc/message_queue.hpp>
#include <iomanip>
#include <iostream>

using namespace std::chrono_literals;
namespace bip = boost::interprocess;
using boost::asio::ip::tcp;
using boost::system::error_code;
using Queue = boost::interprocess::message_queue;

static constexpr auto MAX_MESG_LEN = 100;
static constexpr auto MAX_MESGS = 10;

struct Message {
    using Len = boost::endian::big_uint32_t;

    struct header_t {
        Len len;
    };
    static const auto header_length = sizeof(header_t);
    std::array<char, MAX_MESG_LEN + header_length> buf;

    char const* data() const { return buf.data();             } 
    char*       data()       { return buf.data();             } 
    char const* body() const { return data() + header_length; } 
    char*       body()       { return data() + header_length; }

    static_assert(std::is_standard_layout_v<header_t> and
                  std::is_trivial_v<header_t>);

    Len body_length() const     { return std::min(h().len, max_body_length()); } 
    Len max_body_length() const { return buf.max_size() - header_length;       } 
    bool decode_header()        { return h().len <= max_body_length();         } 

    bool set_body(std::string_view value) {
        assert(value.length() <= max_body_length());
        h().len = value.length();
        std::copy_n(value.begin(), body_length(), body());

        return (value.length() == body_length()); // not truncated
    }

  private:
    header_t&       h()       { return *reinterpret_cast<header_t*>(data());       } 
    header_t const& h() const { return *reinterpret_cast<header_t const*>(data()); }
};

struct Session : std::enable_shared_from_this<Session> {
    Session(tcp::socket&& s) : socket_(std::move(s)) {}

    void start() {
        post(strand_,
             [ this, self = shared_from_this() ] { do_read_header(); });
    }

private:
    using Strand = boost::asio::strand<tcp::socket::executor_type>;
    using Timer  = boost::asio::steady_timer;

    tcp::socket socket_{strand_};
    Strand      strand_{make_strand(socket_.get_executor())};
    Message     res;
    Queue       request_queue_{bip::open_or_create, "SendQueue", MAX_MESGS, MAX_MESG_LEN};
    Timer       recv_deadline_{strand_};

    void do_read_header() {
        auto self(shared_from_this());
        std::cout << "do_read_header: " << res.header_length << std::endl;
        recv_deadline_.expires_from_now(5s);
        recv_deadline_.async_wait([ self, this ](error_code ec) {
            if (!ec) {
                std::cerr << "header timeout" << std::endl;
                socket_.cancel();
            }
        });

        boost::asio::async_read(
            socket_, boost::asio::buffer(res.data(), res.header_length),
            [ this, self ](error_code ec, size_t /*length*/) {
                std::cerr << "header: " << ec.message() << std::endl;
                recv_deadline_.cancel();
                if (!ec && res.decode_header()) {
                    do_read_body();
                } else {
                    socket_.shutdown(tcp::socket::shutdown_both);
                }
            });
    }

    void do_read_body() {
        auto self(shared_from_this());
        // Message msg;
        std::cout << "do_read_body: " << res.body_length() << std::endl;

        recv_deadline_.expires_from_now(1s);
        recv_deadline_.async_wait([self, this] (error_code ec) {
            if (!ec) {
                std::cerr << "body timeout" << std::endl;
                socket_.cancel();
            }
        });

        boost::asio::async_read(
            socket_,
            boost::asio::buffer(res.body(), res.body_length()),
            boost::asio::transfer_exactly(res.body_length()),
            [ this, self ](error_code ec, std::size_t length) {
                std::cerr << "body: " << ec.message() << std::endl;
                recv_deadline_.cancel();
                if (!ec) {
                    try {
                        // Not safe to print unless NUL-terminated, see e.g.
                        // https://stackoverflow.com/questions/66278813/boost-deadline-timer-causes-stack-buffer-overflow/66279497#66279497
                        if (length)
                            request_queue_.send(res.body(), res.body_length(), 0);
                    } catch (const std::exception& ex) {
                        std::cout << ex.what() << std::endl;
                    }
                    do_read_header();
                } else {
                    socket_.shutdown(tcp::socket::shutdown_both);
                }
            });
    }
};

class Server {
  public:
    Server(boost::asio::io_service& io_service, short port)
        : acceptor_(io_service, tcp::endpoint(tcp::v4(), port)),
          socket_(io_service) {
        do_accept();
    }

  private:
    void do_accept() {
        acceptor_.async_accept(socket_, [ this ](error_code ec) {
            std::cerr << "async_accept: " << ec.message() << std::endl;
            if (!ec) {
                std::cerr << "session: " << socket_.remote_endpoint() << std::endl;
                std::make_shared<Session>(std::move(socket_))->start();
            }

            do_accept();
        });
    }

    tcp::acceptor acceptor_;
    tcp::socket   socket_;
};

int main(int argc, char**) {
    Queue queue{bip::open_or_create, "SendQueue", MAX_MESGS, MAX_MESG_LEN}; // ensure it exists

    if (argc == 1) {
        boost::asio::io_context ioc;
        Server s(ioc, 8989);

        ioc.run_for(10s);
    } else {
        while (true) {
            using Buf = std::array<char, MAX_MESG_LEN>;
            Buf      buf;
            unsigned prio;
            size_t   n;
            queue.receive(buf.data(), buf.size(), n, prio);

            std::cout << "Received: " << std::quoted(std::string_view(buf.data(), n)) << std::endl;
        }
    }
}

Testable with

./sotest

In another terminal:

./sotest consumer

And somewhere else e.g. some requests that don't timeout:

for msg in '0000 0000' '0000 0001 31' '0000 000c 6865 6c6c 6f20 776f 726c 640a'
do
    xxd -r -p <<< "$msg" |
        netcat localhost 8989 -w 1
done

Or, multi-request on single session, then session times out (-w 6 exceeds 5s):

msg='0000 0000 0000 0001 31 0000 000c 6865 6c6c 6f20 776f 726c 640a'; xxd -r -p <<< "$msg"| netcat localhost 8989 -w 6

enter image description here

sehe
  • 328,274
  • 43
  • 416
  • 565
  • Added a GIF of a live demo, because no online compilers do IPC message queues – sehe Mar 10 '21 at 17:53
  • thank you very mach. In my code I have alos `do_write` that read from the message queue and `async_write` it to the client - but what you answer me help me the solved it also :). – mosh Mar 14 '21 at 15:36
0

I had to solve a similar issue when implementing the Serial Port wrapper and this is the simplified version of the code:

// Possible outcome of a read. Set by callback
enum class ReadResult
{
    ResultInProgress,
    ResultSuccess,
    ResultError,
    ResultTimeout
};

std::streamsize read(char* s, std::streamsize n, boost::posix_time::time_duration timeout)
{

    boost::asio::io_service io;
    boost::asio::serial_port port(io);
    // result is atomic to avoid race condition
    std::atomic<ReadResult> result(ReadResult::ResultInProgress); 
    std::streamsize bytesTransferred = 0;

    // Create async timer that fires after duration 
    boost::asio::deadline_timer timer(io);
    timer.expires_from_now(timeout);
    timer.async_wait(boost::bind([&result](const boost::system::error_code& error){
    // If there wasn't any error and reading is still in progress, set result as timeout        
        if (!error && result == ReadResult::ResultInProgress) 
            result = ReadResult::ResultTimeout;
    },boost::asio::placeholders::error));

    // Read asynchronously
    port.async_read_some(boost::asio::buffer(s, n), boost::bind([&result,&bytesTransferred](const boost::system::error_code& error,
    const size_t transferred){
        // If there wasn't any error on read finish set result as sucess else as error
        if (!error){
             result = ReadResult::ResultSuccess;
             bytesTransferred = transferred;
             return;
        }
        result = ReadResult::ResultError;
    },boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));

    // Run loop until timeout, error or success occurs
    for (;;)
    {
        io.run_one();
        switch (result)
        {
        case ReadResult::ResultSuccess:
            // Success, cancel timer and return amount of bytes read
            timer.cancel();
            return bytesTransferred;
        case ReadResult::ResultTimeout:
            // Timeout occured, cancel read and throw exception
            port.cancel();
            throw(TimeoutException("Timeout expired"));
        case ReadResult::ResultError:
            // Error occured, cancel read and timer and throw exception
            port.cancel();
            timer.cancel();
            throw(std::ios_base::failure("Error while reading"));
        default:
            //if result is still in progress remain in the loop
            break;
        }
    }
}

So basically, what you have to do is :

  1. initialize timer with io_service
  2. call async_wait on a timer with a callback function that sets result timeout flag
  3. call async_read on your socket with a callback that sets result success flag
  4. loop until result is no longer "InProgress", note that io.run_one() in loop is important
  5. handle result the result

you could use it for any asynchronous function

Yatin
  • 2,348
  • 6
  • 20
  • 38