feat(tcp): connect over IPv4 and IPv6, and say why a connect failed
- ClientTCP resolves with AF_UNSPEC and tries every address the name resolves to, in getaddrinfo's order, the way Python's socket.create_connection does. It used to ask for AF_INET only and keep the first answer, so `localhost` could not reach a server listening on ::1 alone, which is what a dev server on a recent Node does. The connect timeout bounds each address on its own; when none connects, what the last one failed with is thrown. - A failed resolve, socket() or connect throws SocketErrorException: the std::runtime_error it always was, with the same message, plus the errno (SocketErrorKind::System) or the getaddrinfo code (SocketErrorKind::Resolver). A caller that reports the cause in its own words, such as a Python-style "[Errno 111] Connection refused", no longer has to parse it back out of the text. - The move constructor carries the resolved addresses along. New: ShouldConnectDualStack, ShouldReportConnectErrors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3a58564422
commit
78853033e8
6 changed files with 383 additions and 51 deletions
|
|
@ -43,6 +43,60 @@ namespace {
|
|||
if (errno == EAGAIN || errno == EWOULDBLOCK) throw SocketTimeoutException();
|
||||
throw std::runtime_error(std::strerror(errno));
|
||||
}
|
||||
|
||||
// One connect attempt to one address: a connected, blocking socket, or a
|
||||
// throw that leaves nothing open.
|
||||
int ConnectTo(const sockaddr* address, socklen_t length, std::chrono::milliseconds timeout) {
|
||||
const int fd = socket(address->sa_family, SOCK_STREAM, 0);
|
||||
if (fd == -1) {
|
||||
// An address family the host cannot open — IPv6 switched off,
|
||||
// say — fails this address alone; the caller moves on.
|
||||
const int failure = errno;
|
||||
throw SocketErrorException(SocketErrorKind::System, failure, std::string("Could not open socket: ") + std::strerror(failure));
|
||||
}
|
||||
|
||||
// With a timeout the connect runs non-blocking, so poll can bound the
|
||||
// wait; the socket is made blocking again once it is connected.
|
||||
const bool bounded = timeout > std::chrono::milliseconds::zero();
|
||||
const int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (bounded) {
|
||||
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||
}
|
||||
int failure = 0;
|
||||
if (connect(fd, address, length) == -1) {
|
||||
failure = errno;
|
||||
if (failure == EINPROGRESS) {
|
||||
pollfd connecting { .fd = fd, .events = POLLOUT, .revents = 0 };
|
||||
const int waitMs = static_cast<int>(std::min<std::chrono::milliseconds::rep>(timeout.count(), std::numeric_limits<int>::max()));
|
||||
int ready;
|
||||
do {
|
||||
ready = poll(&connecting, 1, waitMs);
|
||||
} while (ready == -1 && errno == EINTR);
|
||||
if (ready == 0) {
|
||||
close(fd);
|
||||
throw SocketTimeoutException();
|
||||
}
|
||||
if (ready == -1) {
|
||||
failure = errno;
|
||||
} else {
|
||||
// The connect's own result.
|
||||
socklen_t size = sizeof(failure);
|
||||
getsockopt(fd, SOL_SOCKET, SO_ERROR, &failure, &size);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failure != 0) {
|
||||
// Report the failure instead of handing back a socket that is not
|
||||
// connected to anything — every later send/recv on it would fail
|
||||
// with a far less obvious error.
|
||||
close(fd);
|
||||
throw SocketErrorException(SocketErrorKind::System, failure, std::string("Could not connect to server: ") + std::strerror(failure));
|
||||
}
|
||||
if (bounded) {
|
||||
fcntl(fd, F_SETFL, flags);
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
}
|
||||
|
||||
ClientTCP::ClientTCP(int socketid) : socketid(socketid)
|
||||
|
|
@ -55,17 +109,28 @@ ClientTCP::ClientTCP(const char* hostName, std::uint16_t port, std::chrono::mill
|
|||
// getaddrinfo rather than gethostbyname: the latter is not thread safe,
|
||||
// and it signals failure by returning null — which was then dereferenced
|
||||
// straight into a crash on any unresolvable host.
|
||||
//
|
||||
// AF_UNSPEC, and every result kept: `localhost` resolves to ::1 and
|
||||
// 127.0.0.1, and a server may listen on either one alone. Asking for
|
||||
// AF_INET only, as this used to, made a server bound to ::1 unreachable
|
||||
// by name. No AI_ADDRCONFIG, as Python's create_connection passes none
|
||||
// either: it hides ::1 on a host whose only IPv6 address is loopback.
|
||||
addrinfo hints{};
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
addrinfo* resolved = nullptr;
|
||||
const std::string service = std::to_string(port);
|
||||
const int status = getaddrinfo(hostName, service.c_str(), &hints, &resolved);
|
||||
if (status != 0 || resolved == nullptr) {
|
||||
throw std::runtime_error(std::string("Could not resolve host '") + hostName + "': "
|
||||
+ gai_strerror(status));
|
||||
throw SocketErrorException(SocketErrorKind::Resolver, status,
|
||||
std::string("Could not resolve host '") + hostName + "': " + gai_strerror(status));
|
||||
}
|
||||
for (const addrinfo* entry = resolved; entry != nullptr; entry = entry->ai_next) {
|
||||
Address address {};
|
||||
std::memcpy(&address.storage, entry->ai_addr, entry->ai_addrlen);
|
||||
address.length = entry->ai_addrlen;
|
||||
addresses.push_back(address);
|
||||
}
|
||||
serv_addr = *reinterpret_cast<sockaddr_in*>(resolved->ai_addr);
|
||||
freeaddrinfo(resolved);
|
||||
|
||||
Connect(connectTimeout);
|
||||
|
|
@ -79,7 +144,7 @@ ClientTCP::ClientTCP(std::string hostName, std::uint16_t port, std::chrono::mill
|
|||
// The moved-from object gives up ownership; the socket itself stays open —
|
||||
// closing it here (as this used to) meant moving a ClientTCP silently
|
||||
// dropped the connection it was carrying.
|
||||
ClientTCP::ClientTCP(ClientTCP&& other) noexcept : socketid(other.socketid) {
|
||||
ClientTCP::ClientTCP(ClientTCP&& other) noexcept : socketid(other.socketid), addresses(std::move(other.addresses)) {
|
||||
other.socketid = -1;
|
||||
}
|
||||
|
||||
|
|
@ -92,52 +157,23 @@ ClientTCP::~ClientTCP()
|
|||
}
|
||||
|
||||
void ClientTCP::Connect(std::chrono::milliseconds timeout) {
|
||||
if((socketid = socket(AF_INET, SOCK_STREAM, 0)) == -1){
|
||||
throw std::runtime_error(std::string("Could not open socket: ") + std::strerror(errno));
|
||||
}
|
||||
|
||||
// With a timeout the connect runs non-blocking, so poll can bound the
|
||||
// wait; the socket is made blocking again once it is connected.
|
||||
const bool bounded = timeout > std::chrono::milliseconds::zero();
|
||||
const int flags = fcntl(socketid, F_GETFL, 0);
|
||||
if (bounded) {
|
||||
fcntl(socketid, F_SETFL, flags | O_NONBLOCK);
|
||||
if (addresses.empty()) {
|
||||
throw std::logic_error("ClientTCP::Connect: no address to connect to; a socket handed over by accept() cannot connect again");
|
||||
}
|
||||
int failure = 0;
|
||||
if(connect(socketid,(sockaddr*)&serv_addr, sizeof(sockaddr)) == -1){
|
||||
failure = errno;
|
||||
if (failure == EINPROGRESS) {
|
||||
pollfd connecting { .fd = socketid, .events = POLLOUT, .revents = 0 };
|
||||
const int waitMs = static_cast<int>(std::min<std::chrono::milliseconds::rep>(timeout.count(), std::numeric_limits<int>::max()));
|
||||
int ready;
|
||||
do {
|
||||
ready = poll(&connecting, 1, waitMs);
|
||||
} while (ready == -1 && errno == EINTR);
|
||||
if (ready == 0) {
|
||||
close(socketid);
|
||||
socketid = -1;
|
||||
throw SocketTimeoutException();
|
||||
}
|
||||
if (ready == -1) {
|
||||
failure = errno;
|
||||
} else {
|
||||
// The connect's own result.
|
||||
socklen_t length = sizeof(failure);
|
||||
getsockopt(socketid, SOL_SOCKET, SO_ERROR, &failure, &length);
|
||||
}
|
||||
// Each address in turn, in the order getaddrinfo ranked them; when none
|
||||
// connects, the caller sees what the last one failed with, as with
|
||||
// Python's create_connection.
|
||||
std::exception_ptr last;
|
||||
for (const Address& address : addresses) {
|
||||
try {
|
||||
socketid = ConnectTo(reinterpret_cast<const sockaddr*>(&address.storage), address.length, timeout);
|
||||
return;
|
||||
} catch (...) {
|
||||
last = std::current_exception();
|
||||
}
|
||||
}
|
||||
if (failure != 0) {
|
||||
// Report the failure instead of handing back a socket that is not
|
||||
// connected to anything — every later send/recv on it would fail
|
||||
// with a far less obvious error.
|
||||
close(socketid);
|
||||
socketid = -1;
|
||||
throw std::runtime_error(std::string("Could not connect to server: ") + std::strerror(failure));
|
||||
}
|
||||
if (bounded) {
|
||||
fcntl(socketid, F_SETFL, flags);
|
||||
}
|
||||
socketid = -1;
|
||||
std::rethrow_exception(last);
|
||||
}
|
||||
|
||||
void ClientTCP::Stop() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue