-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add timed TCP connection establishment #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
81c5b26
feat: add platform connect primitive
zuudevs 29adf5c
feat: implement timed socket connect
zuudevs 31bde7f
feat: add tcp connection interface
zuudevs 3d07974
feat: implement tcp connection candidate iteration
zuudevs 16ec954
test: add tcp connection coverage
zuudevs b428382
fix: make tcp connection header self contained
zuudevs b976ec2
fix: classify empty endpoint lists as connect failure
zuudevs 5ed2cb8
build: wire tcp connection sources
zuudevs f0faafd
test: wire tcp connection tests
zuudevs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}; | ||
| } | ||
| #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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On POSIX, if a signal is delivered while a connection attempt is pending,
poll()returns-1witherrno == EINTR; this path reportsConnectFailedand abandons a potentially viable endpoint well before the configured deadline. Retry the wait after recomputing the remaining timeout when the error isEINTR, while preserving other errors as failures.Useful? React with 👍 / 👎.