but presumably the socket gets opened and closed repeatedly on failure
I was skeptical. The documentation specifically states:
- The underlying socket is automatically opened if needed.
- An automatically opened socket is not returned to the closed state upon failure
It seems fair to expect that if the socket was already open, it will also not be returned to the closed state.
However, you're right to smell the opportunity for a problem here. And I went to check.
The Surprise
Indeed, the bind doesn't appear to hold, even when there's a single endpoint, but it's using the endpoint-sequence overload. Here's a comprehensive demonstration:
#include <boost/asio.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <iostream>
namespace asio = boost::asio;
using asio::ip::tcp;
using error_code = boost::system::error_code;
auto fake_dns_result() {
tcp::endpoint eps[]{
{{}, 7878}, // not available
{{}, 6767}, // running netcat
};
return tcp::resolver::results_type::create( //
std::begin(eps), std::end(eps), "localhost", "dummysvc");
}
void test(tcp::endpoint local_ep) {
asio::io_context ioc;
auto endpoints = fake_dns_result();
boost::beast::tcp_stream s(ioc);
s.socket().open(tcp::v4());
s.socket().bind(local_ep);
auto log = [&s, local_ep](error_code ec, tcp::endpoint const& next) {
std::cout << ec.message() << " next:" << next << " "
<< (s.socket().is_open() ? "open" : "closed")
<< " bound:" << s.socket().local_endpoint()
<< std::endl;
return true;
};
s.async_connect(endpoints, log, [&s](auto ec, const auto& /*ep*/) {
std::cout << " --> Final " << ec.message() << " local "
<< s.socket().local_endpoint() << " to "
<< s.socket().remote_endpoint() << "\n\n";
});
ioc.run();
}
int main() {
using A = asio::ip::address_v4;
test({}); // 0.0.0.0:0
test({A::loopback(), 0}); // 127.0.0.1:0
test({A{{127, 0, 0, 42}}, 0}); // 127.0.0.1:0
test({A{{192, 168, 50, 225}}, 0}); // 192.168.50.225:0
}
See it Live On Coliru:
Success next:0.0.0.0:7878 open bound:0.0.0.0:36700
Connection refused next:0.0.0.0:6767 open bound:127.0.0.1:35126
--> Final Success local 127.0.0.1:59672 to 127.0.0.1:6767
Success next:0.0.0.0:7878 open bound:127.0.0.1:51845
Connection refused next:0.0.0.0:6767 open bound:127.0.0.1:35130
--> Final Success local 127.0.0.1:59676 to 127.0.0.1:6767
Success next:0.0.0.0:7878 open bound:127.0.0.42:37227
Connection refused next:0.0.0.0:6767 open bound:127.0.0.1:35134
--> Final Success local 127.0.0.1:59680 to 127.0.0.1:6767
Success next:0.0.0.0:7878 open bound:173.203.57.63:49499
Connection refused next:0.0.0.0:6767 open bound:127.0.0.1:35138
--> Final Success local 127.0.0.1:59684 to 127.0.0.1:6767
As you suggested, the bound endpoint is not honoured. Even moving the bind into the connection condition doesn't help:
s.socket().open(tcp::v4());
auto log = [&s, local_ep](error_code ec, tcp::endpoint const& next) {
std::cout << ec.message() << " next:" << next << " "
<< (s.socket().is_open() ? "open" : "closed")
<< " bound:" << s.socket().local_endpoint()
<< std::endl;
s.socket().bind(local_ep);
return true;
};
Still prints the same
Success next:0.0.0.0:7878 open bound:173.203.57.63:49499
Connection refused next:0.0.0.0:6767 open bound:127.0.0.1:35138
--> Final Success local 127.0.0.1:59684 to 127.0.0.1:6767
Stripping The Beast
Reviewing the implementation, it looks like asio::[async_]connect should have the exact same behaviour, because Beast only adds the timeout logic. Let's reduce: Coliru.
As expected, same output. On to reviewing the Asio implementation.
Stripping The Async
Just to simplify the review, let's also check the synchronous connect behaves the same:
error_code ec;
/*auto ep =*/ asio::connect(s, endpoints, log, ec);
std::cout << " --> Final " << ec.message() << " local "
<< s.local_endpoint() << " to " << s.remote_endpoint() << "\n\n";
Still the same: Coliru
The Culprit
Indeed, the implementation of the range-connect in Asio looks like this:
for (Iterator iter = begin; iter != end; ++iter)
{
iter = (detail::call_connect_condition(connect_condition, ec, iter, end));
if (iter != end)
{
s.close(ec);
s.connect(*iter, ec);
if (!ec)
return iter;
}
else
break;
}
It follows that it will be impossible to get the required behavior on range-connect, short of
- creating your own socket type that binds on open.
- writing your own range-connect operation that uses a local endpoint