How to asynchronously read to std::string using Boost::asio?

28,735

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:

  1. Your handle_accept method calls async_read and sends a handler "into the reactor" - basically you've asked the io_service to invoke Connection::handle_user_read when it finishes reading data from the socket. The io_service stores this functor and continues its loop, waiting for the asynchronous read operation to complete.
  2. After your call to async_read, the Connection object is deallocated for some reason (program termination, an error condition, etc.)
  3. Suppose the io_service now determines that the asynchronous read is complete, after the Connection object has been deallocated but before the io_service is destroyed (this can occur, for example, if io_service::run is running in a separate thread, as is typical). Now, the io_service attempts to invoke the handler, and it has an invalid reference to a Connection 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.

Share:
28,735

Related videos on Youtube

SpyBot
Author by

SpyBot

Software Engineer

Updated on April 27, 2021

Comments

  • SpyBot
    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 with async_write(), but not with async_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 declaring char user_[max_len] and using Boost::asio::buffer(user_, max_len)?

    Also, what's the point of inheriting from boost::enable_shared_from_this<Connection> and using shared_from_this() instead of this in async_read() and async_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
    MSalters almost 5 years
    This 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, but std::string has internal copies when it grows anyway. Note that the extraction method operator>> is the usual whitespace-parsing method.from iostream. You can also call getline on the istream.