How to asynchronously read to std::string using Boost::asio?
Solution 1
The Boost.Asio documentation states:
A buffer object represents a contiguous region of memory as a 2-tuple consisting of a pointer and size in bytes. A tuple of the form {void*, size_t} specifies a mutable (modifiable) region of memory.
This means that in order for a call to async_read
to write data to a buffer, it must be (in the underlying buffer object) a contiguous block of memory. Additionally, the buffer object must be able to write to that block of memory.
std::string
does not allow arbitrary writes into its buffer, so async_read
cannot write chunks of memory into a string's buffer (note that std::string
does give the caller read-only access to the underlying buffer via the data()
method, which guarantees that the returned pointer will be valid until the next call to a non-const member function. For this reason, Asio can easily create a const_buffer
wrapping an std::string
, and you can use it with async_write
).
The Asio documentation has example code for a simple "chat" program (see http://www.boost.org/doc/libs/1_43_0/doc/html/boost_asio/examples.html#boost_asio.examples.chat) that has a good method of overcoming this problem. Basically, you need to have the sending TCP send along the size of a message first, in a "header" of sorts, and your read handler must interpret the header to allocate a buffer of a fixed size suitable for reading the actual data.
As far as the need for using shared_from_this()
in async_read
and async_write
, the reason is that it guarantees that the method wrapped by boost::bind
will always refer to a live object. Consider the following situation:
- Your
handle_accept
method callsasync_read
and sends a handler "into the reactor" - basically you've asked theio_service
to invokeConnection::handle_user_read
when it finishes reading data from the socket. Theio_service
stores this functor and continues its loop, waiting for the asynchronous read operation to complete. - After your call to
async_read
, theConnection
object is deallocated for some reason (program termination, an error condition, etc.) - Suppose the
io_service
now determines that the asynchronous read is complete, after theConnection
object has been deallocated but before theio_service
is destroyed (this can occur, for example, ifio_service::run
is running in a separate thread, as is typical). Now, theio_service
attempts to invoke the handler, and it has an invalid reference to aConnection
object.
The solution is to allocate Connection
via a shared_ptr
and use shared_from_this()
instead of this
when sending a handler "into the reactor" - this allows io_service
to store a shared reference to the object, and shared_ptr
guarantees that it won't be deallocated until the last reference expires.
So, your code should probably look something like:
class Connection : public boost::enable_shared_from_this<Connection>
{
public:
Connection(tcp::acceptor &acceptor) :
acceptor_(acceptor),
socket_(acceptor.get_io_service(), tcp::v4())
{ }
void start()
{
acceptor_.get_io_service().post(
boost::bind(&Connection::start_accept, shared_from_this()));
}
private:
void start_accept()
{
acceptor_.async_accept(socket_,
boost::bind(&Connection::handle_accept, shared_from_this(),
placeholders::error));
}
void handle_accept(const boost::system::error_code& err)
{
if (err)
{
disconnect();
}
else
{
async_read(socket_, boost::asio::buffer(user_),
boost::bind(&Connection::handle_user_read, shared_from_this(),
placeholders::error, placeholders::bytes_transferred));
}
}
//...
};
Note that you now must make sure that each Connection
object is allocated via a shared_ptr
, e.g.:
boost::shared_ptr<Connection> new_conn(new Connection(...));
Hope this helps!
Solution 2
This isn't intended to be an answer per se, but just a lengthy comment: a very simple way to convert from an ASIO buffer to a string is to stream from it:
asio::streambuf buff;
asio::read_until(source, buff, '\r'); // for example
istream is(&buff);
is >> targetstring;
This is a data copy, of course, but that's what you need to do if you want it in a string.
Solution 3
You can use a std:string
with async\_read()
like this:
async_read(socket_, boost::asio::buffer(&user_[0], user_.size()),
boost::bind(&Connection::handle_user_read, this,
placeholders::error, placeholders::bytes_transferred));
However, you'd better make sure that the std::string
is big enough to accept the packet that you're expecting and padded with zeros before calling async\_read()
.
And as for why you should NEVER bind a member function callback to a this
pointer if the object can be deleted, a more complete description and a more robust method can be found here: Boost async_* functions and shared_ptr's.
Solution 4
Boost Asio has two styles of buffers. There's boost::asio::buffer(your_data_structure)
, which cannot grow, and is therefore generally useless for unknown input, and there's boost::asio::streambuf
which can grow.
Given a boost::asio::streambuf buf
, you turn it into a string with std::string(std::istreambuf_iterator<char>(&buf), {});
.
This is not efficient as you end up copying data once more, but that would require making boost::asio::buffer
aware of growable containers, i.e. containers that have a .resize(N)
method. You can't make it efficient without touching Boost code.
Related videos on Youtube
Comments
-
SpyBot about 3 years
I'm learning Boost::asio and all that async stuff. How can I asynchronously read to variable
user_
of type std::string?Boost::asio::buffer(user_)
works only withasync_write()
, but not withasync_read()
. It works with vector, so what is the reason for it not to work with string? Is there another way to do that besides declaringchar user_[max_len]
and usingBoost::asio::buffer(user_, max_len)
?Also, what's the point of inheriting from
boost::enable_shared_from_this<Connection>
and usingshared_from_this()
instead ofthis
inasync_read()
andasync_write()
? I've seen that a lot in the examples.Here is a part of my code:
class Connection { public: Connection(tcp::acceptor &acceptor) : acceptor_(acceptor), socket_(acceptor.get_io_service(), tcp::v4()) { } void start() { acceptor_.get_io_service().post( boost::bind(&Connection::start_accept, this)); } private: void start_accept() { acceptor_.async_accept(socket_, boost::bind(&Connection::handle_accept, this, placeholders::error)); } void handle_accept(const boost::system::error_code& err) { if (err) { disconnect(); } else { async_read(socket_, boost::asio::buffer(user_), boost::bind(&Connection::handle_user_read, this, placeholders::error, placeholders::bytes_transferred)); } } void handle_user_read(const boost::system::error_code& err, std::size_t bytes_transferred) { if (err) { disconnect(); } else { ... } } ... void disconnect() { socket_.shutdown(tcp::socket::shutdown_both); socket_.close(); socket_.open(tcp::v4()); start_accept(); } tcp::acceptor &acceptor_; tcp::socket socket_; std::string user_; std::string pass_; ... };
-
MSalters almost 5 yearsThis is the correct answer. Boost.Asio can read from many buffer types, and write to many fixed-size buffers, but
asio::streambuf
is the only variable-size buffer it will work with. As MikeC correctly notes, this is a data copy, butstd::string
has internal copies when it grows anyway. Note that the extraction methodoperator>>
is the usual whitespace-parsing method.from iostream. You can also callgetline
on theistream
.