Reputation: 180303
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)
Upvotes: 2
Views: 4762
Reputation: 51971
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
orasync_read_until
functions to violate their contract. E.g. a read ofN
bytes may finish early due toEOF
.
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
Upvotes: 3
Reputation: 180303
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 :(
Upvotes: 0