Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ add_library(internal
${CMAKE_CURRENT_SOURCE_DIR}/url.cpp
${CMAKE_CURRENT_SOURCE_DIR}/platform/native_socket.cpp
${CMAKE_CURRENT_SOURCE_DIR}/platform/network_runtime.cpp
${CMAKE_CURRENT_SOURCE_DIR}/platform/socket_connect.cpp
${CMAKE_CURRENT_SOURCE_DIR}/net/resolver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/net/tcp_connection.cpp
)

add_library(${PROJECT_NAME}::internal ALIAS internal)
Expand Down
85 changes: 85 additions & 0 deletions src/net/tcp_connection.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#include "net/tcp_connection.hpp"

#include "platform/network_runtime.hpp"
#include "platform/socket_connect.hpp"

#include <chrono>

namespace cpp_request::detail::net {

Result<TcpConnection> TcpConnection::connect(
const std::vector<Endpoint>& endpoints,
std::chrono::milliseconds timeout) {
if (timeout.count() <= 0) {
return Error{ErrorCode::ConnectTimeout};
}

if (endpoints.empty()) {
return Error{ErrorCode::ConnectFailed};
}

const auto runtime = platform::ensure_network_runtime();
if (!runtime.ok) {
return Error{ErrorCode::SocketCreateFailed, runtime.native_code};
}

const auto deadline = std::chrono::steady_clock::now() + timeout;
bool created_socket = false;
bool saw_valid_endpoint = false;
int last_native_error = 0;

for (const Endpoint& endpoint : endpoints) {
if (!endpoint.valid()) {
continue;
}

saw_valid_endpoint = true;
const auto now = std::chrono::steady_clock::now();
if (now >= deadline) {
return Error{ErrorCode::ConnectTimeout, last_native_error};
}

int create_error = 0;
auto socket = platform::create_tcp_socket(
endpoint.family(),
endpoint.socket_type(),
endpoint.protocol(),
create_error);

if (!socket) {
last_native_error = create_error;
continue;
}

created_socket = true;
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
deadline - std::chrono::steady_clock::now());

const auto attempt = platform::connect_with_timeout(
socket.get(),
endpoint.native_address(),
endpoint.native_address_size(),
remaining);

if (attempt.status == platform::ConnectStatus::Connected) {
return TcpConnection{std::move(socket)};
}

last_native_error = attempt.native_code;
if (attempt.status == platform::ConnectStatus::TimedOut) {
return Error{ErrorCode::ConnectTimeout, last_native_error};
}
}

if (!saw_valid_endpoint) {
return Error{ErrorCode::ConnectFailed};
}

if (!created_socket) {
return Error{ErrorCode::SocketCreateFailed, last_native_error};
}

return Error{ErrorCode::ConnectFailed, last_native_error};
}

} // namespace cpp_request::detail::net
46 changes: 46 additions & 0 deletions src/net/tcp_connection.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#pragma once

#include "net/endpoint.hpp"
#include "platform/native_socket.hpp"

#include <cpp_request/result.hpp>

#include <chrono>
#include <utility>
#include <vector>

namespace cpp_request::detail::net {

class TcpConnection final {
public:
TcpConnection() noexcept = default;

TcpConnection(const TcpConnection&) = delete;
TcpConnection& operator=(const TcpConnection&) = delete;
TcpConnection(TcpConnection&&) noexcept = default;
TcpConnection& operator=(TcpConnection&&) noexcept = default;

[[nodiscard]] static Result<TcpConnection> connect(
const std::vector<Endpoint>& endpoints,
std::chrono::milliseconds timeout);

[[nodiscard]] bool connected() const noexcept {
return socket_.valid();
}

[[nodiscard]] platform::NativeSocketHandle native_handle() const noexcept {
return socket_.get();
}

void close() noexcept {
socket_.close();
}

private:
explicit TcpConnection(platform::NativeSocket socket) noexcept
: socket_(std::move(socket)) {}

platform::NativeSocket socket_;
};

} // namespace cpp_request::detail::net
170 changes: 170 additions & 0 deletions src/platform/socket_connect.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#include "platform/socket_connect.hpp"

#ifdef _WIN32
#include <winsock2.h>
#else
#include <cerrno>
#include <fcntl.h>
#include <poll.h>
#include <sys/socket.h>
#endif

#include <algorithm>
#include <limits>

