2
votes

I'm implementing a very simple protocol using Boost Asio. I send a simple query and I get back a variable-length response. The async send appears to work, and the write handler is called. Since I don't know how long the response is, I start by reading the fixed 8 byte header. This will always be present, and it contains the length of the remainder. Relevant call:

char input[256]; // Large enough to also hold the variable part.
async_read(socket, buffer(input), transfer_at_least(8), callback);

In callback, I get a boost::system::error_code which says "End of file". Sure enough, the socket is no longer open. But it's a TCP socket. What is the point of failing at end-of-input and closing the socket? More input will arrive. I know the remote side doesn't close the socket, that's production code which is known to implement this protocol correctly.

The whole reason for the async read is of course to not block waiting for the response. So how do I get a callback after the first 8 bytes, without Asio closing the socket on me?

Existing questions are similar but different: either they don't use async reads, or they don't do fixed-size reads, or they have issues only after the first read, or they have issues with io_service::run (I don't, it returns as expected after the EOF error happens)

2
Is your input buffer on the stack like your code suggests?Xaqq
Are you on Linux? If so, I suggest running with strace to see what is really going on (assuming your buffer is not actually on the stack, as the previous comments asks.)kec
@Xaqq: No, it's a class member. The object currently isn't deleted at all (I was planning to use std::shared_ptr but somewhere between boost::asio and std::shared_ptr the use count is corrupted. Weird.)MSalters
So if not class member, then strace will tell you if in fact the socket has been closed on the other side (read will return 0). You can then also strace the other side to get clues as to why and when it decided to close.kec
Also, note that at "end-of-input" there is nothing more that can be done. The other side closed the connection. This is a different condition from socket still open but nothing available. On Linux/UNIX, this is a EAGAIN/EWOULDBLOCK error, assuming that the file description has been marked non-blocking. If the socket has not been marked non-blocking, then a read call when the other side has not closed the socket but there is nothing to read will simply block.kec

2 Answers

3
votes

End of file (boost::asio::error::eof) indicates that the remote peer closed the connection. It does not indicate that the stream has no more data available to read. The Boost.Asio streams documentation states:

The end of a stream can cause read, async_read, read_until or async_read_until functions to violate their contract. E.g. a read of N bytes may finish early due to EOF.

Although it could be possible that this error is occurring as a result of undefined behavior. Boost.Asio requires that the buffer provided to async_read() (input) must remain valid until the handler (callback) is called.

Also, if the socket had been closed locally, then the operation's error would be boost::asio::error::operation_aborted.


Here is a basic application that can be used to demonstrate this behavior:

#include <boost/array.hpp>
#include <boost/asio.hpp>

void on_read(
  const boost::system::error_code& error,
  std::size_t bytes_transferred)
{
  std::cout << "read " << bytes_transferred << " bytes with "
            << error.message() << std::endl;
}

int main(int argc, char* argv[])
{
  if (argc != 2)
  {
    std::cerr << "Usage: <port>\n";
    return 1;
  }

  // Create socket and connet to local port.
  namespace ip = boost::asio::ip;
  boost::asio::io_service io_service;
  ip::tcp::socket socket(io_service);
  socket.connect(ip::tcp::endpoint(
      ip::address::from_string("127.0.0.1"), std::atoi(argv[1])));

  // Start read operation.
  boost::array<char, 512> buffer;
  async_read(socket, 
             boost::asio::buffer(buffer),
             boost::asio::transfer_at_least(7),
             &on_read);

  // Run the service. 
  io_service.run();
}

The following demonstrates writing two times to the TCP connection. The first write is small enough that the client will have read all of the stream's data before the next write.

$ nc -l 12345 &
[1] 11709
$ ./a.out 12345 &
[2] 11712
$ fg 1
nc -l 12345
helloenter
worldenter
read 12 bytes with Success

The same program, but the connection is explicitly killed:

$ nc -l 12345 &
[1] 11773
$ ./a.out 12345 &
[2] 11775
$ fg 1
nc -l 12345
helloenter
worldctrl-c
read 6 bytes with End of file
0
votes

Turns out to be a race condition in the protocol implementation. If the initial message is not accepted, the socket is closed by the remote side. The local side did check this after sending the initial message. However, after moving to async operations, the socket check was done after the async write returned locally, and before the async read.

There's no way to predict exactly when the remote side will close the socket, you can't wait for that. The only thing you know is that if you receive those 8 bytes, it didn't close the socket. So the EOF result while reading asynchronously is something that I just have to handle.

And I suspect the existing (synchronous) code may have similar timing issues that haven't surfaced yet :(