Read and write data properly, part 2

174

Remember tcp::socket::async_connect member function which accepts an endpoint as a parameter and free function boost::asio::async_connect which works with a range of endpoints. It's a sort of common practice in Boost.Asio when a free function is a higher-level generalization of some existing functionality offered via member functions. There is also such a generalization for read and write functions.

Free functions for I/O operations are build on top of socket member functions and offer more convenient and customizable behavior. Functions we're going to talk about are: boost::asio::async_read, boost::asio::async_read_until and boost::asio::async_write.

Buffer sequences

Just like socket's I/O member functions, free functions work with buffer sequences.

Dynamic buffers and streambuf

In addition, these functions work with dynamic buffers. Internally they implement a chain of asynchronous operations dealing with buffer's prepare and commit when read data, and consume when write. Let's take a look once again.

boost::asio::async_read: it does prepare and commit, you have to do consume:

// streambuf::prepare and streambuf::commit are dealing with inside boost::asio::async_read
boost::asio::async_read(socket, streambuf, [&] (error_code error, std::size_t bytes_transferred)
{
    // Read the data received
    auto data = streambuf.data();

    // Consume it after
    streambuf.consume(bytes_transferred);
});

boost::asio::async_write: you do prepare and commit, it does consume:

// streambuf::consume is dealing with inside boost::asio::async_write
auto view = streambuf.prepare(1024);

// Fill the buffer view with some data
std::memcpy(view.data(), source, 1024);

// Move the data to the output sequence
streambuf.commit(1024);

// streambuf::consume is made inside boost::asio::async_write
boost::asio::async_write(socket, streambuf, [&] (error_code error, std::size_t bytes_transferred)
{
    // No need to call consume
});

Completion conditions

As you remember, socket I/O member functions may return even if the data was delivered partially. So dealing with member functions you have to implement a chain of I/O function calls to ensure that all data has been delivered.

Free functions offer a convenient way to control such a delivery without call chain on the user's side. To specify the delivery behavior you should pass an additional parameter into these functions which is called “Completion condition”.

There are three completion conditions provided by Boost.Asio: transfer_all, transfer_at_least and transfer_exactly. Let's take a look at the examples:

boost::asio::async_write(socket, streambuf, boost::asio::transfer_all(), [&] (error_code error, std::size_t bytes_transferred)
{
    // All data has been sent
});

boost::asio::async_read(socket, streambuf, boost::asio::transfer_at_least(32), [&] (error_code error, std::size_t bytes_transferred)
{
    // 32 bytes or more has been received
});

boost::asio::async_read(socket, streambuf, boost::asio::transfer_exactly(32), [&] (error_code error, std::size_t bytes_transferred)
{
    // Exactly 32 bytes has been received
});

If you don't specify a completion condition then transfer_all is default behavior.

Completion condition is a function object. You can implement your own completion condition in such a way:

auto my_completion_condition = [&] (boost::system::error_code error, std::size_t bytes_transferred) -> std::size_t
{
    if(error)
    {
        return 0;
    }
    else
    {
        return total_size - bytes_transferred;
    }
};

boost::asio::async_write(socket, streambuf, my_completion_condition, [&] (error_code error, std::size_t bytes_transferred)
{
    // All data has been sent
});

The first parameter (error) of a completion condition is the result of the last underlying socket function call (send or receive). The second parameter (bytes_transferred) is a total amount of bytes transferred so far (not the last socket send or receive but the whole chain). Completion condition should return 0 if the operation has been completed. Non-zero return value means the operation is still in progress and the value is the amount of bytes which still should be transferred.

async_read_until

This function is useful when it's more convenient to determine a completion condition basing on the content of the data received rather than on the amount of bytes transferred. There are several overloads provided by Boost.Asio. We've already seen one of them in the earlier lessons. Now let's look at all of them:

// Read the data from the socket until we find \n character
boost::asio::async_read_until(socket, streambuf, '\n', [&] (error_code error, std::size_t bytes_transferred)
{
    // Character \n has been found
});

// Read the data from the socket until we find "-----END CERTIFICATE-----" string
boost::asio::async_read_until(socket, streambuf, "-----END CERTIFICATE-----", [&] (error_code error, std::size_t bytes_transferred)
{
    // String "-----END CERTIFICATE-----" has been found
});

// Read the data from the socket until the data match the gived regular expression
boost::asio::async_read_until(socket, streambuf, boost::regex("[0-9]+"), [&] (error_code error, std::size_t bytes_transferred)
{
    // The given regexp has been matched
});

You can also provide your own match condition. Match condition is a function object. It accepts a range of iterators where you should search for the match condition. It returns a pair of iterator and bool. If the bool is false then the given range doesn't match your condition. If it's true then the iterator should point to the next element after the matched one.

using iterator = boost::asio::buffers_iterator<boost::asio::streambuf::const_buffers_type>;

auto my_match = [&] (iterator begin, iterator end) -> std::pair<iterator, bool>
{
    for(auto it = begin; it != end;)
    {
        if(*it++ == '\n')
        {
            return {it, true};
        }
    }

    return {end, false};
};

// Read the data from the socket until we find \n character
boost::asio::async_read_until(socket, streambuf, my_match, [&] (error_code error, std::size_t bytes_transferred)
{
    // Character \n has been found
});

So, dealing with Boost.Asio I/O free functions you should decide:

  1. What type of buffers is more convenient for you, static, dynamic or both?
  2. How will you control the delivery, basing on the amount of data transferred or on its content?

Oh, that's a big lesson, so let's stop while it didn't grow even bigger! In the next lesson I'll give you several tips on dealing with I/O free functions.

Share this page:

Learning plan

How to work with Boost.Asio dynamic buffers manually
Let's briefly summarize everything we've learned about different Boost.Asio buffers
How to deal with read and write functions properly to gain desired I/O behavior
26. Read and write data properly, part 2
How to deal with Boost.Asio I/O free functions: async_read, async_read_until and async_write
Several additional tips on dealing with Boost.Asio I/O free functions
How to deal with secure connections with Boost.Asio and OpenSSL
A short break before we go into Boost.Asio application design principles