namespace cpp_request::detail::platform {
namespace {

#ifdef _WIN32
bool set_nonblocking(NativeSocketHandle socket, bool enabled, int& native_code) noexcept {
u_long mode = enabled ? 1UL : 0UL;
if (::ioctlsocket(socket, FIONBIO, &mode) == 0) {
return true;
}
native_code = ::WSAGetLastError();
return false;
}

bool connect_in_progress(int code) noexcept {
return code == WSAEWOULDBLOCK || code == WSAEINPROGRESS || code == WSAEALREADY;
}
#else
bool set_nonblocking(NativeSocketHandle socket, bool enabled, int& native_code) noexcept {
const int flags = ::fcntl(socket, F_GETFL, 0);
if (flags == -1) {
native_code = errno;
return false;
}

const int next = enabled ? (flags | O_NONBLOCK) : (flags & ~O_NONBLOCK);
if (::fcntl(socket, F_SETFL, next) == 0) {
return true;
}
native_code = errno;
return false;
}

bool connect_in_progress(int code) noexcept {
return code == EINPROGRESS || code == EWOULDBLOCK || code == EALREADY;
}
#endif

ConnectAttemptResult socket_error_result(NativeSocketHandle socket) noexcept {
int error = 0;
#ifdef _WIN32
int length = sizeof(error);
if (::getsockopt(socket, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&error), &length) != 0) {
return {ConnectStatus::Failed, ::WSAGetLastError()};
}
#else
socklen_t length = sizeof(error);
if (::getsockopt(socket, SOL_SOCKET, SO_ERROR, &error, &length) != 0) {
return {ConnectStatus::Failed, errno};
}
#endif
return error == 0
? ConnectAttemptResult{ConnectStatus::Connected, 0}
: ConnectAttemptResult{ConnectStatus::Failed, error};
}

} // namespace

NativeSocket create_tcp_socket(
int family,
int socket_type,
int protocol,
int& native_code) noexcept {
native_code = 0;
const NativeSocketHandle handle = ::socket(family, socket_type, protocol);
if (handle == kInvalidSocket) {
native_code = last_socket_error();
return {};
}
return NativeSocket{handle};
}

ConnectAttemptResult connect_with_timeout(
NativeSocketHandle socket,
const sockaddr* address,
std::size_t address_size,
std::chrono::milliseconds timeout) noexcept {
if (timeout.count() <= 0) {
return {ConnectStatus::TimedOut, 0};
}

int native_code = 0;
if (!set_nonblocking(socket, true, native_code)) {
return {ConnectStatus::Failed, native_code};
}

#ifdef _WIN32
const int connect_result = ::connect(
socket,
address,
static_cast<int>(address_size));
#else
const int connect_result = ::connect(
socket,
address,
static_cast<socklen_t>(address_size));
#endif

if (connect_result == 0) {
if (!set_nonblocking(socket, false, native_code)) {
return {ConnectStatus::Failed, native_code};
}
return {ConnectStatus::Connected, 0};
}

native_code = last_socket_error();
if (!connect_in_progress(native_code)) {
return {ConnectStatus::Failed, native_code};
}

#ifdef _WIN32
fd_set write_set;
fd_set except_set;
FD_ZERO(&write_set);
FD_ZERO(&except_set);
FD_SET(socket, &write_set);
FD_SET(socket, &except_set);

const auto total_ms = timeout.count();
timeval tv{};
tv.tv_sec = static_cast<long>(total_ms / 1000);
tv.tv_usec = static_cast<long>((total_ms % 1000) * 1000);

const int wait_result = ::select(0, nullptr, &write_set, &except_set, &tv);
if (wait_result == 0) {
return {ConnectStatus::TimedOut, 0};
}
if (wait_result == SOCKET_ERROR) {
return {ConnectStatus::Failed, ::WSAGetLastError()};
}
#else
pollfd descriptor{};
descriptor.fd = socket;
descriptor.events = POLLOUT;

const auto bounded = std::min<long long>(
timeout.count(),
std::numeric_limits<int>::max());
const int wait_result = ::poll(&descriptor, 1, static_cast<int>(bounded));
if (wait_result == 0) {
return {ConnectStatus::TimedOut, 0};
}
if (wait_result < 0) {
return {ConnectStatus::Failed, errno};
Comment on lines +156 to +157

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry poll when a signal interrupts the wait

On POSIX, if a signal is delivered while a connection attempt is pending, poll() returns -1 with errno == EINTR; this path reports ConnectFailed and abandons a potentially viable endpoint well before the configured deadline. Retry the wait after recomputing the remaining timeout when the error is EINTR, while preserving other errors as failures.

Useful? React with 👍 / 👎.

}
#endif

auto result = socket_error_result(socket);
if (result.status == ConnectStatus::Connected) {
if (!set_nonblocking(socket, false, native_code)) {
return {ConnectStatus::Failed, native_code};
}
}
return result;
}

} // namespace cpp_request::detail::platform
35 changes: 35 additions & 0 deletions src/platform/socket_connect.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include "platform/native_socket.hpp"

#include <chrono>
#include <cstddef>

struct sockaddr;

namespace cpp_request::detail::platform {

enum class ConnectStatus {
Connected,
Failed,
TimedOut
};

struct ConnectAttemptResult {
ConnectStatus status{ConnectStatus::Failed};
int native_code{0};
};

[[nodiscard]] NativeSocket create_tcp_socket(
int family,
int socket_type,
int protocol,
int& native_code) noexcept;

[[nodiscard]] ConnectAttemptResult connect_with_timeout(
NativeSocketHandle socket,
const sockaddr* address,
std::size_t address_size,
std::chrono::milliseconds timeout) noexcept;

} // namespace cpp_request::detail::platform
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ add_executable(cpp_request_tests
${CMAKE_CURRENT_SOURCE_DIR}/url_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/native_socket_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/resolver_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tcp_connection_test.cpp
)

target_include_directories(cpp_request_tests
Expand Down
Loading
Loading