Search code examples
boost-asioasyncsocket

issues about using async_write right after async_read_until


My code is as follows:

boost::asio::streambuf b1;

boost::asio::async_read_until(upstream_socket_, b1, '@',
           boost::bind(&bridge::handle_upstream_read, shared_from_this(),
           boost::asio::placeholders::error,
           boost::asio::placeholders::bytes_transferred));


void handle_upstream1_read(const boost::system::error_code& error,
                            const size_t& bytes_transferred)
  {
     if (!error)
     {

       async_write(downstream_socket_,
          b2,
          boost::bind(&bridge::handle_downstream_write,
          shared_from_this(),
          boost::asio::placeholders::error));  
     }
     else
        close();
  }

According to the documentation of async_read_until, http://www.boost.org/doc/libs/1_55_0/doc/html/boost_asio/reference/async_read_until/overload1.html, After a successful async_read_until operation, the streambuf may contain additional data beyond the delimiter. An application will typically leave that data in the streambuf for a subsequent async_read_until operation to examine.

I know that the streambuf may contain additional data beyond the delimiter, but, in my case, will it write those additional data (the data beyond the char'@') to the downstream_socket_ inside the async_write operation? Or will async_write function be smart enough not to write those additional data until the next time the handle_upstream1_read function is being called?

According to the approaches in the documentation, the data in streambuf are stored in the istream first ( std::istream response_stream(&streambuf); ) and then put it into a string by using std::getline() funciton.

Do I really need to store the streambuf in istream first and then convert it into a string and then convert it back to char arrary (so that I can send the char array to the downstream_socket_ ) instead of just using the async_write to write the data( up to but not including the delimter, '@' ) to the downstream_socket_ ?

I prefer the second approach so that I don't need to make several conversion on the data. However, it seems that something is wrong when I tried the second approach.

My ideal case is that:

  1. upstream_socket_ received xxxx@yyyy by using async_read_until
  2. xxxx@ is written to the downstream_socket_
  3. upstream_socket_ received zzzz@kkkk by using async_read_until
  4. yyyyzzzz@ is written to the downstream_socket_

It seems that async_write operation still writes the data beyond the delimiter to the downstream_socket_. (but I am not 100% sure about this)

I appreciate if anyone can provide a little help !


Solution

  • The async_write() overload being used is considered complete when all of the streambuf's data, its input sequence, has been written to the WriteStream (socket). It is equivalent to calling:

    boost::asio::async_write(stream, streambuf,
        boost::asio::transfer_all(), handler);
    

    One can limit the amount of bytes written and consumed from the streambuf object by calling this async_write() overload with the boost::asio::transfer_exactly completion condition:

    boost::asio::async_write(stream, streambuf, 
        boost::asio::transfer_exactly(n), handler);
    

    Alternatively, one can write directly from the streambuf's input sequence. However, one will need to explicitly consume from the streambuf.

    boost::asio::async_write(stream,
        boost::asio::buffer(streambuf.data(), n), handler);
    // Within the completion handler...
    streambuf.consume(n);
    

    Note that when the async_read_until() operation completes, the completion handler's bytes_transferred argument contains the number of bytes in the streambuf's input sequence up to and including the delimiter, or 0 if an error occurred.


    Here is a complete example demonstrating using both approaches. The example is written using synchronous operations in an attempt to simplify the flow:

    #include <iostream>
    #include <boost/asio.hpp>
    #include <boost/bind.hpp>
    
    // 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() {}
    
    /// @brief Helper function that extracts a string from a streambuf.
    std::string make_string(
      boost::asio::streambuf& streambuf,
      std::size_t n)
    {
      return std::string(
          boost::asio::buffers_begin(streambuf.data()),
          boost::asio::buffers_begin(streambuf.data()) + n);
    }
    
    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, boost::bind(&noop));
      client_socket.async_connect(acceptor.local_endpoint(), boost::bind(&noop));
      io_service.run();
    
      // Mockup write_buffer as if it read "xxxx@yyyy" with read_until()
      // using '@' as a delimiter.
      boost::asio::streambuf write_buffer;
      std::ostream output(&write_buffer);
      output << "xxxx@yyyy";
      assert(write_buffer.size() == 9);
      auto bytes_transferred = 5;
    
      // Write to server.
      boost::asio::write(server_socket, write_buffer,
          boost::asio::transfer_exactly(bytes_transferred));
       // Verify write operation consumed part of the input sequence.
      assert(write_buffer.size() == 4);
    
      // Read from client.
      boost::asio::streambuf read_buffer;
      bytes_transferred = boost::asio::read(
          client_socket, read_buffer.prepare(bytes_transferred));
      read_buffer.commit(bytes_transferred);
    
      // Copy from the read buffers input sequence.
      std::cout << "Read: " << 
                   make_string(read_buffer, bytes_transferred) << std::endl;
      read_buffer.consume(bytes_transferred);
    
      // Mockup write_buffer as if it read "zzzz@kkkk" with read_until()
      // using '@' as a delimiter.
      output << "zzzz@kkkk";
      assert(write_buffer.size() == 13); 
      bytes_transferred = 9; // yyyyzzzz@
    
      // Write to server.
      boost::asio::write(server_socket, buffer(write_buffer.data(),
          bytes_transferred));
      // Verify write operation did not consume the input sequence.
      assert(write_buffer.size() == 13);
      write_buffer.consume(bytes_transferred);
    
      // Read from client.
      bytes_transferred = boost::asio::read(
          client_socket, read_buffer.prepare(bytes_transferred));
      read_buffer.commit(bytes_transferred);
    
      // Copy from the read buffers input sequence.
      std::cout << "Read: " << 
                   make_string(read_buffer, bytes_transferred) << std::endl;
      read_buffer.consume(bytes_transferred);
    }
    

    Output:

    Read: xxxx@
    Read: yyyyzzzz@
    

    A few other notes:

    • The streambuf owns the memory, and std::istream and std::ostream use the memory. Using streams may be a good idea when one needs to extract formatted input or insert formatted output. For instance, when one wishes to read the string "123" as an integer 123.
    • One can directly access the streambuf's input sequence and iterate over it. In the example above, I use boost::asio::buffers_begin() to help construct a std::string by iterating over a streambuf's input sequence.

      std::string(
          boost::asio::buffers_begin(streambuf.data()),
          boost::asio::buffers_begin(streambuf.data()) + n);
      
    • A stream-based transport protocol is being used, so handle incoming data as a stream. Be aware that even if the intermediary server reframes messages and sends "xxxx@" in one write operation and "yyyyzzzz@" in a subsequent write operation, the downstream may read "xxxx@yyyy" in a single read operation.