While the read_until()
operations commit all data read into the streambuf's input sequence, they return a bytes_transferred
value containing the number of bytes up to and including the first delimiter. In essence, it provides the size of the frame, and one can limit istream
to only read a portion of the streambuf
's input sequence by either:
- Using a custom
istream
that limits the amount of bytes read from a streambuf. One of the easier ways to accomplish this is to use Boost.IOStream's boost::iostreams::stream
and implement a model of the Source concept.
- Create a custom
streambuf
that derives from Boost.Asio's streambuf
. To limit the amount of bytes read from the available input sequence, custom functions will need to manipulate the end of the input sequence. Furthermore, the custom streambuf
will need to handle underflow.
Custom Source
for Boost.IOStream
Boost.IOStream's boost::iostreams::stream
object delegates I/O operations to a Device. The Device is user code that implements a model of various Boost.IOStream concepts. In this case, the Source concept, which provides read-access to a sequence of characters, is the only one needed. Furthermore, when the boost::iostreams::stream
uses a Source Device, it will inherit from std::basic_istream
.
In the following code, asio_streambuf_input_device
is a model of the Source concept that reads from a Boost.Asio streambuf. When a given amount of bytes have been read, asio_streambuf_input_device
indicates underflow even if the underlying streambuf still has data in its input sequence.
/// Type that implements a model of the Boost.IOStream's Source concept
/// for reading data from a Boost.Asio streambuf
class asio_streambuf_input_device
: public boost::iostreams::source // Use convenience class.
{
public:
explicit
asio_streambuf_input_device(
boost::asio::streambuf& streambuf,
std::streamsize bytes_transferred
)
: streambuf_(streambuf),
bytes_remaining_(bytes_transferred)
{}
std::streamsize read(char_type* buffer, std::streamsize buffer_size)
{
// Determine max amount of bytes to copy.
auto bytes_to_copy =
std::min(bytes_remaining_, std::min(
static_cast<std::streamsize>(streambuf_.size()), buffer_size));
// If there is no more data to be read, indicate end-of-sequence per
// Source concept.
if (0 == bytes_to_copy)
{
return -1; // Indicate end-of-sequence, per Source concept.
}
// Copy from the streambuf into the provided buffer.
std::copy_n(buffers_begin(streambuf_.data()), bytes_to_copy, buffer);
// Update bytes remaining.
bytes_remaining_ -= bytes_to_copy;
// Consume from the streambuf.
streambuf_.consume(bytes_to_copy);
return bytes_to_copy;
}
private:
boost::asio::streambuf& streambuf_;
std::streamsize bytes_remaining_;
};
// ...
// Create a custom iostream that sets a limit on the amount of bytes
// that will be read from the streambuf.
boost::iostreams::stream<asio_streambuf_input_device> input(streambuf, n);
parse(input);
Here is a complete example demonstrating this approach:
#include <functional>
#include <iostream>
#include <string>
#include <boost/asio.hpp>
#include <boost/iostreams/concepts.hpp> // boost::iostreams::source
#include <boost/iostreams/stream.hpp>
/// Type that implements a model of the Boost.IOStream's Source concept
/// for reading data from a Boost.Asio streambuf
class asio_streambuf_input_device
: public boost::iostreams::source // Use convenience class.
{
public:
explicit
asio_streambuf_input_device(
boost::asio::streambuf& streambuf,
std::streamsize bytes_transferred
)
: streambuf_(streambuf),
bytes_remaining_(bytes_transferred)
{}
std::streamsize read(char_type* buffer, std::streamsize buffer_size)
{
// Determine max amount of bytes to copy.
auto bytes_to_copy =
std::min(bytes_remaining_, std::min(
static_cast<std::streamsize>(streambuf_.size()), buffer_size));
// If there is no more data to be read, indicate end-of-sequence per
// Source concept.
if (0 == bytes_to_copy)
{
return -1; // Indicate end-of-sequence, per Source concept.
}
// Copy from the streambuf into the provided buffer.
std::copy_n(buffers_begin(streambuf_.data()), bytes_to_copy, buffer);
// Update bytes remaining.
bytes_remaining_ -= bytes_to_copy;
// Consume from the streambuf.
streambuf_.consume(bytes_to_copy);
return bytes_to_copy;
}
private:
boost::asio::streambuf& streambuf_;
std::streamsize bytes_remaining_;
};
/// @brief Convert a streambuf to a string.
std::string make_string(boost::asio::streambuf& streambuf)
{
return std::string(buffers_begin(streambuf.data()),
buffers_end(streambuf.data()));
}
// This example is not interested in the handlers, so provide a noop function
// that will be passed to bind to meet the handler concept requirements.
void noop() {}
int main()
{
using boost::asio::ip::tcp;
boost::asio::io_service io_service;
// Create all I/O objects.
tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), 0));
tcp::socket server_socket(io_service);
tcp::socket client_socket(io_service);
// Connect client and server sockets.
acceptor.async_accept(server_socket, std::bind(&noop));
client_socket.async_connect(acceptor.local_endpoint(), std::bind(&noop));
io_service.run();
// Write to client.
const std::string message =
"12@"
"345@";
write(server_socket, boost::asio::buffer(message));
boost::asio::streambuf streambuf;
{
auto bytes_transferred = read_until(client_socket, streambuf, '@');
// Verify that the entire message "12@345@" was read into
// streambuf's input sequence.
assert(message.size() == streambuf.size());
std::cout << "streambuf contains: " << make_string(streambuf) <<
std::endl;
// Create a custom iostream that sets a limit on the amount of bytes
// that will be read from the streambuf.
boost::iostreams::stream<asio_streambuf_input_device> input(
streambuf, bytes_transferred);
int data = 0;
input >> data; // Consumes "12" from input sequence.
assert(data == 12);
std::cout << "Extracted: " << data << std::endl;
assert(!input.eof());
input.get(); // Consume "@" from input sequence.
assert(!input.eof());
input.get(); // No more data available.
assert(input.eof());
std::cout << "istream has reached EOF" << std::endl;
}
std::cout << "streambuf contains: " << make_string(streambuf) <<
std::endl;
{
// As the streambuf's input sequence already contains the delimiter,
// this operation will not actually attempt to read data from the
// socket.
auto bytes_transferred = read_until(client_socket, streambuf, '@');
// Create a custom iostream that sets a limit on the amount of bytes
// that will be read from the streambuf.
boost::iostreams::stream<asio_streambuf_input_device> input(
streambuf, bytes_transferred);
std::string data;
getline(input, data, '@'); // Consumes delimiter.
assert(data == "345");
std::cout << "Extracted: " << data << std::endl;
assert(!input.eof());
input.get(); // Underflow.
assert(input.eof());
std::cout << "istream has reached EOF" << std::endl;
}
assert(streambuf.size() == 0);
std::cout << "streambuf is empty" << std::endl;
}
Output:
streambuf contains: 12@345@
Extracted: 12
istream has reached EOF
streambuf contains: 345@
Extracted: 345
istream has reached EOF
streambuf is empty
Derive from boost::asio::streambuf
One can safely derive from the Boost.Asio's streambuf
and implement custom behaviors. In this case, the goal is to limit the amount of bytes an istream
can extract from the input sequence before causing underflow. This can be accomplish by:
- Updating the streambuf's get-area (input sequence) pointers so it only contains the desired amount of bytes to be read. This is accomplished by setting the end of the get-area pointer (
egptr
) to be n
bytes after the current character get-area pointer (gptr
). In the code below, I refer to this as framing.
- Handling a
underflow()
. If the end of the current frame has been reached, then return EOF
.
/// @brief Type that derives from Boost.Asio streambuf and can frame the
/// input sequence to a portion of the actual input sequence.
template <typename Allocator = std::allocator<char> >
class basic_framed_streambuf
: public boost::asio::basic_streambuf<Allocator>
{
private:
typedef boost::asio::basic_streambuf<Allocator> parent_type;
public:
explicit
basic_framed_streambuf(
std::size_t maximum_size = (std::numeric_limits< std::size_t >::max)(),
const Allocator& allocator = Allocator()
)
: parent_type(maximum_size, allocator),
egptr_(nullptr)
{}
/// @brief Limit the current input sequence to n characters.
///
/// @remark An active frame is invalidated by any member function that
/// modifies the input or output sequence.
void frame(std::streamsize n)
{
// Store actual end of input sequence.
egptr_ = this->egptr();
// Set the input sequence end to n characters from the current
// input sequence pointer..
this->setg(this->eback(), this->gptr(), this->gptr() + n);
}
/// @brief Restore the end of the input sequence.
void unframe()
{
// Restore the end of the input sequence.
this->setg(this->eback(), this->gptr(), this->egptr_);
egptr_ = nullptr;
}
protected:
// When the end of the input sequence has been reached, underflow
// will be invoked.
typename parent_type::int_type underflow()
{
// If the streambuf is currently framed, then return eof
// on underflow. Otherwise, defer to the parent implementation.
return egptr_ ? parent_type::traits_type::eof()
: parent_type::underflow();
}
private:
char* egptr_;
};
// ...
basic_framed_streambuf<> streambuf;
// ....
streambuf.frame(n);
std::istream input(&streambuf);
parse(input);
streambuf.unframe();
Here is a complete example demonstrating this approach:
#include <functional>
#include <iostream>
#include <string>
#include <boost/asio.hpp>
/// @brief Type that derives from Boost.Asio streambuf and can frame the
/// input sequence to a portion of the actual input sequence.
template <typename Allocator = std::allocator<char> >
class basic_framed_streambuf
: public boost::asio::basic_streambuf<Allocator>
{
private:
typedef boost::asio::basic_streambuf<Allocator> parent_type;
public:
explicit
basic_framed_streambuf(
std::size_t maximum_size = (std::numeric_limits< std::size_t >::max)(),
const Allocator& allocator = Allocator()
)
: parent_type(maximum_size, allocator),
egptr_(nullptr)
{}
/// @brief Limit the current input sequence to n characters.
///
/// @remark An active frame is invalidated by any member function that
/// modifies the input or output sequence.
void frame(std::streamsize n)
{
// Store actual end of input sequence.
egptr_ = this->egptr();
// Set the input sequence end to n characters from the current
// input sequence pointer..
this->setg(this->eback(), this->gptr(), this->gptr() + n);
}
/// @brief Restore the end of the input sequence.
void unframe()
{
// Restore the end of the input sequence.
this->setg(this->eback(), this->gptr(), this->egptr_);
egptr_ = nullptr;
}
protected:
// When the end of the input sequence has been reached, underflow
// will be invoked.
typename parent_type::int_type underflow()
{
// If the streambuf is currently framed, then return eof
// on underflow. Otherwise, defer to the parent implementation.
return egptr_ ? parent_type::traits_type::eof()
: parent_type::underflow();
}
private:
char* egptr_;
};
typedef basic_framed_streambuf<> framed_streambuf;
/// @brief RAII type that helps frame a basic_framed_streambuf within a
/// given scope.
template <typename Streambuf>
class streambuf_frame
{
public:
explicit streambuf_frame(Streambuf& streambuf, std::streamsize n)
: streambuf_(streambuf)
{
streambuf_.frame(n);
}
~streambuf_frame() { streambuf_.unframe(); }
streambuf_frame(const streambuf_frame&) = delete;
streambuf_frame& operator=(const streambuf_frame&) = delete;
private:
Streambuf& streambuf_;
};
/// @brief Convert a streambuf to a string.
std::string make_string(boost::asio::streambuf& streambuf)
{
return std::string(buffers_begin(streambuf.data()),
buffers_end(streambuf.data()));
}
// This example is not interested in the handlers, so provide a noop function
// that will be passed to bind to meet the handler concept requirements.
void noop() {}
int main()
{
using boost::asio::ip::tcp;
boost::asio::io_service io_service;
// Create all I/O objects.
tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), 0));
tcp::socket server_socket(io_service);
tcp::socket client_socket(io_service);
// Connect client and server sockets.
acceptor.async_accept(server_socket, std::bind(&noop));
client_socket.async_connect(acceptor.local_endpoint(), std::bind(&noop));
io_service.run();
// Write to client.
const std::string message =
"12@"
"345@";
write(server_socket, boost::asio::buffer(message));
framed_streambuf streambuf;
// Demonstrate framing the streambuf's input sequence manually.
{
auto bytes_transferred = read_until(client_socket, streambuf, '@');
// Verify that the entire message "12@345@" was read into
// streambuf's input sequence.
assert(message.size() == streambuf.size());
std::cout << "streambuf contains: " << make_string(streambuf) <<
std::endl;
// Frame the streambuf based on bytes_transferred. This is all data
// up to and including the first delimiter.
streambuf.frame(bytes_transferred);
// Use an istream to read data from the currently framed streambuf.
std::istream input(&streambuf);
int data = 0;
input >> data; // Consumes "12" from input sequence.
assert(data == 12);
std::cout << "Extracted: " << data << std::endl;
assert(!input.eof());
input.get(); // Consume "@" from input sequence.
assert(!input.eof());
input.get(); // No more data available in the frame, so underflow.
assert(input.eof());
std::cout << "istream has reached EOF" << std::endl;
// Restore the streambuf.
streambuf.unframe();
}
// Demonstrate using an RAII helper to frame the streambuf's input
// sequence.
{
// As the streambuf's input sequence already contains the delimiter,
// this operation will not actually attempt to read data from the
// socket.
auto bytes_transferred = read_until(client_socket, streambuf, '@');
std::cout << "streambuf contains: " << make_string(streambuf) <<
std::endl;
// Frame the streambuf based on bytes_transferred. This is all data
// up to and including the first delimiter. Use a frame RAII object
// to only frame the streambuf within the current scope.
streambuf_frame<framed_streambuf> frame(streambuf, bytes_transferred);
// Use an istream to read data from the currently framed streambuf.
std::istream input(&streambuf);
std::string data;
getline(input, data, '@'); // Consumes delimiter.
assert(data == "345");
std::cout << "Extracted: " << data << std::endl;
assert(!input.eof());
input.get(); // No more data available in the frame, so underflow.
assert(input.eof());
std::cout << "istream has reached EOF" << std::endl;
// The frame object's destructor will unframe the streambuf.
}
assert(streambuf.size() == 0);
std::cout << "streambuf is empty" << std::endl;
}
Output:
streambuf contains: 12@345@
Extracted: 12
istream has reached EOF
streambuf contains: 345@
Extracted: 345
istream has reached EOF
streambuf is empty