Read and write data properly, part 3
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:
- If the remote peer has closed the connection. In this case
boost::asio::error::eof
is generated; - If the input buffer is full (reading);
- 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?