Read and write data properly, part 3

154

streambuf maximum size

When using streambuf, you should specify its maximum size in constructor. Default maximum size is defined as this, and, as you see, it's not very useful:

basic_streambuf(std::size_t maximum_size = std::numeric_limits<std::size_t>::max(), ...)

Receiving data, streambuf won't grow beyond its maximum size.

boost::asio::streambuf streambuf(16);

boost::asio::async_read(socket, streambuf, [&] (boost::asio::error_code error, std::size_t bytes_transferred)
{
    std::cout << error.message() << ", bytes transferred: " << bytes_transferred << "\n";
});
The operation completed successfully, bytes transferred: 16

Notice the difference between async_read and async_read_until when they bump into the streambuf maximum size limit:

boost::asio::streambuf streambuf(16);

boost::asio::async_read_until(socket, streambuf, '\n', [&] (boost::asio::error_code error, std::size_t bytes_transferred)
{
    std::cout << error.message() << ", bytes transferred: " << bytes_transferred << "\n";
});
Element not found, bytes transferred: 0

async_read_until generate boost::asio::error::not_found error while async_read doesn't treat it as an error.

Dynamic buffer maximum size

Just like the case of streambuf, working with dynamic buffers you can (and usually should) specify its maximum allowed size. Pass maximum allowed size with second argument of dynamic_buffer function:

std::vector<char> buffer;

boost::asio::async_read_until(socket, boost::asio::dynamic_buffer(buffer, 16), '\n', [&] (boost::asio::error_code error, std::size_t bytes_transferred)
{
    std::cout << error.message() << ", bytes transferred: " << bytes_transferred << "\n";
});

Completing the operation

Besides completion condition triggering, async_read, async_read_until and async_write will complete in these additional cases:

  1. If the remote peer has closed the connection. In this case boost::asio::error::eof is generated;
  2. If the input buffer is full (reading);
  3. If you close the socket during the operation. In this case platform-depended error is generated. On Windows systems it's ERROR_CONNECTION_ABORTED.

It is safe to cancel asynchronous operation by closing a socket as long as you do it in the same thread (single threaded environment) or within the same strand (multithreaded environment).

prepare, commit and consume complexity

Boost.Asio streambuf::prepare in some cases may move some part of internal data, so its complexity should be considered as O(n) where n is amount of bytes currently stored in the underlying buffer. Also streambuf may reallocate the underlying buffer so the whole buffer is moved in that case, which is still O(n), however n now means total amount of stored and prepared bytes. commit and consume are O(1) because its just a pointer arithmetics.

Dynamic buffers, on the other hand, has O(1) for prepare and commit, however consume is implemented by range-based erase function, which in both cases for std::vector and std::string mean O(n). And of course, prepare still may lead to reallocation of underlying memory, which gives O(n).

Buffers integrity

Buffer views are non-owning entities, so you should keep the underlying memory alive as long as the buffer is in use.

In the same way, dealing with dynamic buffers or streambuf you shouldn't touch them with functions which may lead to memory reallocation if some asynchronous operation working with those buffers is still in progress:

std::vector<char> input, output;
// ...
boost::asio::async_write(socket, io::dynamic_buffer(output), some_handler);

boost::asio::async_read(socket, io::dynamic_buffer(input), [&] (boost::asio::error_code error, std::size_t bytes_transferred)
{
    auto view = output.prepare(bytes_transferred); // This is bad
    boost::buffers_copy(view, input.data());
    output.commit(bytes_transferred);
});

In the example above we didn't check if async_write operation is still in progress, and output.prepare function call may lead to reallocation of the underlying buffer and invalidation of iterators or pointers which are currently in use somewhere inside async_write.

And even if you reserve required amount of memory and limit maximum dynamic buffer size properly, that wouldn't be enough. Boost.Asio documentation says that results of prepare and data member functions are invalidated when you modify input or output sequence (for streambuf), or resize or erase underlying container as well as call prepare, commit or consume (for vector and string dynamic buffers). So just don't touch currently occupied buffers, mkay?

Lesson 28
Share this page:

Learning plan

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
How to deal with Boost.Asio I/O free functions: async_read, async_read_until and async_write
27. Read and write data properly, part 3
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
A short notes on Boost.Asio server application quality issues