c++asynchronousnetwork-programmingboost-asiostd-future

Boost asio:async_read() using boost::asio::use_future


When calling asio::async_read() using a future, is there a way to get the number of bytes transferred when a boost:asio::error::eof exception occurs? It would seem that there are many cases when one would want to get the data transferred even if the peer disconnects.

For example:

namespace ba = boost::asio;

int32_t Session::read (unsigned char* pBuffer, uint32_t bufferSizeToRead)
{
   // Create a mutable buffer
   ba::mutable_buffer buffer (pBuffer, bufferSizeToRead);
   int32_t result = 0;

   // We do an async call using a future.  A thread from the io_context pool does the
   // actual read while the the thread calling this method will blocks on the 
   // std::future::get()
   std::future<std::size_t> future =
         ba::async_read(m_socket, buffer, ba::bind_executor(m_sessionStrand, ba::use_future));
   try
   {
      // We block the calling thread here until we get the results of the async_read_some()...
      result = future.get();
   }
   catch (boost::system::system_error &ex)  // boost::system::system_error
   {
      auto exitCode = ex.code().value();
      if ( exitCode == ba::error::eof )
      {
         log ("Connection closed by the peer");
      }
   }

   return results;  // This is zero if eof occurs
}

The code sample above represents our issue. It was designed to support a 3rd-party library. The library expects a blocking call. The new code under development is using ASIO with a minimal number of network threads. The expectation is that this 3rd party library calls session::read using its dedicated thread and we adapt the call to an asynchronous call. The network call must be async since we are supporting many such calls from different libraries with minimal threads.

What was unexpected and discovered late is that ASIO treats a connection closed as an error. Without the future, using a handler we could get the bytes transferred up to the point where the disconnect occurred. However, using a future, the exception is thrown and the bytes transferred becomes unknown.

void handler (const boost::system::error_code& ec,
      std::size_t bytesTransferred );
  1. Is there a way to do the above with a future and also get the bytes transferred?
  2. Or ss there an alternative approach where we can provide the library a blocking call by still use an asio::async_read or similar.

Our expectation is that we could get the bytes transferred even if the client closed the connection. We're puzzled that when using a future this does not seem possible.


Solution

  • It's an implementation limitation of futures.

    Modern async_result<> specializations (that use the initiate member approach) can be used together with as_tuple, e.g.:

    ba::awaitable<std::tuple<boost::system::error_code, size_t>> a =
        ba::async_read(m_socket, buffer, ba::as_tuple(ba::use_awaitable));
    

    Or, more typical:

    auto [ec, n] = co_await async_read(m_socket, buffer, ba::as_tuple(ba::use_awaitable));
    

    However, the corresponding:

    auto future = ba::async_read(m_socket, buffer, ba::as_tuple(ba::use_future));
    

    isn't currently supported. It arguably could, but you'd have to create your own completion token, or ask Asio devs to add support to use_future: https://github.com/chriskohlhoff/asio/issues


    Side-note: if you construct the m_socket from the m_sessioStrand executor, you do not need to bind_executor to the strand:

    using Executor = net::io_context::executor_type;
    struct Session {
        int32_t read(unsigned char* pBuffer, uint32_t bufferSizeToRead);
    
        net::io_context       m_ioc;
        net::strand<Executor> m_sessionStrand{m_ioc.get_executor()};
        tcp::socket           m_socket{m_sessionStrand};
    };