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
15 changes: 15 additions & 0 deletions include/cpp_request/client.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#pragma once

#include <chrono>
#include <cstdint>
#include <string>
#include <string_view>

#include <cpp_request/request.hpp>
Expand All @@ -12,6 +14,12 @@ namespace cpp_request {
class Client final {
public:
Client() noexcept = default;
~Client() noexcept;

Client(const Client&) = delete;
Client& operator=(const Client&) = delete;
Client(Client&& other) noexcept;
Client& operator=(Client&& other) noexcept;

[[nodiscard]] Result<Response> request(const Request& request);

Expand Down Expand Up @@ -41,6 +49,13 @@ class Client final {
}

private:
void close_reusable_connection() noexcept;

std::uintptr_t reusable_socket_token_{0};
bool has_reusable_connection_{false};
std::string reusable_host_;
std::uint16_t reusable_port_{0};

std::chrono::milliseconds connect_timeout_{5000};
std::chrono::milliseconds read_timeout_{30000};
std::chrono::milliseconds write_timeout_{30000};
Expand Down
183 changes: 170 additions & 13 deletions src/core/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,97 @@
#include "http/response_parser.hpp"
#include "net/resolver.hpp"
#include "net/tcp_connection.hpp"
#include "platform/native_socket.hpp"

#include <array>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <string_view>
#include <utility>

namespace cpp_request {
namespace {

[[nodiscard]] constexpr char ascii_lower(char ch) noexcept {
return ch >= 'A' && ch <= 'Z'
? static_cast<char>(ch + ('a' - 'A'))
: ch;
}

[[nodiscard]] bool ascii_iequals(
std::string_view lhs,
std::string_view rhs) noexcept {
if (lhs.size() != rhs.size()) {
return false;
}

for (std::size_t index = 0; index < lhs.size(); ++index) {
if (ascii_lower(lhs[index]) != ascii_lower(rhs[index])) {
return false;
}
}
return true;
}

[[nodiscard]] std::string_view trim_ows(std::string_view value) noexcept {
while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) {
value.remove_prefix(1);
}
while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) {
value.remove_suffix(1);
}
return value;
}

[[nodiscard]] bool contains_token(
std::string_view value,
std::string_view expected) noexcept {
std::size_t begin = 0;
while (begin <= value.size()) {
const std::size_t comma = value.find(',', begin);
const std::size_t end = comma == std::string_view::npos
? value.size()
: comma;
if (ascii_iequals(trim_ows(value.substr(begin, end - begin)), expected)) {
return true;
}
if (comma == std::string_view::npos) {
break;
}
begin = comma + 1;
}
return false;
}

[[nodiscard]] bool request_wants_connection_close(const Request& request) noexcept {
for (const auto& field : request.headers()) {
if (ascii_iequals(field.name, "Connection")
&& contains_token(field.value, "close")) {
return true;
}
}
return false;
}

[[nodiscard]] bool same_origin(
std::string_view stored_host,
std::uint16_t stored_port,
std::string_view host,
std::uint16_t port) noexcept {
return stored_port == port && ascii_iequals(stored_host, host);
}

[[nodiscard]] std::uintptr_t encode_socket_handle(
detail::platform::NativeSocketHandle handle) noexcept {
return static_cast<std::uintptr_t>(handle);
}

[[nodiscard]] detail::platform::NativeSocketHandle decode_socket_handle(
std::uintptr_t token) noexcept {
return static_cast<detail::platform::NativeSocketHandle>(token);
}

[[nodiscard]] std::chrono::milliseconds remaining_timeout(
std::chrono::steady_clock::time_point deadline) noexcept {
const auto now = std::chrono::steady_clock::now();
Expand All @@ -40,27 +121,89 @@ namespace {

} // namespace

Client::~Client() noexcept {
close_reusable_connection();
}

Client::Client(Client&& other) noexcept
: reusable_socket_token_(std::exchange(other.reusable_socket_token_, 0)),
has_reusable_connection_(std::exchange(other.has_reusable_connection_, false)),
reusable_host_(std::move(other.reusable_host_)),
reusable_port_(std::exchange(other.reusable_port_, 0)),
connect_timeout_(other.connect_timeout_),
read_timeout_(other.read_timeout_),
write_timeout_(other.write_timeout_) {}

Client& Client::operator=(Client&& other) noexcept {
if (this == &other) {
return *this;
}

close_reusable_connection();

reusable_socket_token_ = std::exchange(other.reusable_socket_token_, 0);
has_reusable_connection_ = std::exchange(other.has_reusable_connection_, false);
reusable_host_ = std::move(other.reusable_host_);
reusable_port_ = std::exchange(other.reusable_port_, 0);
connect_timeout_ = other.connect_timeout_;
read_timeout_ = other.read_timeout_;
write_timeout_ = other.write_timeout_;
return *this;
}

void Client::close_reusable_connection() noexcept {
if (has_reusable_connection_) {
detail::platform::NativeSocket socket{
decode_socket_handle(reusable_socket_token_)};
socket.close();
}

reusable_socket_token_ = 0;
has_reusable_connection_ = false;
reusable_host_.clear();
reusable_port_ = 0;
}

Result<Response> Client::request(const Request& request_value) {
auto serialized_result = detail::http::serialize_request(request_value);
if (!serialized_result) {
return serialized_result.error();
}
auto serialized = std::move(serialized_result).value();

auto endpoints_result = detail::net::Resolver::resolve(
serialized.url.host(),
serialized.url.port());
if (!endpoints_result) {
return endpoints_result.error();
}
detail::net::TcpConnection connection;
const bool reuse_existing = has_reusable_connection_
&& same_origin(
reusable_host_,
reusable_port_,
serialized.url.host(),
serialized.url.port());

if (reuse_existing) {
connection = detail::net::TcpConnection::adopt_native_handle(
decode_socket_handle(reusable_socket_token_));
reusable_socket_token_ = 0;
has_reusable_connection_ = false;
reusable_host_.clear();
reusable_port_ = 0;
} else {
close_reusable_connection();

auto endpoints_result = detail::net::Resolver::resolve(
serialized.url.host(),
serialized.url.port());
if (!endpoints_result) {
return endpoints_result.error();
}

auto connection_result = detail::net::TcpConnection::connect(
endpoints_result.value(),
connect_timeout_);
if (!connection_result) {
return connection_result.error();
auto connection_result = detail::net::TcpConnection::connect(
endpoints_result.value(),
connect_timeout_);
if (!connection_result) {
return connection_result.error();
}
connection = std::move(connection_result).value();
}
auto connection = std::move(connection_result).value();

const auto write_deadline = std::chrono::steady_clock::now() + write_timeout_;

Expand Down Expand Up @@ -110,7 +253,21 @@ Result<Response> Client::request(const Request& request_value) {
}
}

return parser.take_response();
const bool reusable = connection.connected()
&& parser.connection_reusable()
&& parser.pending_bytes().empty()
&& !request_wants_connection_close(request_value);

Response response = parser.take_response();

if (reusable) {
reusable_socket_token_ = encode_socket_handle(connection.release_native_handle());
has_reusable_connection_ = true;
reusable_host_.assign(serialized.url.host());
reusable_port_ = serialized.url.port();
}

return response;
}

Result<Response> Client::get(std::string_view url) {
Expand Down
9 changes: 9 additions & 0 deletions src/net/tcp_connection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ class TcpConnection final {
const std::vector<Endpoint>& endpoints,
std::chrono::milliseconds timeout);

[[nodiscard]] static TcpConnection adopt_native_handle(
platform::NativeSocketHandle handle) noexcept {
return TcpConnection{platform::NativeSocket{handle}};
}

[[nodiscard]] Result<std::size_t> write_all(
std::string_view data,
std::chrono::milliseconds timeout);
Expand All @@ -43,6 +48,10 @@ class TcpConnection final {
return socket_.get();
}

[[nodiscard]] platform::NativeSocketHandle release_native_handle() noexcept {
return socket_.release();
}

void close() noexcept {
socket_.close();
}
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ add_executable(cpp_request_tests
${CMAKE_CURRENT_SOURCE_DIR}/response_parser_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/response_upgrade_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/client_test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/client_reuse_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
Expand Down
Loading
Loading