From 32466cccf710d12d99485d50d029d4c690146ceb Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Mon, 29 Sep 2025 19:15:12 +0100 Subject: [PATCH 1/9] move code to beast fork --- example/websocket/client/CMakeLists.txt | 1 + example/websocket/client/Jamfile | 1 + .../client/crypto-ai-ssl/CMakeLists.txt | 29 ++ .../websocket/client/crypto-ai-ssl/Jamfile | 24 ++ .../crypto-ai-ssl/historic_price_fetcher.cpp | 275 ++++++++++++++ .../crypto-ai-ssl/historic_price_fetcher.hpp | 103 +++++ .../crypto-ai-ssl/json_price_decoder.hpp | 177 +++++++++ .../crypto-ai-ssl/live_price_listener.cpp | 354 ++++++++++++++++++ .../crypto-ai-ssl/live_price_listener.hpp | 130 +++++++ .../client/crypto-ai-ssl/processor_base.hpp | 28 ++ .../websocket_client_crypto_ai_ssl.cpp | 126 +++++++ 11 files changed, 1248 insertions(+) create mode 100644 example/websocket/client/crypto-ai-ssl/CMakeLists.txt create mode 100644 example/websocket/client/crypto-ai-ssl/Jamfile create mode 100644 example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp create mode 100644 example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp create mode 100644 example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp create mode 100644 example/websocket/client/crypto-ai-ssl/live_price_listener.cpp create mode 100644 example/websocket/client/crypto-ai-ssl/live_price_listener.hpp create mode 100644 example/websocket/client/crypto-ai-ssl/processor_base.hpp create mode 100644 example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp diff --git a/example/websocket/client/CMakeLists.txt b/example/websocket/client/CMakeLists.txt index e399c3daa2..66b61a101f 100644 --- a/example/websocket/client/CMakeLists.txt +++ b/example/websocket/client/CMakeLists.txt @@ -17,5 +17,6 @@ if (OPENSSL_FOUND) add_subdirectory(async-ssl) add_subdirectory(async-ssl-system-executor) add_subdirectory(coro-ssl) + add_subdirectory(crypto-ai-ssl) add_subdirectory(sync-ssl) endif () diff --git a/example/websocket/client/Jamfile b/example/websocket/client/Jamfile index bd4402181d..0f1cec2d23 100644 --- a/example/websocket/client/Jamfile +++ b/example/websocket/client/Jamfile @@ -17,4 +17,5 @@ build-project sync ; build-project async-ssl ; build-project async-ssl-system-executor ; build-project coro-ssl ; +build-project crypto-ai-ssl ; build-project sync-ssl ; diff --git a/example/websocket/client/crypto-ai-ssl/CMakeLists.txt b/example/websocket/client/crypto-ai-ssl/CMakeLists.txt new file mode 100644 index 0000000000..2f1d868e82 --- /dev/null +++ b/example/websocket/client/crypto-ai-ssl/CMakeLists.txt @@ -0,0 +1,29 @@ +# +# Copyright (c) 2025 Mungo Gill +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +# Official repository: https://github.com/boostorg/beast +# + +add_executable(websocket-client-crypto-ai-ssl + Jamfile + historic_price_fetcher.cpp + live_price_listener.cpp + websocket_client_crypto_ai_ssl.cpp) + +source_group("" FILES + Jamfile + historic_price_fetcher.cpp + live_price_listener.cpp + websocket_client_crypto_ai_ssl.cpp) + +target_include_directories(websocket-client-crypto-ai-ssl + PRIVATE ${PROJECT_SOURCE_DIR}) + +target_link_libraries(websocket-client-crypto-ai-ssl + PRIVATE Boost::beast Boost::json Boost::url OpenSSL::SSL OpenSSL::Crypto) + +set_target_properties(websocket-client-crypto-ai-ssl + PROPERTIES FOLDER "example-websocket-client") diff --git a/example/websocket/client/crypto-ai-ssl/Jamfile b/example/websocket/client/crypto-ai-ssl/Jamfile new file mode 100644 index 0000000000..11ca0c237b --- /dev/null +++ b/example/websocket/client/crypto-ai-ssl/Jamfile @@ -0,0 +1,24 @@ +# +# Copyright (c) 2015 Mungo Gill +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +# Official repository: https://github.com/boostorg/beast +# + +import ac ; + +project + : requirements + [ ac.check-library /boost/beast/test//lib-asio-ssl : /boost/beast/test//lib-asio-ssl/static : no ] + ; + +exe websocket-client-crypto-ai-ssl : + historic_price_fetcher.cpp + live_price_listener.cpp + websocket_client_crypto_ai_ssl.cpp + : + coverage:no + ubasan:no + ; diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp new file mode 100644 index 0000000000..4f32689d70 --- /dev/null +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp @@ -0,0 +1,275 @@ +// +// Copyright (c) 2025 Mungo Gill +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance +// + +#include "processor_base.hpp" +#include "historic_price_fetcher.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace boost; +using namespace std::placeholders; + +using namespace beast; // from + +using tcp = boost::asio::ip::tcp; // from + +// Start the asynchronous operation +void historic_price_fetcher::run() +{ + // We use a fixed host + host_ = "api.coinbase.com"; + //host_ = "ws-feed.exchange.coinbase.com"; + + // Set SNI Hostname (many hosts need this to handshake successfully) + if (!SSL_set_tlsext_host_name(stream_.native_handle(), host_.c_str())) + { + beast::error_code ec{ + static_cast(::ERR_get_error()), + net::error::get_ssl_category() }; + return error_handler_(ec, "SNI"); + } + + // Set the expected hostname in the peer certificate for verification + stream_.set_verify_callback(boost::asio::ssl::host_name_verification(host_)); + + // Set up an HTTP GET request message + req_.version(11); + req_.method(http::verb::get); + req_.set(http::field::host, host_); + req_.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + + active_ = true; + + // Look up the domain name + resolver_.async_resolve( + host_, + "443", + [this](error_code ec, tcp::resolver::results_type results) + { + on_resolve(ec, results); + } + ); +} + +void historic_price_fetcher::on_write(beast::error_code ec, std::size_t bytes_transferred) +{ + boost::ignore_unused(bytes_transferred); + + if (ec) { + cancel(); + return error_handler_(ec, "write"); + } + + if (!active_) + return; + + // Read a message into our buffer + http::async_read( + stream_, buffer_, response_, + [this](error_code ec, std::size_t bytes_transferred) + { + on_read(ec, bytes_transferred); + } + ); +} + +void historic_price_fetcher::next_request() +{ + if (coins_.size() > 0) + { + std::string security = coins_.back(); + coins_.pop_back(); + + // Set up an HTTP GET request message + req_.target("/v2/prices/" + security + "/spot"); + + // Set a timeout on the operation + beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); + + // Send the message + http::async_write(stream_, req_, + [this](error_code ec, std::size_t bytes_transferred) + { + on_write(ec, bytes_transferred); + } + ); + } + else { + cancel(); + } +} + +void historic_price_fetcher::on_ssl_handshake(beast::error_code ec) +{ + if (ec) { + cancel(); + return error_handler_(ec, "ssl_handshake"); + } + + if (!active_) + return; + + if (coins_.size() > 0) + next_request(); + else { + cancel(); + } +} + +void historic_price_fetcher::on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type ep) +{ + boost::ignore_unused(ep); + + if (ec) { + cancel(); + return error_handler_(ec, "connect"); + } + + if (!active_) + return; + + // Set a timeout on the operation + beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); + + // Perform the SSL handshake + stream_.async_handshake( + boost::asio::ssl::stream_base::client, + [this](error_code ec) + { + on_ssl_handshake(ec); + } + ); +} + +void historic_price_fetcher::on_resolve(beast::error_code ec, tcp::resolver::results_type results) +{ + if (ec) { + cancel(); + return error_handler_(ec, "resolve"); + } + + if (!active_) + return; + + // Set a timeout on the operation + beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); + + // Make the connection on the IP address we get from a lookup + beast::get_lowest_layer(stream_).async_connect( + results, + [this](error_code ec, tcp::resolver::results_type::endpoint_type ep) + { + on_connect(ec, ep); + } + ); +} + +void historic_price_fetcher::cancel() +{ + active_ = false; + + net::post(strand_, [this]() + { + if (beast::get_lowest_layer(stream_).socket().is_open()) + stream_.async_shutdown( + [this](error_code ec) + { + on_shutdown(ec); + } + ); + } + ); +} + +void historic_price_fetcher::on_read(beast::error_code ec, std::size_t bytes_transferred) +{ + boost::ignore_unused(bytes_transferred); + + // This indicates that the session was closed + if (ec == http::error::end_of_stream) { + cancel(); + return; + } + else if (ec) { + cancel(); + if (active_) return error_handler_(ec, "read"); + } + + if (!active_) + return; + + // Write the message to standard out + //std::cout << "Response: " << response_ << "\n" << std::endl; + //std::cout << "Body: " << response_.body() << "\n\n" << std::endl; + + // The response can be quite long, and we can avoid an allocation + // by swapping the body into an empty string. + std::string temp; + temp.swap(response_.body()); + + receive_handler_(std::move(temp)); + + next_request(); + + //// Set a timeout on the operation + //beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); + + //// Gracefully close the stream + //stream_.async_shutdown( + // [this](error_code ec) + // { + // on_shutdown(ec); + // } + //); + + //receive_handler_(beast::buffers_to_string(buffer_.data())); + + /* + buffer_.consume(buffer_.max_size()); + + count++; + if (count > 3) cancel(); + + ws_.async_read( + buffer_, + [this] (error_code ec, std::size_t bytes_transferred) + { + on_read(ec, bytes_transferred); + } + ); + */ +} + +void historic_price_fetcher::on_shutdown(beast::error_code ec) +{ + if (ec && ec != boost::asio::ssl::error::stream_truncated) + return error_handler_(ec, "shutdown"); + + // If we get here then the connection is closed gracefully + + // The make_printable() function helps print a ConstBufferSequence + std::cout << "Final buffer content:" << beast::make_printable(buffer_.data()) << std::endl; +} diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp new file mode 100644 index 0000000000..d55bf716fb --- /dev/null +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp @@ -0,0 +1,103 @@ +// +// Copyright (c) 2025 Mungo Gill +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance +// + +#ifndef BOOST_BEAST_EXAMPLE_HISTORIC_PRICE_FETCHER +#define BOOST_BEAST_EXAMPLE_HISTORIC_PRICE_FETCHER + +#include +#include +#include +#include +#include +#include + +#include + +#include "processor_base.hpp" + + +//using namespace boost; + +// Opens a websocket and subsscribes to price ticks +class historic_price_fetcher : public processor_base +{ + std::function receive_handler_; + std::function error_handler_; + + boost::asio::strand strand_; + boost::asio::ip::tcp::resolver resolver_; + boost::asio::ssl::stream stream_; + + boost::beast::flat_buffer buffer_; + boost::beast::http::request req_; + boost::beast::http::response response_; + + std::string host_; + std::vector coins_ = { "BTC-USD", "ETH-USD" }; + + bool active_; + +public: + // Resolver and socket require an io_context + explicit + historic_price_fetcher( + boost::asio::io_context& ioc + , boost::asio::ssl::context& ctx + , const std::vector& coins + , std::function receive_handler + , std::function err_handler) + : receive_handler_(receive_handler) + , error_handler_(err_handler) + , strand_(boost::asio::make_strand(ioc)) + , resolver_(strand_) + , stream_(strand_, ctx) + , coins_(coins) + , active_(false) + { + } + + // Start the asynchronous operation + void + run(); + + void + cancel() override; + +private: + void + on_resolve( + boost::beast::error_code ec, + boost::asio::ip::tcp::resolver::results_type results); + + void + on_connect( + boost::beast::error_code ec, + boost::asio::ip::tcp::resolver::results_type::endpoint_type ep); + + void + on_ssl_handshake(boost::beast::error_code ec); + + void + next_request(); + + void + on_write( + boost::beast::error_code ec, + std::size_t bytes_transferred); + + void + on_read( + boost::beast::error_code ec, + std::size_t bytes_transferred); + + void + on_shutdown(boost::beast::error_code ec); +}; + +#endif diff --git a/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp b/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp new file mode 100644 index 0000000000..0da198b155 --- /dev/null +++ b/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp @@ -0,0 +1,177 @@ +// +// Copyright (c) 2025 Mungo Gill +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance +// + +#ifndef BOOST_BEAST_EXAMPLE_JSON_PRICE_DECODER_H +#define BOOST_BEAST_EXAMPLE_JSON_PRICE_DECODER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "processor_base.hpp" + +using namespace boost; +using namespace std::placeholders; + +using namespace beast; // from +using namespace http; // from +using namespace websocket; // from +using namespace json; + +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + +// Opens a websocket and subsscribes to price ticks +class json_price_decoder : public processor_base +{ + std::function receive_handler_; + std::function error_handler_; + net::execution_context &ctx_; + bool active_; + +public: + // Resolver and socket require an io_context + explicit + json_price_decoder(net::execution_context& ec + , std::function receive_handler + , std::function err_handler) + : receive_handler_(receive_handler) + , error_handler_(err_handler) + , ctx_(ec) + , active_(false) + { + } + + // Start the asynchronous operation + void + run() + { + active_ = true; + } + + void + post(input_type type, std::string &&str) + { + if (!active_) + return; + + net::post(net::get_associated_executor(ctx_), [this, type, s = std::move(str)] () mutable + { + on_process(type, std::move(s)); + } + ); + } + + void + cancel() override + { + active_ = false; + } + +private: + + inline std::time_t time_gm(struct tm* tm) { +#if defined(_DEFAULT_SOURCE) // Feature test for glibc + return timegm(tm); +#elif defined(_MSC_VER) // Test for Microsoft C/C++ + return _mkgmtime(tm); +#else +#error "Neither timegm nor _mkgmtime available" +#endif + } + + void + on_process( + input_type type + , std::string &&str) + { + if (!active_) + return; + + boost::system::error_code ec; + monotonic_resource mr; + const value jv = parse(str, ec, &mr); + + if (ec) + return error_handler_(ec, "json_price_decoder::on_process"); + + if (type == input_type::LIVE) + { + try { + if (jv.as_object().at("type").as_string() != "ticker") + return; + + json::string_view productstr = jv.as_object().at("product_id").as_string(); + + json::string_view pricestr = jv.as_object().at("price").as_string(); + + std::size_t str_size = 0; + double price = std::stod(pricestr, &str_size); + + json::string_view timestr = jv.as_object().at("time").as_string(); + + std::tm t = {}; // tm_isdst = 0 + std::istringstream ss(timestr); + ss.imbue(std::locale()); // "LANG=C" + + ss >> std::get_time(&t, "%Y-%m-%dT%H:%M:%S."); + if (ss.fail()) + return error_handler_(ec, "json_price_decoder::on_process parse failure"); + + // fix up the day of week, day of year etc + t.tm_isdst = 0; + t.tm_wday = -1; // a canary for a time_gm error + + std::time_t epoch_time = time_gm(&t); + + if (epoch_time == -1 || t.tm_wday == -1) // "real error" + return error_handler_(ec, "json_price_decoder::on_process parse failure"); + + const auto price_time = std::chrono::system_clock::from_time_t(epoch_time); + + std::cout << "Decoded live " << productstr << " price: " << price << " at " << price_time << std::endl; + + } + catch (boost::system::system_error se) { + return error_handler_(ec, "json_price_decoder::on_process parse failure"); + } + catch (std::invalid_argument se) { + return error_handler_(ec, "json_price_decoder::on_process parse failure"); + } + catch (std::out_of_range se) { + return error_handler_(ec, "json_price_decoder::on_process parse failure"); + } + } + if (type == input_type::HISTORIC) + { + std::cerr << "historic decoding not yet written" << std::endl; + } + } +}; + +#endif + diff --git a/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp b/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp new file mode 100644 index 0000000000..fb973a83b2 --- /dev/null +++ b/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp @@ -0,0 +1,354 @@ +// +// Copyright (c) 2025 Mungo Gill +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance +// + +#include "processor_base.hpp" +#include "live_price_listener.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + + +using namespace boost; +using namespace std::placeholders; + +using namespace beast; // from +using namespace http; // from +using namespace websocket; // from + +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + +// Start the asynchronous operation +void live_price_listener::run() +{ + + + // Set SNI Hostname (many hosts need this to handshake successfully) + // Note that ws_.next_layer() references the asio::ssl::stream object. + // Note, SSL_set_tlsext_host_name is an OpenSSL C function, not + // part of asio or beast. + if (!SSL_set_tlsext_host_name(ws_.next_layer().native_handle(), host_.c_str())) + { + beast::error_code ec{ + static_cast(::ERR_get_error()), + net::error::get_ssl_category() }; + return error_handler_(ec, "SNI"); + } + + // Set the expected hostname in the peer certificate for verification. + // OpenSSL will, whenever a certificate is received, call this function + // to check that the certificate's host matches what we think it should be. + ws_.next_layer().set_verify_callback(boost::asio::ssl::host_name_verification(host_)); + + // Ensure any future callbacks do not early-exit. + // (design note: could also have been done at construction time). + active_ = true; + + // Request that ASIO lookup the domain name. For the sake of this example we have + // hard-coded the port to 443 (the usual port for this). + // For contrast with other examples, this has been written using a lambda, but + // `beast::bind_front_handler` is an equally viable alternative. + resolver_.async_resolve( + host_, + "443", + [this](error_code ec, tcp::resolver::results_type results) + { + // Note that `results` is actually an iterator into a container of + // endpoints representing all the IP addresses found by the DNS lookup. + on_resolve(ec, results); + } + ); +} + +void live_price_listener::cancel() +{ + // We set active_=false to rapidly consume all the pending + // completion handlers. + active_ = false; + + // We use net::post to ensure the websocket closure takes place after + // all the currently pending completion handlers. + net::post(strand_, [this]() + { + // If the websocket is still open, close it. + if (ws_.is_open()) + ws_.async_close(websocket::close_code::normal, + [this](error_code ec) + { + on_close(ec); + } + ); + } + ); +} + +// This is the function called when hostname resolution completes. +void live_price_listener::on_resolve(beast::error_code ec, tcp::resolver::results_type results) +{ + // In the event of an error call the `cancel` function which will drain + // any pending completion handlers. In this case the websocket is not yet + // open so the `cancel` function will not attempt to close it. + if (ec) { + cancel(); + return error_handler_(ec, "resolve"); + } + + // If we have been asked to shut down then do no processing. + if (!active_) + return; + + // Set the timeout for the operation. Note that this needs to be set + // each time to reset the countdown. + // This is applied on the underlying socket because neither the ssl + // layer nor the websocket layer have been started yet. + beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30)); + + // Make the connection on on of the IP address we got from the lookup. + // If multiple IP addresses were found then the first one to sucessfully + // connect is used. + // ws_ has 3 layers websocket->ssl->socket + // `get_lowest_layer` returns the bottom-most socket. + beast::get_lowest_layer(ws_).async_connect( + results, + [this](error_code ec, tcp::resolver::results_type::endpoint_type ep) + { + // Note that the endpoint `ep` represents the single IP to which + // we successfully connected (if any). + on_connect(ec, ep); + } + ); +} + +// Once the underlying socket is connected, this function performs the next step, +// namely getting the SSL layer running. +void live_price_listener::on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type ep) +{ + if (ec) { + // In the event of a connection error call the `cancel` function which will drain + // any pending completion handlers. In this case the websocket is not yet + // open so the `cancel` function will not attempt to close it. + cancel(); + return error_handler_(ec, "connect"); + } + + // If we have been asked to shut down then do no further processing. + if (!active_) + return; + + // Set the timeout for the operation. Note that this needs to be set + // each time to reset the countdown. + // This is applied on the underlying socket because neither the ssl + // layer nor the websocket layer have been started yet. + beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30)); + + // Update the host_ string to add the port. This will provide the value of the + // Host HTTP header during the WebSocket handshake. + // See https://tools.ietf.org/html/rfc7230#section-5.4 + host_ += ':' + std::to_string(ep.port()); + + // Perform the SSL handshake + // ws_ has 3 layers websocket->ssl->socket + // `get_next_layer` returns the ssl layer. + ws_.next_layer().async_handshake( + boost::asio::ssl::stream_base::client, + [this](error_code ec) + { + on_ssl_handshake(ec); + } + ); +} + +void live_price_listener::on_ssl_handshake(beast::error_code ec) +{ + if (ec) { + // In the event of an ssl error call the `cancel` function which will drain + // any pending completion handlers. In this case the websocket is not yet + // open so the `cancel` function will not attempt to close it. + cancel(); + return error_handler_(ec, "ssl_handshake"); + } + + // If we have been asked to shut down then do no further processing. + if (!active_) + return; + + // Turn off the timeout on the tcp_stream, because + // the websocket stream has its own timeout system. + beast::get_lowest_layer(ws_).expires_never(); + + // Set suggested timeout settings for the websocket + ws_.set_option( + websocket::stream_base::timeout::suggested( + beast::role_type::client)); + + // We need to set the User-Agent of the handshake. Beast's websocket + // requires that this be done using a decorator. + ws_.set_option(websocket::stream_base::decorator( + [](websocket::request_type& req) + { + req.set(http::field::user_agent, + std::string(BOOST_BEAST_VERSION_STRING) + + " websocket-client-async-ssl"); + }) + ); + + // The websocket should use deflate where possible, to reduce bandwidth + // by compressing the messages on the wire. + // This requires that zlib be included as a dependency. + websocket::permessage_deflate opt; + opt.client_enable = true; // for clients + opt.server_enable = true; // for servers + ws_.set_option(opt); + + // Perform the websocket handshake + ws_.async_handshake(host_, "/", + [this](error_code ec) + { + on_handshake(ec); + } + ); +} + +// This is the function that is called when the websocket is up and usable. +// The previous steps were relatively generic across all websocket connections, +// and from this point on we need to include business logic. +void live_price_listener::on_handshake(beast::error_code ec) +{ + if (ec) { + cancel(); + return error_handler_(ec, "handshake"); + } + + // If we have been asked to shut down then do no further processing. + if (!active_) + return; + + // Construct a coinbase json subscription message, using Boost::json + json::value jv = { + { "type", "subscribe" }, + { "product_ids", json::array(coins_.cbegin(), coins_.cend()) }, + { "channels", json::array{ + "heartbeat", + "ticker_batch" } + } + }; + + // Convert the json object into a string. + std::string subscribe_json_str = serialize(jv); + + // Send the subscription message to the server. + ws_.async_write( + net::buffer(subscribe_json_str), + [this, len=subscribe_json_str.size()](error_code ec, std::size_t bytes_transferred) + { + on_write(ec, len, bytes_transferred); + } + ); +} + +void live_price_listener::on_write( + beast::error_code ec + , std::size_t bytes_required + , std::size_t bytes_transferred) +{ + // Check for errors and verify that the byte count sent in the subscription message + // is what we expected. + if (ec) { + cancel(); + return error_handler_(ec, "write"); + } + else if (bytes_transferred < bytes_required) { + cancel(); + // TODO: figure out what to put in ec here. + return error_handler_(ec, "write"); + } + + // If we have been asked to shut down then do no further processing. + if (!active_) + return; + + // Read a message into our buffer. Note that buffer_ is a member variable as it has + // to persist for the life of the read and until the on_read completion handler is + // finished. + ws_.async_read( + buffer_, + [this](error_code ec, std::size_t bytes_transferred) + { + on_read(ec, bytes_transferred); + } + ); +} + +void live_price_listener::on_read(beast::error_code ec, std::size_t bytes_transferred) +{ + boost::ignore_unused(bytes_transferred); + + // This indicates that the session was closed + if (ec == websocket::error::closed) { + cancel(); + return; + } + else if (ec) { + cancel(); + if (active_) return error_handler_(ec, "read"); + } + + // If we have been asked to shut down then do no further processing. + if (!active_) + return; + + //std::cout << "Interim: " << beast::make_printable(buffer_.data()) << "\n\n" << std::endl; + + receive_handler_(beast::buffers_to_string(buffer_.data())); + + // Erase the const section of the dynamic buffer. We know that the mutable section + // does not contain any data at this point, so this call merely updates internal + // buffer pointers rather than having to call memmove. + buffer_.consume(buffer_.max_size()); + + count++; + if (count > 10) cancel(); + + // This is a very common idiom in async programming. As soon as a read completes, we + // initiate another asynchronous read, almost like an infinite loop. + ws_.async_read( + buffer_, + [this](error_code ec, std::size_t bytes_transferred) + { + on_read(ec, bytes_transferred); + } + ); +} + +void live_price_listener::on_close(beast::error_code ec) +{ + if (ec) + return error_handler_(ec, "close"); + + // If we get here then the connection is closed gracefully + + // The make_printable() function helps print a ConstBufferSequence + std::cout << "Final buffer content:" << beast::make_printable(buffer_.data()) << std::endl; +} diff --git a/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp b/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp new file mode 100644 index 0000000000..695653ceeb --- /dev/null +++ b/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp @@ -0,0 +1,130 @@ +// +// Copyright (c) 2025 Mungo Gill +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance +// + +#ifndef BOOST_BEAST_EXAMPLE_LIVE_PRICE_LISTENER +#define BOOST_BEAST_EXAMPLE_LIVE_PRICE_LISTENER + +#include +#include +#include +#include +#include +#include + +#include + +#include "processor_base.hpp" + +//using namespace boost; + +// Opens a websocket and subsscribes to price ticks +class live_price_listener : public processor_base +{ + // This holds the function called when a live price is received. + std::function receive_handler_; + + // This holds the function called when an error happens. + std::function error_handler_; + + // We want to ensure that operations to set up the websocket are performed in order, + // and that we do not attempt to call async_read when another async_read is in progress. + // This strand needs to persist as long as the websocket is in use. + // Design note: we could "get away" without using a strand, because this class is structured + // so that the next async_* function is not called until *after* the previous asynchronous + // function is complete. However the strand is included since it makes our threading assumptions + // explicit for future developers. + boost::asio::strand strand_; + + // The resolver's role is to perform DNS lookups from a hostname to a set of ip addresses. + boost::asio::ip::tcp::resolver resolver_; + + // The key structure in this listener is the websocket itself. + boost::beast::websocket::stream> ws_; + + // Any calls to async_read need to be passed a buffer into which the response will + // be written. That buffer needs to persist until after the read completes and the + // completion handler is called. + boost::beast::flat_buffer buffer_; + + // The host will be used at multiple stages during the websocket's setup process. + std::string host_; + + // A list of coins that we want to get the prices for. + std::vector coins_; + + // Provide a mechanism to exit the websocket subscription. When active_ is false, + // no more asynchronous calls will be made, and every completion handler called will exit + // immediately. + bool active_; + + int count = 0; + +public: + // It is worth noting that we do not retain the reference to the passed-in io_context. + // The reason for this is that we use a strand to ensure the + explicit + live_price_listener( + boost::asio::io_context& ioc + , boost::asio::ssl::context& ctx + , const std::vector& coins + , std::function receive_handler + , std::function err_handler) + : receive_handler_(receive_handler) + , error_handler_(err_handler) + , strand_(boost::asio::make_strand(ioc)) + , resolver_(strand_) + , ws_(strand_, ctx) + , coins_(coins) + , active_(false) + { + // For this example hard-code the host. + //host_ = "ws-feed-public.sandbox.exchange.coinbase.com"; + host_ = "ws-feed.exchange.coinbase.com"; + } + + // Start the asynchronous operation + void + run(); + + void + cancel() override; + +private: + void + on_resolve( + boost::beast::error_code ec, + boost::asio::ip::tcp::resolver::results_type results); + + void + on_connect( + boost::beast::error_code ec, + boost::asio::ip::tcp::resolver::results_type::endpoint_type ep); + + void + on_ssl_handshake(boost::beast::error_code ec); + + void + on_handshake(boost::beast::error_code ec); + + void + on_write( + boost::beast::error_code ec, + std::size_t bytes_required, + std::size_t bytes_transferred); + + void + on_read( + boost::beast::error_code ec, + std::size_t bytes_transferred); + + void + on_close(boost::beast::error_code ec); +}; + +#endif diff --git a/example/websocket/client/crypto-ai-ssl/processor_base.hpp b/example/websocket/client/crypto-ai-ssl/processor_base.hpp new file mode 100644 index 0000000000..c8750e95c2 --- /dev/null +++ b/example/websocket/client/crypto-ai-ssl/processor_base.hpp @@ -0,0 +1,28 @@ +// +// Copyright (c) 2025 Mungo Gill +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance +// + +#ifndef BOOST_BEAST_EXAMPLE_PROCESSOR_BASE_H +#define BOOST_BEAST_EXAMPLE_PROCESSOR_BASE_H + +class processor_base { + +public: + + enum class input_type { + LIVE, + HISTORIC + }; + + virtual void cancel() = 0; + + virtual ~processor_base() {} +}; + +#endif + diff --git a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp new file mode 100644 index 0000000000..88b48fe2a4 --- /dev/null +++ b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp @@ -0,0 +1,126 @@ +// +// Copyright (c) 2025 Mungo Gill +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance +// + +#include "example/common/root_certificates.hpp" + +#include "historic_price_fetcher.hpp" +#include "live_price_listener.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "json_price_decoder.hpp" + +#include +#include + + +using namespace boost; +using namespace std::placeholders; + +using namespace beast; // from +using namespace http; // from +using namespace websocket; // from + +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + +// Report a failure +void +fail(beast::error_code ec, char const* what) +{ + std::cerr << what << ": " << ec.message() << "\n"; +} + +//------------------------------------------------------------------------------ + +int main(int argc, char** argv) +{ + // Check command line arguments. + if (argc != 2 && argc != 3) + { + std::cerr << + "Usage: " << *argv << " [] \n" << + "Example:\n" << + " " << *argv << " 'BTC-USD,ETH-USD' \n" << + " " << *argv << " 'BTC-USD,ETH-USD' ABC-DEF-GHI-JKL\n"; + return EXIT_FAILURE; + } + + std::string coin_list_str(argv[1]); + std::vector coins; + + char_separator separator(", "); + tokenizer> tokens(coin_list_str, separator); + for (auto it = tokens.begin(); it != tokens.end(); it++) { + coins.push_back(*it); + }; + + // The SSL context is required, and holds certificates + ssl::context ctx{ ssl::context::tlsv12_client }; + + // Verify the remote server's certificate + ctx.set_verify_mode(ssl::verify_peer); + + // This holds the root certificate used for verification + load_root_certificates(ctx); + + net::thread_pool decoder_tp(1); + + auto decoded_recv = [](const std::string& symbol, double price) { + std::cout << "Decoded Recv" << symbol << ":" << price << "\n" << std::endl; + }; + + json_price_decoder decoder_worker(decoder_tp, decoded_recv, fail); + + decoder_worker.run(); + + auto live_input_recv = [&decoder_worker](std::string&& v) { + //std::cout << "Live input Recv" << v << "\n" << std::endl; + decoder_worker.post(processor_base::input_type::LIVE, std::move(v)); + }; + + auto historic_input_recv = [&decoder_worker](std::string&& v) { + std::cout << "Historic input Recv" << v << "\n" << std::endl; + decoder_worker.post(processor_base::input_type::HISTORIC, std::move(v)); + }; + + // The io_context is required for all I/O + net::io_context listen_ioc; + + // Construct and start a the fetcher of historic prices. + historic_price_fetcher historic_fetcher(listen_ioc, ctx, coins, historic_input_recv, fail); + historic_fetcher.run(); + + // Construct and start a the websocket listener. + live_price_listener listen_worker(listen_ioc, ctx, coins, live_input_recv, fail); + listen_worker.run(); + + // Run the I/O service. The call will return when + // the socket is closed. + listen_ioc.run(); + + decoder_tp.join(); + + return EXIT_SUCCESS; +} From 86e9baf1016a7d011ad715ff2bd8606b8f5e9e1a Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Thu, 2 Oct 2025 17:44:04 +0100 Subject: [PATCH 2/9] work in progress --- .../client/crypto-ai-ssl/CMakeLists.txt | 2 + .../websocket/client/crypto-ai-ssl/Jamfile | 1 + .../crypto-ai-ssl/historic_price_fetcher.cpp | 233 ++++++++++++------ .../crypto-ai-ssl/historic_price_fetcher.hpp | 24 +- .../crypto-ai-ssl/json_price_decoder.hpp | 6 +- .../crypto-ai-ssl/live_price_listener.cpp | 97 +++++++- .../crypto-ai-ssl/live_price_listener.hpp | 18 +- .../client/crypto-ai-ssl/price_store.hpp | 126 ++++++++++ .../websocket_client_crypto_ai_ssl.cpp | 33 +-- 9 files changed, 435 insertions(+), 105 deletions(-) create mode 100644 example/websocket/client/crypto-ai-ssl/price_store.hpp diff --git a/example/websocket/client/crypto-ai-ssl/CMakeLists.txt b/example/websocket/client/crypto-ai-ssl/CMakeLists.txt index 2f1d868e82..e415e5bfd1 100644 --- a/example/websocket/client/crypto-ai-ssl/CMakeLists.txt +++ b/example/websocket/client/crypto-ai-ssl/CMakeLists.txt @@ -9,12 +9,14 @@ add_executable(websocket-client-crypto-ai-ssl Jamfile + ai_querier.cpp historic_price_fetcher.cpp live_price_listener.cpp websocket_client_crypto_ai_ssl.cpp) source_group("" FILES Jamfile + ai_querier.cpp historic_price_fetcher.cpp live_price_listener.cpp websocket_client_crypto_ai_ssl.cpp) diff --git a/example/websocket/client/crypto-ai-ssl/Jamfile b/example/websocket/client/crypto-ai-ssl/Jamfile index 11ca0c237b..6b8ae75055 100644 --- a/example/websocket/client/crypto-ai-ssl/Jamfile +++ b/example/websocket/client/crypto-ai-ssl/Jamfile @@ -15,6 +15,7 @@ project ; exe websocket-client-crypto-ai-ssl : + ai_querier.cpp historic_price_fetcher.cpp live_price_listener.cpp websocket_client_crypto_ai_ssl.cpp diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp index 4f32689d70..bb10abd654 100644 --- a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp @@ -22,6 +22,9 @@ #include #include #include +#include +#include +#include #include #include @@ -36,10 +39,22 @@ using namespace beast; // from using tcp = boost::asio::ip::tcp; // from + +static inline std::time_t my_time_gm(struct tm* tm) { +#if defined(_DEFAULT_SOURCE) // Feature test for glibc + return timegm(tm); +#elif defined(_MSC_VER) // Test for Microsoft C/C++ + return _mkgmtime(tm); +#else +#error "Neither timegm nor _mkgmtime available" +#endif +} + // Start the asynchronous operation void historic_price_fetcher::run() { - // We use a fixed host + // For this example use a hard-coded host name. + // In reality this would be stored in some form of configuration. host_ = "api.coinbase.com"; //host_ = "ws-feed.exchange.coinbase.com"; @@ -63,10 +78,15 @@ void historic_price_fetcher::run() active_ = true; + // Find the subscription start time + posix_time::ptime ptime = posix_time::second_clock::universal_time(); + posix_time::ptime sod = posix_time::ptime(ptime.date()); + start_of_day_ = posix_time::to_time_t(sod); + // Look up the domain name resolver_.async_resolve( host_, - "443", + "https", [this](error_code ec, tcp::resolver::results_type results) { on_resolve(ec, results); @@ -74,69 +94,44 @@ void historic_price_fetcher::run() ); } -void historic_price_fetcher::on_write(beast::error_code ec, std::size_t bytes_transferred) +void historic_price_fetcher::cancel() { - boost::ignore_unused(bytes_transferred); - - if (ec) { - cancel(); - return error_handler_(ec, "write"); - } - - if (!active_) - return; + active_ = false; - // Read a message into our buffer - http::async_read( - stream_, buffer_, response_, - [this](error_code ec, std::size_t bytes_transferred) + net::post(strand_, [this]() { - on_read(ec, bytes_transferred); + if (beast::get_lowest_layer(stream_).socket().is_open()) + stream_.async_shutdown( + [this](error_code ec) + { + on_shutdown(ec); + } + ); } ); } -void historic_price_fetcher::next_request() -{ - if (coins_.size() > 0) - { - std::string security = coins_.back(); - coins_.pop_back(); - - // Set up an HTTP GET request message - req_.target("/v2/prices/" + security + "/spot"); - - // Set a timeout on the operation - beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); - - // Send the message - http::async_write(stream_, req_, - [this](error_code ec, std::size_t bytes_transferred) - { - on_write(ec, bytes_transferred); - } - ); - } - else { - cancel(); - } -} - -void historic_price_fetcher::on_ssl_handshake(beast::error_code ec) +void historic_price_fetcher::on_resolve(beast::error_code ec, tcp::resolver::results_type results) { if (ec) { cancel(); - return error_handler_(ec, "ssl_handshake"); + return error_handler_(ec, "resolve"); } if (!active_) return; - if (coins_.size() > 0) - next_request(); - else { - cancel(); - } + // Set a timeout on the operation + beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); + + // Make the connection on the IP address we get from a lookup + beast::get_lowest_layer(stream_).async_connect( + results, + [this](error_code ec, tcp::resolver::results_type::endpoint_type ep) + { + on_connect(ec, ep); + } + ); } void historic_price_fetcher::on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type ep) @@ -164,46 +159,84 @@ void historic_price_fetcher::on_connect(beast::error_code ec, tcp::resolver::res ); } -void historic_price_fetcher::on_resolve(beast::error_code ec, tcp::resolver::results_type results) +void historic_price_fetcher::on_ssl_handshake(beast::error_code ec) { if (ec) { cancel(); - return error_handler_(ec, "resolve"); + return error_handler_(ec, "ssl_handshake"); } if (!active_) return; - // Set a timeout on the operation - beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); + if (coins_.size() > 0) + next_request(); + else { + cancel(); + } +} - // Make the connection on the IP address we get from a lookup - beast::get_lowest_layer(stream_).async_connect( - results, - [this](error_code ec, tcp::resolver::results_type::endpoint_type ep) - { - on_connect(ec, ep); - } - ); +void historic_price_fetcher::next_request() +{ + if (coins_.size() > 0) + { + current_coin_ = coins_.back(); + coins_.pop_back(); + + urls::url url = + urls::format( + "{}://api.coinbase.com/api/v3/brokerage/market/products/{}/candles", + "https", + current_coin_); + + // Data + url.params().append({ "granularity", "ONE_MINUTE"}); + //url.params().append({ "start", std::to_string(start_of_day_) }); + url.params().append({ "limit", std::to_string(5)}); + + // Set up an HTTP GET request message + req_.target(url); + //req_.target("/v2/prices/" + security + "/spot"); + + // Set a timeout on the operation + beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); + + // Send the message + http::async_write(stream_, req_, + [this](error_code ec, std::size_t bytes_transferred) + { + on_write(ec, bytes_transferred); + } + ); + } + else { + cancel(); + } } -void historic_price_fetcher::cancel() +void historic_price_fetcher::on_write(beast::error_code ec, std::size_t bytes_transferred) { - active_ = false; + boost::ignore_unused(bytes_transferred); - net::post(strand_, [this]() + if (ec) { + cancel(); + return error_handler_(ec, "write"); + } + + if (!active_) + return; + + // Read a message into our buffer + http::async_read( + stream_, buffer_, response_, + [this](error_code ec, std::size_t bytes_transferred) { - if (beast::get_lowest_layer(stream_).socket().is_open()) - stream_.async_shutdown( - [this](error_code ec) - { - on_shutdown(ec); - } - ); + on_read(ec, bytes_transferred); } ); } + void historic_price_fetcher::on_read(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); @@ -223,14 +256,16 @@ void historic_price_fetcher::on_read(beast::error_code ec, std::size_t bytes_tra // Write the message to standard out //std::cout << "Response: " << response_ << "\n" << std::endl; - //std::cout << "Body: " << response_.body() << "\n\n" << std::endl; + std::cout << "Body: " << response_.body() << "\n\n" << std::endl; // The response can be quite long, and we can avoid an allocation // by swapping the body into an empty string. std::string temp; temp.swap(response_.body()); - receive_handler_(std::move(temp)); + parse_json(temp); + + //receive_handler_(std::move(temp)); next_request(); @@ -263,6 +298,56 @@ void historic_price_fetcher::on_read(beast::error_code ec, std::size_t bytes_tra */ } + +void historic_price_fetcher::parse_json(core::string_view str) +{ + parser_.reset(); + + boost::system::error_code ec; + parser_.write(str, ec); + + if (ec) + return error_handler_(ec, "historic_price_fetcher::parse_json"); + + json::value jv(parser_.release()); + + // Design note: the json parsing could be done without using the exception + // interface, but the resulting code would be considerably more verbose. + try { + auto candle_list = jv.as_object().at("candles").as_array(); + + if (candle_list.size() == 0) + return error_handler_(ec, "historic_price_fetcher::parse_json no prices"); + + // The coinbase API provides the most recent values first. + for (auto it = candle_list.crbegin(); it != candle_list.crend(); it++) { + core::string_view startstr = it->as_object().at("start").as_string(); + core::string_view openstr = it->as_object().at("open").as_string(); + core::string_view closestr = it->as_object().at("close").as_string(); + + std::size_t str_size = 0; + std::time_t start_time = std::stod(startstr, &str_size); + if (start_time > start_of_day_) { + double open = std::stod(openstr, &str_size); + + if (receive_handler_) + receive_handler_(current_coin_, std::chrono::system_clock::from_time_t(start_time), open); + + std::cout << "Decoded historic " << current_coin_ << " price: " << open << " at " << std::chrono::system_clock::from_time_t(start_time) << std::endl; + } + } + } + catch (boost::system::system_error se) { + return error_handler_(ec, "historic_price_fetcher::parse_json parse failure"); + } + catch (std::invalid_argument se) { + return error_handler_(ec, "historic_price_fetcher::parse_json parse failure"); + } + catch (std::out_of_range se) { + return error_handler_(ec, "historic_price_fetcher::parse_json parse failure"); + } +} + void historic_price_fetcher::on_shutdown(beast::error_code ec) { if (ec && ec != boost::asio::ssl::error::stream_truncated) diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp index d55bf716fb..4833193581 100644 --- a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include @@ -27,7 +28,10 @@ // Opens a websocket and subsscribes to price ticks class historic_price_fetcher : public processor_base { - std::function receive_handler_; + std::function receive_handler_; std::function error_handler_; boost::asio::strand strand_; @@ -39,10 +43,17 @@ class historic_price_fetcher : public processor_base boost::beast::http::response response_; std::string host_; - std::vector coins_ = { "BTC-USD", "ETH-USD" }; + std::vector coins_; + std::string current_coin_; + + std::time_t start_of_day_; bool active_; + // It is more efficient to persist the json parser so that memory allocation does not need + // to be repeated each time we docode a message + boost::json::parser parser_; + public: // Resolver and socket require an io_context explicit @@ -50,7 +61,10 @@ class historic_price_fetcher : public processor_base boost::asio::io_context& ioc , boost::asio::ssl::context& ctx , const std::vector& coins - , std::function receive_handler + , std::function receive_handler , std::function err_handler) : receive_handler_(receive_handler) , error_handler_(err_handler) @@ -58,6 +72,7 @@ class historic_price_fetcher : public processor_base , resolver_(strand_) , stream_(strand_, ctx) , coins_(coins) + , start_of_day_(0) , active_(false) { } @@ -96,6 +111,9 @@ class historic_price_fetcher : public processor_base boost::beast::error_code ec, std::size_t bytes_transferred); + void parse_json( + boost::core::string_view str); + void on_shutdown(boost::beast::error_code ec); }; diff --git a/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp b/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp index 0da198b155..0cb3ea0103 100644 --- a/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp +++ b/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp @@ -94,7 +94,7 @@ class json_price_decoder : public processor_base private: - inline std::time_t time_gm(struct tm* tm) { + inline std::time_t my_time_gm(struct tm* tm) { #if defined(_DEFAULT_SOURCE) // Feature test for glibc return timegm(tm); #elif defined(_MSC_VER) // Test for Microsoft C/C++ @@ -144,9 +144,9 @@ class json_price_decoder : public processor_base // fix up the day of week, day of year etc t.tm_isdst = 0; - t.tm_wday = -1; // a canary for a time_gm error + t.tm_wday = -1; // a canary for a my_time_gm error - std::time_t epoch_time = time_gm(&t); + std::time_t epoch_time = my_time_gm(&t); if (epoch_time == -1 || t.tm_wday == -1) // "real error" return error_handler_(ec, "json_price_decoder::on_process parse failure"); diff --git a/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp b/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp index fb973a83b2..a81e6e8091 100644 --- a/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp +++ b/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,17 @@ using namespace websocket; // from namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; // from + +static inline std::time_t my_time_gm(struct tm* tm) { +#if defined(_DEFAULT_SOURCE) // Feature test for glibc + return timegm(tm); +#elif defined(_MSC_VER) // Test for Microsoft C/C++ + return _mkgmtime(tm); +#else +#error "Neither timegm nor _mkgmtime available" +#endif +} + // Start the asynchronous operation void live_price_listener::run() { @@ -72,7 +84,7 @@ void live_price_listener::run() // `beast::bind_front_handler` is an equally viable alternative. resolver_.async_resolve( host_, - "443", + "https", [this](error_code ec, tcp::resolver::results_type results) { // Note that `results` is actually an iterator into a container of @@ -319,17 +331,26 @@ void live_price_listener::on_read(beast::error_code ec, std::size_t bytes_transf if (!active_) return; - //std::cout << "Interim: " << beast::make_printable(buffer_.data()) << "\n\n" << std::endl; + // The asynchronous read performs its own commit() on the dynamic buffer, thus the readable + // section of the dynamic buffer contains the message we want to decode. + asio::const_buffer buf(buffer_.cdata()); + + // We convert the const_buffer buf into a string_view, and then parse the json string itself. + // Note that an alternative would be to use beast::buffers_to_string but that would + // perform an additional allocation. + parse_json(core::string_view(static_cast(buf.data()), buf.size())); - receive_handler_(beast::buffers_to_string(buffer_.data())); + std::cout << "Interim: " << beast::make_printable(buffer_.data()) << "\n\n" << std::endl; - // Erase the const section of the dynamic buffer. We know that the mutable section - // does not contain any data at this point, so this call merely updates internal - // buffer pointers rather than having to call memmove. - buffer_.consume(buffer_.max_size()); + //receive_handler_(beast::buffers_to_string(buffer_.data())); + + // Erase the const section of the dynamic buffer. + // Note: the clear() function does not deallocate so the capactity of the flat_buffer is + // unchanged, preventing the need for a reallocation each time a message is received. + buffer_.clear(); count++; - if (count > 10) cancel(); + if (count > 20) cancel(); // This is a very common idiom in async programming. As soon as a read completes, we // initiate another asynchronous read, almost like an infinite loop. @@ -342,9 +363,67 @@ void live_price_listener::on_read(beast::error_code ec, std::size_t bytes_transf ); } -void live_price_listener::on_close(beast::error_code ec) + + +void live_price_listener::parse_json(core::string_view str) { + parser_.reset(); + + boost::system::error_code ec; + parser_.write(str, ec); + if (ec) + return error_handler_(ec, "json_price_decoder::parse_json"); + + json::value jv(parser_.release()); + + core::string_view productstr; + core::string_view timestr; + + double price = 0; + + // Design note: the json parsing could be done without using the exception + // interface, but the resulting code would be considerably more verbose. + try { + if (jv.as_object().at("type").as_string() != "ticker") + return; + + productstr = jv.as_object().at("product_id").as_string(); + + core::string_view pricestr = jv.as_object().at("price").as_string(); + + timestr = jv.as_object().at("time").as_string(); + + std::size_t str_size = 0; + price = std::stod(pricestr, &str_size); + } + catch (boost::system::system_error se) { + return error_handler_(ec, "json_price_decoder::parse_json parse failure"); + } + catch (std::invalid_argument se) { + return error_handler_(ec, "json_price_decoder::parse_json parse failure"); + } + catch (std::out_of_range se) { + return error_handler_(ec, "json_price_decoder::parse_json parse failure"); + } + + // As timestr is a *UTC* string, we want to generate a chrono::system_clock::time_point + // representing the UTC time. + posix_time::ptime ptime = posix_time::from_iso_extended_string(timestr); + std::time_t epoch_time = posix_time::to_time_t(ptime); + const auto price_time = std::chrono::system_clock::from_time_t(epoch_time); + + if (receive_handler_) + receive_handler_(productstr, price_time, price); + + std::cout << "Decoded live " << productstr << " price: " << price << " at " << price_time << std::endl; + + //std::this_thread::sleep_for(std::chrono::milliseconds(10*1000)); +} + +void live_price_listener::on_close(beast::error_code ec) +{ + if (ec && ec != boost::asio::ssl::error::stream_truncated) return error_handler_(ec, "close"); // If we get here then the connection is closed gracefully diff --git a/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp b/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp index 695653ceeb..7c218fb8f6 100644 --- a/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp +++ b/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include @@ -27,7 +28,10 @@ class live_price_listener : public processor_base { // This holds the function called when a live price is received. - std::function receive_handler_; + std::function receive_handler_; // This holds the function called when an error happens. std::function error_handler_; @@ -63,6 +67,10 @@ class live_price_listener : public processor_base // immediately. bool active_; + // It is more efficient to persist the json parser so that memory allocation does not need + // to be repeated each time we docode a message + boost::json::parser parser_; + int count = 0; public: @@ -73,7 +81,10 @@ class live_price_listener : public processor_base boost::asio::io_context& ioc , boost::asio::ssl::context& ctx , const std::vector& coins - , std::function receive_handler + , std::function receive_handler , std::function err_handler) : receive_handler_(receive_handler) , error_handler_(err_handler) @@ -123,6 +134,9 @@ class live_price_listener : public processor_base boost::beast::error_code ec, std::size_t bytes_transferred); + void parse_json( + boost::core::string_view str); + void on_close(boost::beast::error_code ec); }; diff --git a/example/websocket/client/crypto-ai-ssl/price_store.hpp b/example/websocket/client/crypto-ai-ssl/price_store.hpp new file mode 100644 index 0000000000..0666742846 --- /dev/null +++ b/example/websocket/client/crypto-ai-ssl/price_store.hpp @@ -0,0 +1,126 @@ +// +// Copyright (c) 2025 Mungo Gill +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance +// + +#ifndef BOOST_BEAST_EXAMPLE_PRICE_STORE_H +#define BOOST_BEAST_EXAMPLE_PRICE_STORE_H + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + + +// Opens a websocket and subsscribes to price ticks +class price_store +{ + std::function update_handler_; + +public: + struct price_entry { + std::chrono::system_clock::time_point time_; + double price_; + }; + +private: + // As we wish to support one thread posting prices and one thread reading prices, + // without having a thread-safe vector and without copying the entire vector each + // time, and given we know we have a limited volume of data, we can adopt a + // "double-buffer" technique (and accept the memory hit). + std::map< + std::string, + std::pair, + std::vector > > entries_; + + std::mutex mutex_; + +public: + explicit + price_store( + const std::vector& coins + , std::function update_handler + ) + : update_handler_(update_handler) + { + const std::lock_guard guard(mutex_); + // Prepopulate the map. + // Note: this could, as an alternative design, be done "lazily" + // as prices come in. + for (auto& s : coins) { + auto rv = entries_.emplace(std::piecewise_construct, std::forward_as_tuple(s), std::make_tuple()); + rv.first->second.first.reserve(60 * 24); + rv.first->second.second.reserve(60 * 24); + } + } + + void + post(const std::string& coin, + std::chrono::system_clock::time_point time, + double price) + { + const std::lock_guard guard(mutex_); + bool update_callback_required = false; + auto it = entries_.find(coin); + if (it == entries_.end()) { + // Unsupported coin - do not record the price + } + else if (it->second.first.size() == 0) { + it->second.first.emplace_back(time, price); + update_callback_required = true; + } + else { + // We limit the stored prices to one every 5 minutes. + static const auto one_minute = + std::chrono::duration_cast( + std::chrono::seconds(60)); + auto gap = time - it->second.first.back().time_; + if (gap >= one_minute) { + it->second.first.emplace_back(time, price); + update_callback_required = true; + } + } + if (update_handler_ && update_callback_required) + update_handler_(coin); + return; + } + + const std::vector& + get(const std::string &coin) + { + const std::lock_guard guard(mutex_); + static const std::vector empty; + + auto pos = entries_.find(coin); + if (pos != entries_.end()) { + std::vector& v1 = pos->second.first; + std::vector& v2 = pos->second.second; + // Add the missing entries into v2. + v2.reserve(v1.capacity()); + for (int i = v2.size(); i < v1.size(); i++) { + v2.push_back(v1[i]); + } + return v2; + } + else + return empty; + } +}; + +#endif + diff --git a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp index 88b48fe2a4..d024daf4a9 100644 --- a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp +++ b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp @@ -30,6 +30,7 @@ #include #include "json_price_decoder.hpp" +#include "price_store.hpp" #include #include @@ -85,24 +86,26 @@ int main(int argc, char** argv) // This holds the root certificate used for verification load_root_certificates(ctx); - net::thread_pool decoder_tp(1); - auto decoded_recv = [](const std::string& symbol, double price) { std::cout << "Decoded Recv" << symbol << ":" << price << "\n" << std::endl; }; - json_price_decoder decoder_worker(decoder_tp, decoded_recv, fail); + auto price_store_update_recv = [](const std::string&) { + + }; - decoder_worker.run(); + price_store store(coins, price_store_update_recv); - auto live_input_recv = [&decoder_worker](std::string&& v) { - //std::cout << "Live input Recv" << v << "\n" << std::endl; - decoder_worker.post(processor_base::input_type::LIVE, std::move(v)); - }; + auto live_input_recv = [&store](const std::string& coin, + std::chrono::system_clock::time_point time, + double price) { + store.post(coin, time, price); + }; - auto historic_input_recv = [&decoder_worker](std::string&& v) { - std::cout << "Historic input Recv" << v << "\n" << std::endl; - decoder_worker.post(processor_base::input_type::HISTORIC, std::move(v)); + auto historic_input_recv = [&store](const std::string& coin, + std::chrono::system_clock::time_point time, + double price) { + store.post(coin, time, price); }; // The io_context is required for all I/O @@ -112,15 +115,17 @@ int main(int argc, char** argv) historic_price_fetcher historic_fetcher(listen_ioc, ctx, coins, historic_input_recv, fail); historic_fetcher.run(); + // Now we run the event loop until all historic prices have been received. + listen_ioc.run(); + // Construct and start a the websocket listener. live_price_listener listen_worker(listen_ioc, ctx, coins, live_input_recv, fail); listen_worker.run(); - // Run the I/O service. The call will return when + // Restartr the event loop. The call will return when // the socket is closed. + listen_ioc.restart(); listen_ioc.run(); - decoder_tp.join(); - return EXIT_SUCCESS; } From a303ba6587ad789a604afed4f0238032c702cd92 Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Fri, 3 Oct 2025 09:10:09 +0100 Subject: [PATCH 3/9] fix repo in copyright notice --- .../websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp | 2 +- .../websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp | 2 +- example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp | 2 +- example/websocket/client/crypto-ai-ssl/live_price_listener.cpp | 2 +- example/websocket/client/crypto-ai-ssl/live_price_listener.hpp | 2 +- example/websocket/client/crypto-ai-ssl/price_store.hpp | 2 +- example/websocket/client/crypto-ai-ssl/processor_base.hpp | 2 +- .../client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp index bb10abd654..5c4409ed64 100644 --- a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp @@ -4,7 +4,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// Official repository: https://github.com/cppalliance +// Official repository: https://github.com/boostorg/beast // #include "processor_base.hpp" diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp index 4833193581..9459eae1fa 100644 --- a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp @@ -4,7 +4,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// Official repository: https://github.com/cppalliance +// Official repository: https://github.com/boostorg/beast // #ifndef BOOST_BEAST_EXAMPLE_HISTORIC_PRICE_FETCHER diff --git a/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp b/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp index 0cb3ea0103..c1d16ca232 100644 --- a/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp +++ b/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp @@ -4,7 +4,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// Official repository: https://github.com/cppalliance +// Official repository: https://github.com/boostorg/beast // #ifndef BOOST_BEAST_EXAMPLE_JSON_PRICE_DECODER_H diff --git a/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp b/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp index a81e6e8091..490444e863 100644 --- a/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp +++ b/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp @@ -4,7 +4,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// Official repository: https://github.com/cppalliance +// Official repository: https://github.com/boostorg/beast // #include "processor_base.hpp" diff --git a/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp b/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp index 7c218fb8f6..d0ca2cf7b8 100644 --- a/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp +++ b/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp @@ -4,7 +4,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// Official repository: https://github.com/cppalliance +// Official repository: https://github.com/boostorg/beast // #ifndef BOOST_BEAST_EXAMPLE_LIVE_PRICE_LISTENER diff --git a/example/websocket/client/crypto-ai-ssl/price_store.hpp b/example/websocket/client/crypto-ai-ssl/price_store.hpp index 0666742846..d38df05695 100644 --- a/example/websocket/client/crypto-ai-ssl/price_store.hpp +++ b/example/websocket/client/crypto-ai-ssl/price_store.hpp @@ -4,7 +4,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// Official repository: https://github.com/cppalliance +// Official repository: https://github.com/boostorg/beast // #ifndef BOOST_BEAST_EXAMPLE_PRICE_STORE_H diff --git a/example/websocket/client/crypto-ai-ssl/processor_base.hpp b/example/websocket/client/crypto-ai-ssl/processor_base.hpp index c8750e95c2..ba7ebdf021 100644 --- a/example/websocket/client/crypto-ai-ssl/processor_base.hpp +++ b/example/websocket/client/crypto-ai-ssl/processor_base.hpp @@ -4,7 +4,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// Official repository: https://github.com/cppalliance +// Official repository: https://github.com/boostorg/beast // #ifndef BOOST_BEAST_EXAMPLE_PROCESSOR_BASE_H diff --git a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp index d024daf4a9..1afa1db936 100644 --- a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp +++ b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp @@ -4,7 +4,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -// Official repository: https://github.com/cppalliance +// Official repository: https://github.com/boostorg/beast // #include "example/common/root_certificates.hpp" From 09cfd380b74c034538999726ea90b620a2d8055f Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Fri, 3 Oct 2025 09:27:36 +0100 Subject: [PATCH 4/9] change beast::error_code to system::error_code and remove write length check --- .../crypto-ai-ssl/historic_price_fetcher.cpp | 16 +++++----- .../crypto-ai-ssl/historic_price_fetcher.hpp | 16 +++++----- .../crypto-ai-ssl/json_price_decoder.hpp | 4 +-- .../crypto-ai-ssl/live_price_listener.cpp | 31 ++++++++----------- .../crypto-ai-ssl/live_price_listener.hpp | 19 ++++++------ .../client/crypto-ai-ssl/price_store.hpp | 2 +- .../websocket_client_crypto_ai_ssl.cpp | 2 +- 7 files changed, 42 insertions(+), 48 deletions(-) diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp index 5c4409ed64..f9e24ba79e 100644 --- a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp @@ -61,7 +61,7 @@ void historic_price_fetcher::run() // Set SNI Hostname (many hosts need this to handshake successfully) if (!SSL_set_tlsext_host_name(stream_.native_handle(), host_.c_str())) { - beast::error_code ec{ + system::error_code ec{ static_cast(::ERR_get_error()), net::error::get_ssl_category() }; return error_handler_(ec, "SNI"); @@ -111,7 +111,7 @@ void historic_price_fetcher::cancel() ); } -void historic_price_fetcher::on_resolve(beast::error_code ec, tcp::resolver::results_type results) +void historic_price_fetcher::on_resolve(system::error_code ec, tcp::resolver::results_type results) { if (ec) { cancel(); @@ -134,7 +134,7 @@ void historic_price_fetcher::on_resolve(beast::error_code ec, tcp::resolver::res ); } -void historic_price_fetcher::on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type ep) +void historic_price_fetcher::on_connect(system::error_code ec, tcp::resolver::results_type::endpoint_type ep) { boost::ignore_unused(ep); @@ -159,7 +159,7 @@ void historic_price_fetcher::on_connect(beast::error_code ec, tcp::resolver::res ); } -void historic_price_fetcher::on_ssl_handshake(beast::error_code ec) +void historic_price_fetcher::on_ssl_handshake(system::error_code ec) { if (ec) { cancel(); @@ -214,7 +214,7 @@ void historic_price_fetcher::next_request() } } -void historic_price_fetcher::on_write(beast::error_code ec, std::size_t bytes_transferred) +void historic_price_fetcher::on_write(system::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); @@ -237,7 +237,7 @@ void historic_price_fetcher::on_write(beast::error_code ec, std::size_t bytes_tr } -void historic_price_fetcher::on_read(beast::error_code ec, std::size_t bytes_transferred) +void historic_price_fetcher::on_read(system::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); @@ -326,7 +326,7 @@ void historic_price_fetcher::parse_json(core::string_view str) core::string_view closestr = it->as_object().at("close").as_string(); std::size_t str_size = 0; - std::time_t start_time = std::stod(startstr, &str_size); + std::time_t start_time = static_cast(std::stoll(startstr, &str_size)); if (start_time > start_of_day_) { double open = std::stod(openstr, &str_size); @@ -348,7 +348,7 @@ void historic_price_fetcher::parse_json(core::string_view str) } } -void historic_price_fetcher::on_shutdown(beast::error_code ec) +void historic_price_fetcher::on_shutdown(system::error_code ec) { if (ec && ec != boost::asio::ssl::error::stream_truncated) return error_handler_(ec, "shutdown"); diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp index 9459eae1fa..417b619a72 100644 --- a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp @@ -32,7 +32,7 @@ class historic_price_fetcher : public processor_base const std::string&, std::chrono::system_clock::time_point, double)> receive_handler_; - std::function error_handler_; + std::function error_handler_; boost::asio::strand strand_; boost::asio::ip::tcp::resolver resolver_; @@ -65,7 +65,7 @@ class historic_price_fetcher : public processor_base const std::string& , std::chrono::system_clock::time_point , double)> receive_handler - , std::function err_handler) + , std::function err_handler) : receive_handler_(receive_handler) , error_handler_(err_handler) , strand_(boost::asio::make_strand(ioc)) @@ -87,35 +87,35 @@ class historic_price_fetcher : public processor_base private: void on_resolve( - boost::beast::error_code ec, + boost::system::error_code ec, boost::asio::ip::tcp::resolver::results_type results); void on_connect( - boost::beast::error_code ec, + boost::system::error_code ec, boost::asio::ip::tcp::resolver::results_type::endpoint_type ep); void - on_ssl_handshake(boost::beast::error_code ec); + on_ssl_handshake(boost::system::error_code ec); void next_request(); void on_write( - boost::beast::error_code ec, + boost::system::error_code ec, std::size_t bytes_transferred); void on_read( - boost::beast::error_code ec, + boost::system::error_code ec, std::size_t bytes_transferred); void parse_json( boost::core::string_view str); void - on_shutdown(boost::beast::error_code ec); + on_shutdown(boost::system::error_code ec); }; #endif diff --git a/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp b/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp index c1d16ca232..d2bd015c76 100644 --- a/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp +++ b/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp @@ -49,7 +49,7 @@ using tcp = boost::asio::ip::tcp; // from class json_price_decoder : public processor_base { std::function receive_handler_; - std::function error_handler_; + std::function error_handler_; net::execution_context &ctx_; bool active_; @@ -58,7 +58,7 @@ class json_price_decoder : public processor_base explicit json_price_decoder(net::execution_context& ec , std::function receive_handler - , std::function err_handler) + , std::function err_handler) : receive_handler_(receive_handler) , error_handler_(err_handler) , ctx_(ec) diff --git a/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp b/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp index 490444e863..0dc46ceea2 100644 --- a/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp +++ b/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp @@ -63,7 +63,7 @@ void live_price_listener::run() // part of asio or beast. if (!SSL_set_tlsext_host_name(ws_.next_layer().native_handle(), host_.c_str())) { - beast::error_code ec{ + system::error_code ec{ static_cast(::ERR_get_error()), net::error::get_ssl_category() }; return error_handler_(ec, "SNI"); @@ -117,7 +117,7 @@ void live_price_listener::cancel() } // This is the function called when hostname resolution completes. -void live_price_listener::on_resolve(beast::error_code ec, tcp::resolver::results_type results) +void live_price_listener::on_resolve(system::error_code ec, tcp::resolver::results_type results) { // In the event of an error call the `cancel` function which will drain // any pending completion handlers. In this case the websocket is not yet @@ -155,7 +155,7 @@ void live_price_listener::on_resolve(beast::error_code ec, tcp::resolver::result // Once the underlying socket is connected, this function performs the next step, // namely getting the SSL layer running. -void live_price_listener::on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type ep) +void live_price_listener::on_connect(system::error_code ec, tcp::resolver::results_type::endpoint_type ep) { if (ec) { // In the event of a connection error call the `cancel` function which will drain @@ -192,7 +192,7 @@ void live_price_listener::on_connect(beast::error_code ec, tcp::resolver::result ); } -void live_price_listener::on_ssl_handshake(beast::error_code ec) +void live_price_listener::on_ssl_handshake(system::error_code ec) { if (ec) { // In the event of an ssl error call the `cancel` function which will drain @@ -246,7 +246,7 @@ void live_price_listener::on_ssl_handshake(beast::error_code ec) // This is the function that is called when the websocket is up and usable. // The previous steps were relatively generic across all websocket connections, // and from this point on we need to include business logic. -void live_price_listener::on_handshake(beast::error_code ec) +void live_price_listener::on_handshake(system::error_code ec) { if (ec) { cancel(); @@ -273,29 +273,24 @@ void live_price_listener::on_handshake(beast::error_code ec) // Send the subscription message to the server. ws_.async_write( net::buffer(subscribe_json_str), - [this, len=subscribe_json_str.size()](error_code ec, std::size_t bytes_transferred) + [this](error_code ec, std::size_t bytes_transferred) { - on_write(ec, len, bytes_transferred); + on_write(ec, bytes_transferred); } ); } void live_price_listener::on_write( - beast::error_code ec - , std::size_t bytes_required + system::error_code ec , std::size_t bytes_transferred) { - // Check for errors and verify that the byte count sent in the subscription message - // is what we expected. + boost::ignore_unused(bytes_transferred); + + // Check for errors. if (ec) { cancel(); return error_handler_(ec, "write"); } - else if (bytes_transferred < bytes_required) { - cancel(); - // TODO: figure out what to put in ec here. - return error_handler_(ec, "write"); - } // If we have been asked to shut down then do no further processing. if (!active_) @@ -313,7 +308,7 @@ void live_price_listener::on_write( ); } -void live_price_listener::on_read(beast::error_code ec, std::size_t bytes_transferred) +void live_price_listener::on_read(system::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); @@ -421,7 +416,7 @@ void live_price_listener::parse_json(core::string_view str) //std::this_thread::sleep_for(std::chrono::milliseconds(10*1000)); } -void live_price_listener::on_close(beast::error_code ec) +void live_price_listener::on_close(system::error_code ec) { if (ec && ec != boost::asio::ssl::error::stream_truncated) return error_handler_(ec, "close"); diff --git a/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp b/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp index d0ca2cf7b8..ae02a35ca5 100644 --- a/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp +++ b/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp @@ -34,7 +34,7 @@ class live_price_listener : public processor_base double)> receive_handler_; // This holds the function called when an error happens. - std::function error_handler_; + std::function error_handler_; // We want to ensure that operations to set up the websocket are performed in order, // and that we do not attempt to call async_read when another async_read is in progress. @@ -85,7 +85,7 @@ class live_price_listener : public processor_base const std::string& , std::chrono::system_clock::time_point , double)> receive_handler - , std::function err_handler) + , std::function err_handler) : receive_handler_(receive_handler) , error_handler_(err_handler) , strand_(boost::asio::make_strand(ioc)) @@ -109,36 +109,35 @@ class live_price_listener : public processor_base private: void on_resolve( - boost::beast::error_code ec, + boost::system::error_code ec, boost::asio::ip::tcp::resolver::results_type results); void on_connect( - boost::beast::error_code ec, + boost::system::error_code ec, boost::asio::ip::tcp::resolver::results_type::endpoint_type ep); void - on_ssl_handshake(boost::beast::error_code ec); + on_ssl_handshake(boost::system::error_code ec); void - on_handshake(boost::beast::error_code ec); + on_handshake(boost::system::error_code ec); void on_write( - boost::beast::error_code ec, - std::size_t bytes_required, + boost::system::error_code ec, std::size_t bytes_transferred); void on_read( - boost::beast::error_code ec, + boost::system::error_code ec, std::size_t bytes_transferred); void parse_json( boost::core::string_view str); void - on_close(boost::beast::error_code ec); + on_close(boost::system::error_code ec); }; #endif diff --git a/example/websocket/client/crypto-ai-ssl/price_store.hpp b/example/websocket/client/crypto-ai-ssl/price_store.hpp index d38df05695..e173744747 100644 --- a/example/websocket/client/crypto-ai-ssl/price_store.hpp +++ b/example/websocket/client/crypto-ai-ssl/price_store.hpp @@ -112,7 +112,7 @@ class price_store std::vector& v2 = pos->second.second; // Add the missing entries into v2. v2.reserve(v1.capacity()); - for (int i = v2.size(); i < v1.size(); i++) { + for (std::size_t i = v2.size(); i < v1.size(); i++) { v2.push_back(v1[i]); } return v2; diff --git a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp index 1afa1db936..86d8950bf9 100644 --- a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp +++ b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp @@ -48,7 +48,7 @@ using tcp = boost::asio::ip::tcp; // from // Report a failure void -fail(beast::error_code ec, char const* what) +fail(system::error_code ec, char const* what) { std::cerr << what << ": " << ec.message() << "\n"; } From 47a0995c4a98af5dbdb464ba17e6662040d6bc8e Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Fri, 3 Oct 2025 18:34:38 +0100 Subject: [PATCH 5/9] persist subscription json string and remove some dead code --- .../crypto-ai-ssl/historic_price_fetcher.cpp | 11 -- .../crypto-ai-ssl/json_price_decoder.hpp | 177 ------------------ .../crypto-ai-ssl/live_price_listener.cpp | 14 +- .../crypto-ai-ssl/live_price_listener.hpp | 4 + .../websocket_client_crypto_ai_ssl.cpp | 1 - 5 files changed, 6 insertions(+), 201 deletions(-) delete mode 100644 example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp index f9e24ba79e..412d0882a9 100644 --- a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp @@ -39,17 +39,6 @@ using namespace beast; // from using tcp = boost::asio::ip::tcp; // from - -static inline std::time_t my_time_gm(struct tm* tm) { -#if defined(_DEFAULT_SOURCE) // Feature test for glibc - return timegm(tm); -#elif defined(_MSC_VER) // Test for Microsoft C/C++ - return _mkgmtime(tm); -#else -#error "Neither timegm nor _mkgmtime available" -#endif -} - // Start the asynchronous operation void historic_price_fetcher::run() { diff --git a/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp b/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp deleted file mode 100644 index d2bd015c76..0000000000 --- a/example/websocket/client/crypto-ai-ssl/json_price_decoder.hpp +++ /dev/null @@ -1,177 +0,0 @@ -// -// Copyright (c) 2025 Mungo Gill -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/boostorg/beast -// - -#ifndef BOOST_BEAST_EXAMPLE_JSON_PRICE_DECODER_H -#define BOOST_BEAST_EXAMPLE_JSON_PRICE_DECODER_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "processor_base.hpp" - -using namespace boost; -using namespace std::placeholders; - -using namespace beast; // from -using namespace http; // from -using namespace websocket; // from -using namespace json; - -namespace net = boost::asio; // from -using tcp = boost::asio::ip::tcp; // from - -// Opens a websocket and subsscribes to price ticks -class json_price_decoder : public processor_base -{ - std::function receive_handler_; - std::function error_handler_; - net::execution_context &ctx_; - bool active_; - -public: - // Resolver and socket require an io_context - explicit - json_price_decoder(net::execution_context& ec - , std::function receive_handler - , std::function err_handler) - : receive_handler_(receive_handler) - , error_handler_(err_handler) - , ctx_(ec) - , active_(false) - { - } - - // Start the asynchronous operation - void - run() - { - active_ = true; - } - - void - post(input_type type, std::string &&str) - { - if (!active_) - return; - - net::post(net::get_associated_executor(ctx_), [this, type, s = std::move(str)] () mutable - { - on_process(type, std::move(s)); - } - ); - } - - void - cancel() override - { - active_ = false; - } - -private: - - inline std::time_t my_time_gm(struct tm* tm) { -#if defined(_DEFAULT_SOURCE) // Feature test for glibc - return timegm(tm); -#elif defined(_MSC_VER) // Test for Microsoft C/C++ - return _mkgmtime(tm); -#else -#error "Neither timegm nor _mkgmtime available" -#endif - } - - void - on_process( - input_type type - , std::string &&str) - { - if (!active_) - return; - - boost::system::error_code ec; - monotonic_resource mr; - const value jv = parse(str, ec, &mr); - - if (ec) - return error_handler_(ec, "json_price_decoder::on_process"); - - if (type == input_type::LIVE) - { - try { - if (jv.as_object().at("type").as_string() != "ticker") - return; - - json::string_view productstr = jv.as_object().at("product_id").as_string(); - - json::string_view pricestr = jv.as_object().at("price").as_string(); - - std::size_t str_size = 0; - double price = std::stod(pricestr, &str_size); - - json::string_view timestr = jv.as_object().at("time").as_string(); - - std::tm t = {}; // tm_isdst = 0 - std::istringstream ss(timestr); - ss.imbue(std::locale()); // "LANG=C" - - ss >> std::get_time(&t, "%Y-%m-%dT%H:%M:%S."); - if (ss.fail()) - return error_handler_(ec, "json_price_decoder::on_process parse failure"); - - // fix up the day of week, day of year etc - t.tm_isdst = 0; - t.tm_wday = -1; // a canary for a my_time_gm error - - std::time_t epoch_time = my_time_gm(&t); - - if (epoch_time == -1 || t.tm_wday == -1) // "real error" - return error_handler_(ec, "json_price_decoder::on_process parse failure"); - - const auto price_time = std::chrono::system_clock::from_time_t(epoch_time); - - std::cout << "Decoded live " << productstr << " price: " << price << " at " << price_time << std::endl; - - } - catch (boost::system::system_error se) { - return error_handler_(ec, "json_price_decoder::on_process parse failure"); - } - catch (std::invalid_argument se) { - return error_handler_(ec, "json_price_decoder::on_process parse failure"); - } - catch (std::out_of_range se) { - return error_handler_(ec, "json_price_decoder::on_process parse failure"); - } - } - if (type == input_type::HISTORIC) - { - std::cerr << "historic decoding not yet written" << std::endl; - } - } -}; - -#endif - diff --git a/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp b/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp index 0dc46ceea2..eb4029ea3f 100644 --- a/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp +++ b/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp @@ -42,16 +42,6 @@ namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; // from -static inline std::time_t my_time_gm(struct tm* tm) { -#if defined(_DEFAULT_SOURCE) // Feature test for glibc - return timegm(tm); -#elif defined(_MSC_VER) // Test for Microsoft C/C++ - return _mkgmtime(tm); -#else -#error "Neither timegm nor _mkgmtime available" -#endif -} - // Start the asynchronous operation void live_price_listener::run() { @@ -268,11 +258,11 @@ void live_price_listener::on_handshake(system::error_code ec) }; // Convert the json object into a string. - std::string subscribe_json_str = serialize(jv); + subscribe_json_str_ = serialize(jv); // Send the subscription message to the server. ws_.async_write( - net::buffer(subscribe_json_str), + net::buffer(subscribe_json_str_), [this](error_code ec, std::size_t bytes_transferred) { on_write(ec, bytes_transferred); diff --git a/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp b/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp index ae02a35ca5..f3ce219998 100644 --- a/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp +++ b/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp @@ -56,6 +56,10 @@ class live_price_listener : public processor_base // completion handler is called. boost::beast::flat_buffer buffer_; + // The subscription message needs to persist until the asynchronous operation initiated + // by ws_.async_write() completes. Thus it is held here as a member. + std::string subscribe_json_str_; + // The host will be used at multiple stages during the websocket's setup process. std::string host_; diff --git a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp index 86d8950bf9..f605b476f4 100644 --- a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp +++ b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp @@ -29,7 +29,6 @@ #include #include -#include "json_price_decoder.hpp" #include "price_store.hpp" #include From 090d9cd624fdd6626035a6980fe838928efa6037 Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Wed, 8 Oct 2025 16:38:48 +0100 Subject: [PATCH 6/9] historic price fetcher to use composed operations - work in progress --- .../crypto-ai-ssl/historic_price_fetcher.cpp | 4 + .../crypto-ai-ssl/historic_price_fetcher.hpp | 612 +++++++++++++++--- .../websocket_client_crypto_ai_ssl.cpp | 272 +++++++- 3 files changed, 802 insertions(+), 86 deletions(-) diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp index 412d0882a9..3b5b6fcf7d 100644 --- a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp @@ -32,6 +32,8 @@ #include #include +#if 0 + using namespace boost; using namespace std::placeholders; @@ -347,3 +349,5 @@ void historic_price_fetcher::on_shutdown(system::error_code ec) // The make_printable() function helps print a ConstBufferSequence std::cout << "Final buffer content:" << beast::make_printable(buffer_.data()) << std::endl; } + +#endif diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp index 417b619a72..f98a0616e8 100644 --- a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp @@ -10,112 +10,564 @@ #ifndef BOOST_BEAST_EXAMPLE_HISTORIC_PRICE_FETCHER #define BOOST_BEAST_EXAMPLE_HISTORIC_PRICE_FETCHER +#include +#include +#include #include #include #include #include #include #include +#include +#include #include +#include +#include +#include +#include +#include #include #include "processor_base.hpp" +namespace boost { + template + class historic_fetcher; + + template + class historic_fetcher_op + { + enum class state { + starting, + resolving, + connecting, + ssl_handshaking, + writing, + reading, + error, + complete + }; + + using resolver_type = asio::ip::basic_resolver< + asio::ip::tcp, Executor>; + + using tcp_stream_type = beast::basic_stream< + asio::ip::tcp, + Executor>; + + using ssl_stream_type = boost::asio::ssl::stream; + + using client_type = historic_fetcher; + + using buffer_type = boost::beast::flat_buffer; + using request_type = beast::http::request; + using response_type = beast::http::response; + + client_type& client_; + resolver_type& resolver_; + ssl_stream_type& ssl_stream_; + + buffer_type& buffer_; + request_type& request_; + response_type& response_; + + state state_; + + public: + + explicit + historic_fetcher_op( + client_type& client + , resolver_type& resolver + , ssl_stream_type& ssl_stream + , buffer_type& buffer + , request_type& request + , response_type& response) + : client_(client) + , resolver_(resolver) + , ssl_stream_(ssl_stream) + , state_(state::starting) + , buffer_(buffer) + , request_(request) + , response_(response) + { + // Set the expected hostname in the peer certificate for verification + ssl_stream_.set_verify_callback(boost::asio::ssl::host_name_verification(client.get_host())); + + // Set up an HTTP GET request message + request_.version(11); + request_.method(beast::http::verb::get); + request_.set(beast::http::field::host, client.get_host()); + request_.set(beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING); + } + + template + void operator()( + Self& self + , system::error_code ec = {}) + { + if (ec) { + state_ = state::error; + return self.complete(ec); + } + + switch (state_) { + case state::starting: { + // Set SNI Hostname (many hosts need this to handshake successfully) + if (!SSL_set_tlsext_host_name(ssl_stream_.native_handle(), client_.get_host())) + { + system::error_code ssl_ec{ + static_cast(::ERR_get_error()), + asio::error::get_ssl_category() }; + state_ = state::error; + return self.complete(ssl_ec); + } + + // Look up the domain name + state_ = state::resolving; + resolver_.async_resolve( + client_.get_host(), + "https", + std::move(self) + ); + } break; + case state::ssl_handshaking: { + // If there are any coins left to request, request one. + return send_next_request(self); + } break; + default: { + // This should not happen. + throw std::logic_error("unreachable"); + } + } + }; + + template + void operator()( + Self& self + , system::error_code ec + , asio::ip::tcp::resolver::results_type results) + { + if (ec) { + state_ = state::error; + return self.complete(ec); + } + + switch (state_) { + case state::resolving: { + // Set a timeout on the operation + beast::get_lowest_layer(ssl_stream_).expires_after(std::chrono::seconds(30)); + + // Make the connection on the IP address we get from a lookup + state_ = state::connecting; + for (auto& r : results) { + std::cout << r.endpoint() << std::endl; + } + beast::get_lowest_layer(ssl_stream_).async_connect( + results, + std::move(self) + ); + } break; + default: { + // This should not happen. + throw std::logic_error("unreachable"); + } + } + }; + + template + void operator()( + Self& self + , system::error_code ec + , asio::ip::tcp::resolver::results_type::endpoint_type ep) + { + boost::ignore_unused(ep); + + if (ec) { + state_ = state::error; + return self.complete(ec); + } + + switch (state_) { + case state::connecting: { + // Set a timeout on the operation + beast::get_lowest_layer(ssl_stream_).expires_after(std::chrono::seconds(30)); + + // Perform the SSL handshake + state_ = state::ssl_handshaking; + ssl_stream_.async_handshake( + boost::asio::ssl::stream_base::client, + std::move(self) + ); + } break; + default: { + // This should not happen. + throw std::logic_error("unreachable"); + } + } + }; + + template + void operator()( + Self& self + , system::error_code ec + , std::size_t bytes_transferred) + { + boost::ignore_unused(bytes_transferred); + + if (state_ == state::reading && ec == beast::http::error::end_of_stream) { + state_ = state::complete; + system::error_code ok{}; + return self.complete(ok); + } + else if (ec) { + state_ = state::error; + return self.complete(ec); + } + + switch (state_) { + case state::writing: { + // Read a message into our buffer + state_ = state::reading; + beast::http::async_read( + ssl_stream_, + buffer_, + response_, + std::move(self) + ); + } break; + case state::reading: { + // Process the received message + + // Write the message to standard out + //std::cout << "Response: " << response_ << "\n" << std::endl; + std::cout << "Body: " << response_.body() << "\n\n" << std::endl; + + // The response can be quite long, and we can avoid an allocation + // by swapping the body into an empty string. + std::string temp; + temp.swap(response_.body()); + + client_.process_response(temp, ec); + if (ec) { + state_ = state::error; + return self.complete(ec); + } + + send_next_request(self); + } break; + default: { + // This should not happen. + throw std::logic_error("unreachable"); + } + } + }; + + template + void send_next_request(Self& self) + { + if (!client_.requests_outstanding()) { + state_ = state::complete; + system::error_code ok{}; + return self.complete(ok); + } + + // Set up an HTTP GET request message + request_.target(client_.next_request()); + + // Set a timeout on the operation + beast::get_lowest_layer(ssl_stream_).expires_after(std::chrono::seconds(30)); + + // Send the message + state_ = state::writing; + beast::http::async_write( + ssl_stream_, + request_, + std::move(self) + ); + } + }; + + template + class historic_fetcher + { + using resolver_type = asio::ip::basic_resolver< + asio::ip::tcp, Executor>; + + using tcp_stream_type = beast::basic_stream< + asio::ip::tcp, + Executor>; + + using ssl_stream_type = boost::asio::ssl::stream; + + using buffer_type = boost::beast::flat_buffer; + using request_type = beast::http::request; + using response_type = beast::http::response; + + std::function receive_handler_; + + resolver_type resolver_; + ssl_stream_type ssl_stream_; + + buffer_type buffer_; + request_type request_; + response_type response_; + + std::vector coins_; + std::time_t start_of_day_; + std::string host_; + + std::string current_coin_; + + // It is more efficient to persist the json parser so that memory allocation does not need + // to be repeated each time we docode a message + json::parser parser_; + + public: + //using socket_type = + // asio::basic_socket; + + //using endpoint_type = asio::ip::tcp::endpoint; + + explicit + historic_fetcher( + Executor& exec + , boost::asio::ssl::context& ctx + , const std::vector& coins + , std::function receive_handler) + : receive_handler_(receive_handler) + , resolver_(exec) + , ssl_stream_(exec, ctx) + , coins_(coins) + , start_of_day_(0) + { + // For this example use a hard-coded host name. + // In reality this would be stored in some form of configuration. + host_ = "api.coinbase.com"; + //host_ = "www.boost.org"; + //host_ = "ws-feed.exchange.coinbase.com"; + + // Set the expected hostname in the peer certificate for verification + ssl_stream_.set_verify_callback(boost::asio::ssl::host_name_verification(host_)); + + // Find the subscription start time + posix_time::ptime ptime = posix_time::second_clock::universal_time(); + posix_time::ptime sod = posix_time::ptime(ptime.date()); + start_of_day_ = posix_time::to_time_t(sod); + } + + const char* get_host() { + return host_.c_str(); + } + + bool requests_outstanding() + { + return coins_.size() != 0; + } + + std::string next_request() + { + current_coin_ = coins_.back(); + coins_.pop_back(); + + urls::url url = + urls::format( + "{}://api.coinbase.com/api/v3/brokerage/market/products/{}/candles", + "https", + current_coin_); + + // Data + url.params().append({ "granularity", "ONE_MINUTE" }); + //url.params().append({ "start", std::to_string(start_of_day_) }); + url.params().append({ "limit", std::to_string(5) }); + + return url.buffer(); + } + + void process_response( + boost::core::string_view str, boost::system::error_code& ec) + { + parser_.reset(); + + parser_.write(str, ec); + + if (ec) return; + + json::value jv(parser_.release()); + + // Design note: the json parsing could be done without using the exception + // interface, but the resulting code would be considerably more verbose. + try { + auto candle_list = jv.as_object().at("candles").as_array(); + + if (candle_list.size() == 0) { + ec = json::make_error_code(boost::json::error::size_mismatch); + return; + } + + // The coinbase API provides the most recent values first. + for (auto it = candle_list.crbegin(); it != candle_list.crend(); it++) { + core::string_view startstr = it->as_object().at("start").as_string(); + core::string_view openstr = it->as_object().at("open").as_string(); + core::string_view closestr = it->as_object().at("close").as_string(); + + std::size_t str_size = 0; + std::time_t start_time = static_cast(std::stoll(startstr, &str_size)); + if (start_time > start_of_day_) { + double open = std::stod(openstr, &str_size); + + if (receive_handler_) + receive_handler_(current_coin_, std::chrono::system_clock::from_time_t(start_time), open); + + std::cout << "Decoded historic " << current_coin_ << " price: " << open << " at " << std::chrono::system_clock::from_time_t(start_time) << std::endl; + } + } + } + catch (boost::system::system_error se) { + ec = se.code(); + } + catch (std::invalid_argument se) { + ec = json::make_error_code(boost::json::error::incomplete); + } + catch (std::out_of_range se) { + ec = json::make_error_code(boost::json::error::out_of_range); + } + } + + template< + class Executor, + BOOST_ASIO_COMPLETION_TOKEN_FOR(void( + system::error_code)) CompletionToken> + BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void( + system::error_code)) + async_historic_fetch( + Executor& exec + , CompletionToken&& token) + { + return asio::async_compose< + CompletionToken, + void(system::error_code)>( + historic_fetcher_op( + *this + , resolver_ + , ssl_stream_ + , buffer_ + , request_ + , response_) + , token + , exec); + } + }; + + + // The old non-executor version temporarily pasted below. +#if 0 //using namespace boost; // Opens a websocket and subsscribes to price ticks -class historic_price_fetcher : public processor_base -{ - std::function receive_handler_; - std::function error_handler_; - - boost::asio::strand strand_; - boost::asio::ip::tcp::resolver resolver_; - boost::asio::ssl::stream stream_; - - boost::beast::flat_buffer buffer_; - boost::beast::http::request req_; - boost::beast::http::response response_; - - std::string host_; - std::vector coins_; - - std::string current_coin_; - - std::time_t start_of_day_; - bool active_; - - // It is more efficient to persist the json parser so that memory allocation does not need - // to be repeated each time we docode a message - boost::json::parser parser_; - -public: - // Resolver and socket require an io_context - explicit - historic_price_fetcher( - boost::asio::io_context& ioc - , boost::asio::ssl::context& ctx - , const std::vector& coins - , std::function receive_handler - , std::function err_handler) - : receive_handler_(receive_handler) - , error_handler_(err_handler) - , strand_(boost::asio::make_strand(ioc)) - , resolver_(strand_) - , stream_(strand_, ctx) - , coins_(coins) - , start_of_day_(0) - , active_(false) + class historic_price_fetcher : public processor_base { - } + std::function receive_handler_; + std::function error_handler_; + + boost::asio::strand strand_; + boost::asio::ip::tcp::resolver resolver_; + boost::asio::ssl::stream stream_; - // Start the asynchronous operation - void - run(); + boost::beast::flat_buffer buffer_; + boost::beast::http::request req_; + boost::beast::http::response response_; - void - cancel() override; + std::string host_; + std::vector coins_; -private: - void - on_resolve( - boost::system::error_code ec, - boost::asio::ip::tcp::resolver::results_type results); + std::string current_coin_; - void - on_connect( - boost::system::error_code ec, - boost::asio::ip::tcp::resolver::results_type::endpoint_type ep); + std::time_t start_of_day_; + bool active_; - void - on_ssl_handshake(boost::system::error_code ec); + // It is more efficient to persist the json parser so that memory allocation does not need + // to be repeated each time we docode a message + boost::json::parser parser_; - void - next_request(); + public: + // Resolver and socket require an io_context + explicit + historic_price_fetcher( + boost::asio::io_context& ioc + , boost::asio::ssl::context& ctx + , const std::vector& coins + , std::function receive_handler + , std::function err_handler) + : receive_handler_(receive_handler) + , error_handler_(err_handler) + , strand_(boost::asio::make_strand(ioc)) + , resolver_(strand_) + , stream_(strand_, ctx) + , coins_(coins) + , start_of_day_(0) + , active_(false) + { + } - void - on_write( - boost::system::error_code ec, - std::size_t bytes_transferred); + // Start the asynchronous operation + void + run(); - void - on_read( - boost::system::error_code ec, - std::size_t bytes_transferred); + void + cancel() override; - void parse_json( - boost::core::string_view str); + private: + void + on_resolve( + boost::system::error_code ec, + boost::asio::ip::tcp::resolver::results_type results); - void - on_shutdown(boost::system::error_code ec); -}; + void + on_connect( + boost::system::error_code ec, + boost::asio::ip::tcp::resolver::results_type::endpoint_type ep); + void + on_ssl_handshake(boost::system::error_code ec); + + void + next_request(); + + void + on_write( + boost::system::error_code ec, + std::size_t bytes_transferred); + + void + on_read( + boost::system::error_code ec, + std::size_t bytes_transferred); + + void parse_json( + boost::core::string_view str); + + void + on_shutdown(boost::system::error_code ec); + }; #endif + +} // end namespace boost + +#endif + diff --git a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp index f605b476f4..940250a9a3 100644 --- a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp +++ b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp @@ -7,6 +7,8 @@ // Official repository: https://github.com/boostorg/beast // +#if 1 + #include "example/common/root_certificates.hpp" #include "historic_price_fetcher.hpp" @@ -77,13 +79,13 @@ int main(int argc, char** argv) }; // The SSL context is required, and holds certificates - ssl::context ctx{ ssl::context::tlsv12_client }; + ssl::context ssl_ctx{ ssl::context::tlsv12_client }; // Verify the remote server's certificate - ctx.set_verify_mode(ssl::verify_peer); + ssl_ctx.set_verify_mode(ssl::verify_peer); // This holds the root certificate used for verification - load_root_certificates(ctx); + load_root_certificates(ssl_ctx); auto decoded_recv = [](const std::string& symbol, double price) { std::cout << "Decoded Recv" << symbol << ":" << price << "\n" << std::endl; @@ -111,14 +113,34 @@ int main(int argc, char** argv) net::io_context listen_ioc; // Construct and start a the fetcher of historic prices. - historic_price_fetcher historic_fetcher(listen_ioc, ctx, coins, historic_input_recv, fail); - historic_fetcher.run(); + asio::strand strand = asio::make_strand(listen_ioc); + + auto fetcher = boost::historic_fetcher( + strand, + ssl_ctx, + coins, + historic_input_recv); + + fetcher.async_historic_fetch( + strand, + [](boost::system::error_code ec) + { + if (ec.failed()) + { + fail(ec, "async_historic_fetch"); + } + }); + //historic_price_fetcher historic_fetcher(listen_ioc, ssl_ctx, coins, historic_input_recv, fail); + //historic_fetcher.run(); // Now we run the event loop until all historic prices have been received. listen_ioc.run(); + // Skip the live stuff until we get the changes to historic pricing working. + return EXIT_SUCCESS; + // Construct and start a the websocket listener. - live_price_listener listen_worker(listen_ioc, ctx, coins, live_input_recv, fail); + live_price_listener listen_worker(listen_ioc, ssl_ctx, coins, live_input_recv, fail); listen_worker.run(); // Restartr the event loop. The call will return when @@ -128,3 +150,241 @@ int main(int argc, char** argv) return EXIT_SUCCESS; } + +#endif + +#if 0 +// Type your code here, or load an example. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace boost { + + template + class historic_fetcher_op + { + enum class state { + starting, + resolving, + connecting, + ssl_handshaking, + writing, + reading, + error, + complete + }; + + using resolver_type = asio::ip::basic_resolver< + asio::ip::tcp, Executor>; + + using tcp_stream_type = beast::basic_stream< + asio::ip::tcp, + Executor>; + + using ssl_stream_type = boost::asio::ssl::stream; + + std::unique_ptr resolver_p_; + std::unique_ptr ssl_stream_p_; + + std::string host_; + state state_; + + public: + explicit + historic_fetcher_op( + Executor& exec + , boost::asio::ssl::context& ctx) + : state_(state::starting) + { + // For this example use a hard-coded host name. + // In reality this would be stored in some form of configuration. + //host_ = "api.coinbase.com"; + host_ = "www.boost.org"; + //host_ = "ws-feed.exchange.coinbase.com"; + + resolver_p_ = std::make_unique(exec); + ssl_stream_p_ = std::make_unique(exec, ctx); + + // Set the expected hostname in the peer certificate for verification + + ssl_stream_p_->set_verify_callback(boost::asio::ssl::host_name_verification(host_)); + } + + template + void operator()( + Self& self + , system::error_code ec = {}) + { + if (ec) { + state_ = state::error; + return self.complete(ec); + } + + switch (state_) { + case state::starting: { + // Set SNI Hostname (many hosts need this to handshake successfully) + if (!SSL_set_tlsext_host_name(ssl_stream_p_->native_handle(), host_.c_str())) + { + system::error_code ssl_ec{ + static_cast(::ERR_get_error()), + asio::error::get_ssl_category() }; + state_ = state::error; + return self.complete(ssl_ec); + } + + // Look up the domain name + state_ = state::resolving; + resolver_p_->async_resolve( + host_, + "443", + std::move(self) + ); + } break; + case state::ssl_handshaking: { + // TODO: If there are any coins left to request, request one. + } break; + default: { + // This should not happen. + throw std::logic_error("unreachable"); + } + } + }; + + template + void operator()( + Self& self + , system::error_code ec + , asio::ip::tcp::resolver::results_type results) + { + if (ec) { + state_ = state::error; + return self.complete(ec); + } + + switch (state_) { + case state::resolving: { + // Set a timeout on the operation + beast::get_lowest_layer(*ssl_stream_p_).expires_after(std::chrono::seconds(30)); + + // Make the connection on the IP address we get from a lookup + state_ = state::connecting; + for (auto& r : results) { + std::cout << r.endpoint() << std::endl; + } + beast::get_lowest_layer(*ssl_stream_p_).async_connect( + results, + std::move(self) + ); + std::cout << "async_connect has been called" << std::endl; + } break; + default: { + // This should not happen. + throw std::logic_error("unreachable"); + } + } + }; + + template + void operator()( + Self& self + , system::error_code ec + , asio::ip::tcp::resolver::results_type::endpoint_type ep) + { + boost::ignore_unused(ep); + + if (ec) { + std::cout << "Connection failed" << std::endl; + state_ = state::error; + return self.complete(ec); + } + + switch (state_) { + case state::connecting: { + // Set a timeout on the operation + std::cout << "Connection succeeded" << std::endl; + + // This is where the ssl handshaking stuff will go if we + // can work out why the connection is failing. + + self.complete(ec); + } break; + default: { + // This should not happen. + throw std::logic_error("unreachable"); + } + } + }; + + }; + + template< + class Executor, + BOOST_ASIO_COMPLETION_TOKEN_FOR(void( + system::error_code)) CompletionToken> + BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void( + system::error_code)) + async_historic_fetch( + Executor& exec + , boost::asio::ssl::context& ssl_ctx + , CompletionToken&& token) + { + return asio::async_compose< + CompletionToken, + void(system::error_code)>( + historic_fetcher_op( + exec + , ssl_ctx) + , token + , exec); + } + +} // end namespace boost + +using namespace boost; + +int main() +{ + // The SSL context is required, and holds certificates + asio::ssl::context ssl_ctx{ asio::ssl::context::tlsv12_client }; + + // Verify the remote server's certificate + ssl_ctx.set_verify_mode(asio::ssl::verify_peer); + + // The io_context is required for all I/O + asio::io_context listen_ioc; + + // Construct and start a the fetcher of historic prices. + //asio::strand strand = asio::make_strand(listen_ioc); + auto executor = listen_ioc.get_executor(); + boost::async_historic_fetch( + executor, + ssl_ctx, + [](boost::system::error_code ec) + { + if (ec.failed()) + { + std::cerr << ec.message() << "\n"; + } + }); + + // Now we run the event loop until all historic prices have been received. + listen_ioc.run(); +} +#endif From b19a7a1c5789271221efcbc6a67a8a2b54226ae8 Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Wed, 15 Oct 2025 14:57:26 +0100 Subject: [PATCH 7/9] switch from using a state to tagging --- .../crypto-ai-ssl/historic_price_fetcher.hpp | 217 ++++++++---------- 1 file changed, 99 insertions(+), 118 deletions(-) diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp index f98a0616e8..8381b76f73 100644 --- a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -51,6 +52,12 @@ namespace boost { complete }; + struct on_resolve {}; + struct on_connect {}; + struct on_ssl_handshake {}; + struct on_write_request {}; + struct on_read_result {}; + using resolver_type = asio::ip::basic_resolver< asio::ip::tcp, Executor>; @@ -74,8 +81,6 @@ namespace boost { request_type& request_; response_type& response_; - state state_; - public: explicit @@ -89,188 +94,165 @@ namespace boost { : client_(client) , resolver_(resolver) , ssl_stream_(ssl_stream) - , state_(state::starting) , buffer_(buffer) , request_(request) , response_(response) { - // Set the expected hostname in the peer certificate for verification - ssl_stream_.set_verify_callback(boost::asio::ssl::host_name_verification(client.get_host())); - - // Set up an HTTP GET request message - request_.version(11); - request_.method(beast::http::verb::get); - request_.set(beast::http::field::host, client.get_host()); - request_.set(beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING); } + template + void operator()( + Self& self) + { + // Look up the domain name + resolver_.async_resolve( + client_.get_host(), + "https", + asio::prepend(std::move(self), on_resolve{}) + ); + }; + template void operator()( Self& self - , system::error_code ec = {}) + , on_ssl_handshake + , system::error_code ec) { if (ec) { - state_ = state::error; return self.complete(ec); } - switch (state_) { - case state::starting: { - // Set SNI Hostname (many hosts need this to handshake successfully) - if (!SSL_set_tlsext_host_name(ssl_stream_.native_handle(), client_.get_host())) - { - system::error_code ssl_ec{ - static_cast(::ERR_get_error()), - asio::error::get_ssl_category() }; - state_ = state::error; - return self.complete(ssl_ec); - } - - // Look up the domain name - state_ = state::resolving; - resolver_.async_resolve( - client_.get_host(), - "https", - std::move(self) - ); - } break; - case state::ssl_handshaking: { - // If there are any coins left to request, request one. - return send_next_request(self); - } break; - default: { - // This should not happen. - throw std::logic_error("unreachable"); - } - } + // Set up an HTTP GET request message + request_.version(11); + request_.method(beast::http::verb::get); + request_.set(beast::http::field::host, client_.get_host()); + request_.set(beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING); + + // If there are any coins left to request, request one. + return send_next_request(self); }; template void operator()( Self& self + , on_resolve , system::error_code ec , asio::ip::tcp::resolver::results_type results) { if (ec) { - state_ = state::error; return self.complete(ec); } - switch (state_) { - case state::resolving: { - // Set a timeout on the operation - beast::get_lowest_layer(ssl_stream_).expires_after(std::chrono::seconds(30)); + // Set a timeout on the operation + beast::get_lowest_layer(ssl_stream_).expires_after(std::chrono::seconds(30)); + + // Make the connection on the IP address we get from a lookup + beast::get_lowest_layer(ssl_stream_).async_connect( + results, + asio::prepend(std::move(self), on_connect{}) + ); - // Make the connection on the IP address we get from a lookup - state_ = state::connecting; - for (auto& r : results) { - std::cout << r.endpoint() << std::endl; - } - beast::get_lowest_layer(ssl_stream_).async_connect( - results, - std::move(self) - ); - } break; - default: { - // This should not happen. - throw std::logic_error("unreachable"); - } - } }; template void operator()( Self& self + , on_connect , system::error_code ec , asio::ip::tcp::resolver::results_type::endpoint_type ep) { boost::ignore_unused(ep); if (ec) { - state_ = state::error; return self.complete(ec); } - switch (state_) { - case state::connecting: { - // Set a timeout on the operation - beast::get_lowest_layer(ssl_stream_).expires_after(std::chrono::seconds(30)); - - // Perform the SSL handshake - state_ = state::ssl_handshaking; - ssl_stream_.async_handshake( - boost::asio::ssl::stream_base::client, - std::move(self) - ); - } break; - default: { - // This should not happen. - throw std::logic_error("unreachable"); + // Set the expected hostname in the peer certificate for verification + ssl_stream_.set_verify_callback(boost::asio::ssl::host_name_verification(client_.get_host())); + + // Set SNI Hostname (many hosts need this to handshake successfully) + if (!SSL_set_tlsext_host_name(ssl_stream_.native_handle(), client_.get_host())) + { + system::error_code ssl_ec{ + static_cast(::ERR_get_error()), + asio::error::get_ssl_category() }; + return self.complete(ssl_ec); } + + // Set a timeout on the operation + beast::get_lowest_layer(ssl_stream_).expires_after(std::chrono::seconds(30)); + + // Perform the SSL handshake + ssl_stream_.async_handshake( + boost::asio::ssl::stream_base::client, + asio::prepend(std::move(self), on_ssl_handshake{}) + ); + }; + + template + void operator()( + Self& self + , on_write_request + , system::error_code ec + , std::size_t bytes_transferred) + { + boost::ignore_unused(bytes_transferred); + + if (ec) { + return self.complete(ec); } + + // Read a message into our buffer + beast::http::async_read( + ssl_stream_, + buffer_, + response_, + asio::prepend(std::move(self), on_read_result{}) + ); + }; template void operator()( Self& self + , on_read_result , system::error_code ec , std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); - if (state_ == state::reading && ec == beast::http::error::end_of_stream) { - state_ = state::complete; + if (ec == beast::http::error::end_of_stream) { system::error_code ok{}; return self.complete(ok); } else if (ec) { - state_ = state::error; return self.complete(ec); } - switch (state_) { - case state::writing: { - // Read a message into our buffer - state_ = state::reading; - beast::http::async_read( - ssl_stream_, - buffer_, - response_, - std::move(self) - ); - } break; - case state::reading: { - // Process the received message - - // Write the message to standard out - //std::cout << "Response: " << response_ << "\n" << std::endl; - std::cout << "Body: " << response_.body() << "\n\n" << std::endl; - - // The response can be quite long, and we can avoid an allocation - // by swapping the body into an empty string. - std::string temp; - temp.swap(response_.body()); - - client_.process_response(temp, ec); - if (ec) { - state_ = state::error; - return self.complete(ec); - } + // Process the received message - send_next_request(self); - } break; - default: { - // This should not happen. - throw std::logic_error("unreachable"); - } + // Write the message to standard out + //std::cout << "Response: " << response_ << "\n" << std::endl; + std::cout << "Body: " << response_.body() << "\n\n" << std::endl; + + // The response can be quite long, and we can avoid an allocation + // by swapping the body into an empty string. + std::string temp; + temp.swap(response_.body()); + + client_.process_response(temp, ec); + if (ec) { + return self.complete(ec); } + + send_next_request(self); + }; template void send_next_request(Self& self) { if (!client_.requests_outstanding()) { - state_ = state::complete; system::error_code ok{}; return self.complete(ok); } @@ -282,11 +264,10 @@ namespace boost { beast::get_lowest_layer(ssl_stream_).expires_after(std::chrono::seconds(30)); // Send the message - state_ = state::writing; beast::http::async_write( ssl_stream_, request_, - std::move(self) + asio::prepend(std::move(self), on_write_request{}) ); } }; From c468a524db1ce08723111f6d60139d2d34e957b9 Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Thu, 16 Oct 2025 11:35:04 +0100 Subject: [PATCH 8/9] address git comments re historic price fetcher --- .../crypto-ai-ssl/historic_price_fetcher.hpp | 280 ++++++++---------- .../websocket_client_crypto_ai_ssl.cpp | 6 +- 2 files changed, 133 insertions(+), 153 deletions(-) diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp index 8381b76f73..35b6a5800e 100644 --- a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp @@ -11,6 +11,7 @@ #define BOOST_BEAST_EXAMPLE_HISTORIC_PRICE_FETCHER #include +#include #include #include #include @@ -41,17 +42,6 @@ namespace boost { template class historic_fetcher_op { - enum class state { - starting, - resolving, - connecting, - ssl_handshaking, - writing, - reading, - error, - complete - }; - struct on_resolve {}; struct on_connect {}; struct on_ssl_handshake {}; @@ -73,30 +63,13 @@ namespace boost { using request_type = beast::http::request; using response_type = beast::http::response; - client_type& client_; - resolver_type& resolver_; - ssl_stream_type& ssl_stream_; - - buffer_type& buffer_; - request_type& request_; - response_type& response_; - public: explicit historic_fetcher_op( - client_type& client - , resolver_type& resolver - , ssl_stream_type& ssl_stream - , buffer_type& buffer - , request_type& request - , response_type& response) + client_type& client) : client_(client) - , resolver_(resolver) - , ssl_stream_(ssl_stream) - , buffer_(buffer) - , request_(request) - , response_(response) + , start_of_day_(0) { } @@ -105,32 +78,15 @@ namespace boost { Self& self) { // Look up the domain name - resolver_.async_resolve( + client_.resolver_.async_resolve( client_.get_host(), "https", - asio::prepend(std::move(self), on_resolve{}) + asio::cancel_after( + std::chrono::seconds(30), + asio::prepend(std::move(self), on_resolve{})) ); }; - template - void operator()( - Self& self - , on_ssl_handshake - , system::error_code ec) - { - if (ec) { - return self.complete(ec); - } - - // Set up an HTTP GET request message - request_.version(11); - request_.method(beast::http::verb::get); - request_.set(beast::http::field::host, client_.get_host()); - request_.set(beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING); - - // If there are any coins left to request, request one. - return send_next_request(self); - }; template void operator()( @@ -140,20 +96,20 @@ namespace boost { , asio::ip::tcp::resolver::results_type results) { if (ec) { - return self.complete(ec); + return do_complete(self, ec); } - // Set a timeout on the operation - beast::get_lowest_layer(ssl_stream_).expires_after(std::chrono::seconds(30)); - // Make the connection on the IP address we get from a lookup - beast::get_lowest_layer(ssl_stream_).async_connect( + beast::get_lowest_layer(client_.ssl_stream_).async_connect( results, - asio::prepend(std::move(self), on_connect{}) - ); + asio::cancel_after( + std::chrono::seconds(30), + asio::prepend(std::move(self), on_connect{}))); }; + + template void operator()( Self& self @@ -164,31 +120,56 @@ namespace boost { boost::ignore_unused(ep); if (ec) { - return self.complete(ec); + return do_complete(self, ec); } // Set the expected hostname in the peer certificate for verification - ssl_stream_.set_verify_callback(boost::asio::ssl::host_name_verification(client_.get_host())); + client_.ssl_stream_.set_verify_callback(boost::asio::ssl::host_name_verification(client_.get_host())); // Set SNI Hostname (many hosts need this to handshake successfully) - if (!SSL_set_tlsext_host_name(ssl_stream_.native_handle(), client_.get_host())) + if (!SSL_set_tlsext_host_name(client_.ssl_stream_.native_handle(), client_.get_host())) { system::error_code ssl_ec{ static_cast(::ERR_get_error()), asio::error::get_ssl_category() }; - return self.complete(ssl_ec); + return do_complete(self, ssl_ec); } - // Set a timeout on the operation - beast::get_lowest_layer(ssl_stream_).expires_after(std::chrono::seconds(30)); - // Perform the SSL handshake - ssl_stream_.async_handshake( + client_.ssl_stream_.async_handshake( boost::asio::ssl::stream_base::client, - asio::prepend(std::move(self), on_ssl_handshake{}) + asio::cancel_after( + std::chrono::seconds(30), + asio::prepend(std::move(self), on_ssl_handshake{})) ); }; + + template + void operator()( + Self& self + , on_ssl_handshake + , system::error_code ec) + { + if (ec) { + return do_complete(self, ec); + } + + // Find the subscription start time + posix_time::ptime ptime = posix_time::second_clock::universal_time(); + posix_time::ptime sod = posix_time::ptime(ptime.date()); + start_of_day_ = posix_time::to_time_t(sod); + + // Set up an HTTP GET request message + client_.request_.version(11); + client_.request_.method(beast::http::verb::get); + client_.request_.set(beast::http::field::host, client_.get_host()); + client_.request_.set(beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING); + + // If there are any coins left to request, request one. + return send_next_request(self); + }; + template void operator()( Self& self @@ -199,17 +180,18 @@ namespace boost { boost::ignore_unused(bytes_transferred); if (ec) { - return self.complete(ec); + return do_complete(self, ec); } // Read a message into our buffer beast::http::async_read( - ssl_stream_, - buffer_, - response_, - asio::prepend(std::move(self), on_read_result{}) + client_.ssl_stream_, + client_.buffer_, + client_.response_, + asio::cancel_after( + std::chrono::seconds(30), + asio::prepend(std::move(self), on_read_result{})) ); - }; template @@ -223,53 +205,64 @@ namespace boost { if (ec == beast::http::error::end_of_stream) { system::error_code ok{}; - return self.complete(ok); + return do_complete(self, ok); } else if (ec) { - return self.complete(ec); + return do_complete(self, ec); } // Process the received message // Write the message to standard out //std::cout << "Response: " << response_ << "\n" << std::endl; - std::cout << "Body: " << response_.body() << "\n\n" << std::endl; + std::cout << "Body: " << client_.response_.body() << "\n\n" << std::endl; // The response can be quite long, and we can avoid an allocation // by swapping the body into an empty string. std::string temp; - temp.swap(response_.body()); + temp.swap(client_.response_.body()); - client_.process_response(temp, ec); + client_.process_response(temp, start_of_day_, ec); if (ec) { - return self.complete(ec); + return do_complete(self, ec); } send_next_request(self); }; + template + void do_complete(Self& self, system::error_code ec) + { + self.complete(ec); + client_.running_.clear(); + return; + } + template void send_next_request(Self& self) { if (!client_.requests_outstanding()) { system::error_code ok{}; - return self.complete(ok); + return do_complete(self, ok); } // Set up an HTTP GET request message - request_.target(client_.next_request()); - - // Set a timeout on the operation - beast::get_lowest_layer(ssl_stream_).expires_after(std::chrono::seconds(30)); + client_.request_.target(client_.next_request()); // Send the message beast::http::async_write( - ssl_stream_, - request_, - asio::prepend(std::move(self), on_write_request{}) + client_.ssl_stream_, + client_.request_, + asio::cancel_after( + std::chrono::seconds(30), + asio::prepend(std::move(self), on_write_request{})) ); } + + private: + client_type& client_; + std::time_t start_of_day_; }; template @@ -288,39 +281,15 @@ namespace boost { using request_type = beast::http::request; using response_type = beast::http::response; - std::function receive_handler_; - - resolver_type resolver_; - ssl_stream_type ssl_stream_; - - buffer_type buffer_; - request_type request_; - response_type response_; - - std::vector coins_; - std::time_t start_of_day_; - std::string host_; - - std::string current_coin_; - - // It is more efficient to persist the json parser so that memory allocation does not need - // to be repeated each time we docode a message - json::parser parser_; + friend class historic_fetcher_op; public: - //using socket_type = - // asio::basic_socket; - - //using endpoint_type = asio::ip::tcp::endpoint; explicit historic_fetcher( Executor& exec , boost::asio::ssl::context& ctx - , const std::vector& coins + , const boost::string_view host , std::function + BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void( + system::error_code)) + async_historic_fetch( + const std::vector& coins, + CompletionToken&& token) + { + coins_ = coins; - // Find the subscription start time - posix_time::ptime ptime = posix_time::second_clock::universal_time(); - posix_time::ptime sod = posix_time::ptime(ptime.date()); - start_of_day_ = posix_time::to_time_t(sod); + bool already_running = running_.test_and_set(); + BOOST_ASSERT(!already_running); + + return asio::async_compose< + CompletionToken, + void(system::error_code)>( + historic_fetcher_op( + *this) + , token + , ssl_stream_); } + private: const char* get_host() { return host_.c_str(); } @@ -375,7 +355,7 @@ namespace boost { } void process_response( - boost::core::string_view str, boost::system::error_code& ec) + boost::core::string_view str, std::time_t start_of_day, boost::system::error_code& ec) { parser_.reset(); @@ -403,7 +383,7 @@ namespace boost { std::size_t str_size = 0; std::time_t start_time = static_cast(std::stoll(startstr, &str_size)); - if (start_time > start_of_day_) { + if (start_time > start_of_day) { double open = std::stod(openstr, &str_size); if (receive_handler_) @@ -424,29 +404,27 @@ namespace boost { } } - template< - class Executor, - BOOST_ASIO_COMPLETION_TOKEN_FOR(void( - system::error_code)) CompletionToken> - BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void( - system::error_code)) - async_historic_fetch( - Executor& exec - , CompletionToken&& token) - { - return asio::async_compose< - CompletionToken, - void(system::error_code)>( - historic_fetcher_op( - *this - , resolver_ - , ssl_stream_ - , buffer_ - , request_ - , response_) - , token - , exec); - } + std::function receive_handler_; + + resolver_type resolver_; + ssl_stream_type ssl_stream_; + + buffer_type buffer_; + request_type request_; + response_type response_; + + std::vector coins_; + std::string host_; + std::atomic_flag running_; + + std::string current_coin_; + + // It is more efficient to persist the json parser so that memory allocation does not need + // to be repeated each time we docode a message + json::parser parser_; }; diff --git a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp index 940250a9a3..0eff612c36 100644 --- a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp +++ b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp @@ -115,14 +115,16 @@ int main(int argc, char** argv) // Construct and start a the fetcher of historic prices. asio::strand strand = asio::make_strand(listen_ioc); + std::string host = "api.coinbase.com"; + auto fetcher = boost::historic_fetcher( strand, ssl_ctx, - coins, + host, historic_input_recv); fetcher.async_historic_fetch( - strand, + coins, [](boost::system::error_code ec) { if (ec.failed()) From 41f4d9fa2049bc3feab5d80ea7a2604f7b8689ad Mon Sep 17 00:00:00 2001 From: Mungo Gill Date: Thu, 16 Oct 2025 13:34:57 +0100 Subject: [PATCH 9/9] more github feedback changes --- .../crypto-ai-ssl/historic_price_fetcher.cpp | 319 -------- .../crypto-ai-ssl/historic_price_fetcher.hpp | 745 ++++++++---------- .../crypto-ai-ssl/live_price_listener.cpp | 7 +- .../crypto-ai-ssl/live_price_listener.hpp | 448 ++++++++++- .../websocket_client_crypto_ai_ssl.cpp | 9 +- 5 files changed, 754 insertions(+), 774 deletions(-) diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp index 3b5b6fcf7d..7fa69752ad 100644 --- a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.cpp @@ -7,7 +7,6 @@ // Official repository: https://github.com/boostorg/beast // -#include "processor_base.hpp" #include "historic_price_fetcher.hpp" #include @@ -32,322 +31,4 @@ #include #include -#if 0 -using namespace boost; -using namespace std::placeholders; - -using namespace beast; // from - -using tcp = boost::asio::ip::tcp; // from - -// Start the asynchronous operation -void historic_price_fetcher::run() -{ - // For this example use a hard-coded host name. - // In reality this would be stored in some form of configuration. - host_ = "api.coinbase.com"; - //host_ = "ws-feed.exchange.coinbase.com"; - - // Set SNI Hostname (many hosts need this to handshake successfully) - if (!SSL_set_tlsext_host_name(stream_.native_handle(), host_.c_str())) - { - system::error_code ec{ - static_cast(::ERR_get_error()), - net::error::get_ssl_category() }; - return error_handler_(ec, "SNI"); - } - - // Set the expected hostname in the peer certificate for verification - stream_.set_verify_callback(boost::asio::ssl::host_name_verification(host_)); - - // Set up an HTTP GET request message - req_.version(11); - req_.method(http::verb::get); - req_.set(http::field::host, host_); - req_.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); - - active_ = true; - - // Find the subscription start time - posix_time::ptime ptime = posix_time::second_clock::universal_time(); - posix_time::ptime sod = posix_time::ptime(ptime.date()); - start_of_day_ = posix_time::to_time_t(sod); - - // Look up the domain name - resolver_.async_resolve( - host_, - "https", - [this](error_code ec, tcp::resolver::results_type results) - { - on_resolve(ec, results); - } - ); -} - -void historic_price_fetcher::cancel() -{ - active_ = false; - - net::post(strand_, [this]() - { - if (beast::get_lowest_layer(stream_).socket().is_open()) - stream_.async_shutdown( - [this](error_code ec) - { - on_shutdown(ec); - } - ); - } - ); -} - -void historic_price_fetcher::on_resolve(system::error_code ec, tcp::resolver::results_type results) -{ - if (ec) { - cancel(); - return error_handler_(ec, "resolve"); - } - - if (!active_) - return; - - // Set a timeout on the operation - beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); - - // Make the connection on the IP address we get from a lookup - beast::get_lowest_layer(stream_).async_connect( - results, - [this](error_code ec, tcp::resolver::results_type::endpoint_type ep) - { - on_connect(ec, ep); - } - ); -} - -void historic_price_fetcher::on_connect(system::error_code ec, tcp::resolver::results_type::endpoint_type ep) -{ - boost::ignore_unused(ep); - - if (ec) { - cancel(); - return error_handler_(ec, "connect"); - } - - if (!active_) - return; - - // Set a timeout on the operation - beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); - - // Perform the SSL handshake - stream_.async_handshake( - boost::asio::ssl::stream_base::client, - [this](error_code ec) - { - on_ssl_handshake(ec); - } - ); -} - -void historic_price_fetcher::on_ssl_handshake(system::error_code ec) -{ - if (ec) { - cancel(); - return error_handler_(ec, "ssl_handshake"); - } - - if (!active_) - return; - - if (coins_.size() > 0) - next_request(); - else { - cancel(); - } -} - -void historic_price_fetcher::next_request() -{ - if (coins_.size() > 0) - { - current_coin_ = coins_.back(); - coins_.pop_back(); - - urls::url url = - urls::format( - "{}://api.coinbase.com/api/v3/brokerage/market/products/{}/candles", - "https", - current_coin_); - - // Data - url.params().append({ "granularity", "ONE_MINUTE"}); - //url.params().append({ "start", std::to_string(start_of_day_) }); - url.params().append({ "limit", std::to_string(5)}); - - // Set up an HTTP GET request message - req_.target(url); - //req_.target("/v2/prices/" + security + "/spot"); - - // Set a timeout on the operation - beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); - - // Send the message - http::async_write(stream_, req_, - [this](error_code ec, std::size_t bytes_transferred) - { - on_write(ec, bytes_transferred); - } - ); - } - else { - cancel(); - } -} - -void historic_price_fetcher::on_write(system::error_code ec, std::size_t bytes_transferred) -{ - boost::ignore_unused(bytes_transferred); - - if (ec) { - cancel(); - return error_handler_(ec, "write"); - } - - if (!active_) - return; - - // Read a message into our buffer - http::async_read( - stream_, buffer_, response_, - [this](error_code ec, std::size_t bytes_transferred) - { - on_read(ec, bytes_transferred); - } - ); -} - - -void historic_price_fetcher::on_read(system::error_code ec, std::size_t bytes_transferred) -{ - boost::ignore_unused(bytes_transferred); - - // This indicates that the session was closed - if (ec == http::error::end_of_stream) { - cancel(); - return; - } - else if (ec) { - cancel(); - if (active_) return error_handler_(ec, "read"); - } - - if (!active_) - return; - - // Write the message to standard out - //std::cout << "Response: " << response_ << "\n" << std::endl; - std::cout << "Body: " << response_.body() << "\n\n" << std::endl; - - // The response can be quite long, and we can avoid an allocation - // by swapping the body into an empty string. - std::string temp; - temp.swap(response_.body()); - - parse_json(temp); - - //receive_handler_(std::move(temp)); - - next_request(); - - //// Set a timeout on the operation - //beast::get_lowest_layer(stream_).expires_after(std::chrono::seconds(30)); - - //// Gracefully close the stream - //stream_.async_shutdown( - // [this](error_code ec) - // { - // on_shutdown(ec); - // } - //); - - //receive_handler_(beast::buffers_to_string(buffer_.data())); - - /* - buffer_.consume(buffer_.max_size()); - - count++; - if (count > 3) cancel(); - - ws_.async_read( - buffer_, - [this] (error_code ec, std::size_t bytes_transferred) - { - on_read(ec, bytes_transferred); - } - ); - */ -} - - -void historic_price_fetcher::parse_json(core::string_view str) -{ - parser_.reset(); - - boost::system::error_code ec; - parser_.write(str, ec); - - if (ec) - return error_handler_(ec, "historic_price_fetcher::parse_json"); - - json::value jv(parser_.release()); - - // Design note: the json parsing could be done without using the exception - // interface, but the resulting code would be considerably more verbose. - try { - auto candle_list = jv.as_object().at("candles").as_array(); - - if (candle_list.size() == 0) - return error_handler_(ec, "historic_price_fetcher::parse_json no prices"); - - // The coinbase API provides the most recent values first. - for (auto it = candle_list.crbegin(); it != candle_list.crend(); it++) { - core::string_view startstr = it->as_object().at("start").as_string(); - core::string_view openstr = it->as_object().at("open").as_string(); - core::string_view closestr = it->as_object().at("close").as_string(); - - std::size_t str_size = 0; - std::time_t start_time = static_cast(std::stoll(startstr, &str_size)); - if (start_time > start_of_day_) { - double open = std::stod(openstr, &str_size); - - if (receive_handler_) - receive_handler_(current_coin_, std::chrono::system_clock::from_time_t(start_time), open); - - std::cout << "Decoded historic " << current_coin_ << " price: " << open << " at " << std::chrono::system_clock::from_time_t(start_time) << std::endl; - } - } - } - catch (boost::system::system_error se) { - return error_handler_(ec, "historic_price_fetcher::parse_json parse failure"); - } - catch (std::invalid_argument se) { - return error_handler_(ec, "historic_price_fetcher::parse_json parse failure"); - } - catch (std::out_of_range se) { - return error_handler_(ec, "historic_price_fetcher::parse_json parse failure"); - } -} - -void historic_price_fetcher::on_shutdown(system::error_code ec) -{ - if (ec && ec != boost::asio::ssl::error::stream_truncated) - return error_handler_(ec, "shutdown"); - - // If we get here then the connection is closed gracefully - - // The make_printable() function helps print a ConstBufferSequence - std::cout << "Final buffer content:" << beast::make_printable(buffer_.data()) << std::endl; -} - -#endif diff --git a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp index 35b6a5800e..6114465820 100644 --- a/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp +++ b/example/websocket/client/crypto-ai-ssl/historic_price_fetcher.hpp @@ -36,495 +36,396 @@ namespace boost { - template - class historic_fetcher; +template +class historic_fetcher; - template - class historic_fetcher_op - { - struct on_resolve {}; - struct on_connect {}; - struct on_ssl_handshake {}; - struct on_write_request {}; - struct on_read_result {}; - - using resolver_type = asio::ip::basic_resolver< - asio::ip::tcp, Executor>; - - using tcp_stream_type = beast::basic_stream< - asio::ip::tcp, - Executor>; +template +class historic_fetcher_op +{ + struct on_resolve {}; + struct on_connect {}; + struct on_ssl_handshake {}; + struct on_write_request {}; + struct on_read_result {}; - using ssl_stream_type = boost::asio::ssl::stream; + using resolver_type = asio::ip::basic_resolver< + asio::ip::tcp, Executor>; - using client_type = historic_fetcher; + using tcp_stream_type = beast::basic_stream< + asio::ip::tcp, + Executor>; - using buffer_type = boost::beast::flat_buffer; - using request_type = beast::http::request; - using response_type = beast::http::response; + using ssl_stream_type = boost::asio::ssl::stream; - public: + using client_type = historic_fetcher; - explicit - historic_fetcher_op( - client_type& client) - : client_(client) - , start_of_day_(0) - { - } + using buffer_type = boost::beast::flat_buffer; + using request_type = beast::http::request; + using response_type = beast::http::response; - template - void operator()( - Self& self) - { - // Look up the domain name - client_.resolver_.async_resolve( - client_.get_host(), - "https", - asio::cancel_after( - std::chrono::seconds(30), - asio::prepend(std::move(self), on_resolve{})) - ); - }; - - - template - void operator()( - Self& self - , on_resolve - , system::error_code ec - , asio::ip::tcp::resolver::results_type results) - { - if (ec) { - return do_complete(self, ec); - } +public: - // Make the connection on the IP address we get from a lookup - beast::get_lowest_layer(client_.ssl_stream_).async_connect( - results, - asio::cancel_after( - std::chrono::seconds(30), - asio::prepend(std::move(self), on_connect{}))); + explicit + historic_fetcher_op( + client_type& client) + : client_(client) + , start_of_day_(0) + { + } - }; + template + void operator()( + Self& self) + { + // Look up the domain name + client_.resolver_.async_resolve( + client_.get_host(), + "https", + asio::cancel_after( + std::chrono::seconds(30), + asio::prepend(std::move(self), on_resolve{})) + ); + }; + template + void operator()( + Self& self + , on_resolve + , system::error_code ec + , asio::ip::tcp::resolver::results_type results) + { + if (ec) { + return do_complete(self, ec); + } - template - void operator()( - Self& self - , on_connect - , system::error_code ec - , asio::ip::tcp::resolver::results_type::endpoint_type ep) - { - boost::ignore_unused(ep); + // Make the connection on the IP address we get from a lookup + beast::get_lowest_layer(client_.ssl_stream_).async_connect( + results, + asio::cancel_after( + std::chrono::seconds(30), + asio::prepend(std::move(self), on_connect{}))); - if (ec) { - return do_complete(self, ec); - } + }; - // Set the expected hostname in the peer certificate for verification - client_.ssl_stream_.set_verify_callback(boost::asio::ssl::host_name_verification(client_.get_host())); - // Set SNI Hostname (many hosts need this to handshake successfully) - if (!SSL_set_tlsext_host_name(client_.ssl_stream_.native_handle(), client_.get_host())) - { - system::error_code ssl_ec{ - static_cast(::ERR_get_error()), - asio::error::get_ssl_category() }; - return do_complete(self, ssl_ec); - } - // Perform the SSL handshake - client_.ssl_stream_.async_handshake( - boost::asio::ssl::stream_base::client, - asio::cancel_after( - std::chrono::seconds(30), - asio::prepend(std::move(self), on_ssl_handshake{})) - ); - }; - - - template - void operator()( - Self& self - , on_ssl_handshake - , system::error_code ec) - { - if (ec) { - return do_complete(self, ec); - } + template + void operator()( + Self& self + , on_connect + , system::error_code ec + , asio::ip::tcp::resolver::results_type::endpoint_type ep) + { + boost::ignore_unused(ep); - // Find the subscription start time - posix_time::ptime ptime = posix_time::second_clock::universal_time(); - posix_time::ptime sod = posix_time::ptime(ptime.date()); - start_of_day_ = posix_time::to_time_t(sod); - - // Set up an HTTP GET request message - client_.request_.version(11); - client_.request_.method(beast::http::verb::get); - client_.request_.set(beast::http::field::host, client_.get_host()); - client_.request_.set(beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING); - - // If there are any coins left to request, request one. - return send_next_request(self); - }; - - template - void operator()( - Self& self - , on_write_request - , system::error_code ec - , std::size_t bytes_transferred) - { - boost::ignore_unused(bytes_transferred); + if (ec) { + return do_complete(self, ec); + } - if (ec) { - return do_complete(self, ec); - } + // Set the expected hostname in the peer certificate for verification + client_.ssl_stream_.set_verify_callback(boost::asio::ssl::host_name_verification(client_.get_host())); - // Read a message into our buffer - beast::http::async_read( - client_.ssl_stream_, - client_.buffer_, - client_.response_, - asio::cancel_after( - std::chrono::seconds(30), - asio::prepend(std::move(self), on_read_result{})) - ); - }; - - template - void operator()( - Self& self - , on_read_result - , system::error_code ec - , std::size_t bytes_transferred) + // Set SNI Hostname (many hosts need this to handshake successfully) + if (!SSL_set_tlsext_host_name(client_.ssl_stream_.native_handle(), client_.get_host())) { - boost::ignore_unused(bytes_transferred); - - if (ec == beast::http::error::end_of_stream) { - system::error_code ok{}; - return do_complete(self, ok); - } - else if (ec) { - return do_complete(self, ec); - } - - // Process the received message + system::error_code ssl_ec{ + static_cast(::ERR_get_error()), + asio::error::get_ssl_category() }; + return do_complete(self, ssl_ec); + } - // Write the message to standard out - //std::cout << "Response: " << response_ << "\n" << std::endl; - std::cout << "Body: " << client_.response_.body() << "\n\n" << std::endl; + // Perform the SSL handshake + client_.ssl_stream_.async_handshake( + boost::asio::ssl::stream_base::client, + asio::cancel_after( + std::chrono::seconds(30), + asio::prepend(std::move(self), on_ssl_handshake{})) + ); + }; - // The response can be quite long, and we can avoid an allocation - // by swapping the body into an empty string. - std::string temp; - temp.swap(client_.response_.body()); - client_.process_response(temp, start_of_day_, ec); - if (ec) { - return do_complete(self, ec); - } + template + void operator()( + Self& self + , on_ssl_handshake + , system::error_code ec) + { + if (ec) { + return do_complete(self, ec); + } - send_next_request(self); + // Find the subscription start time + posix_time::ptime ptime = posix_time::second_clock::universal_time(); + posix_time::ptime sod = posix_time::ptime(ptime.date()); + start_of_day_ = posix_time::to_time_t(sod); - }; + // Set up an HTTP GET request message + client_.request_.version(11); + client_.request_.method(beast::http::verb::get); + client_.request_.set(beast::http::field::host, client_.get_host()); + client_.request_.set(beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING); - template - void do_complete(Self& self, system::error_code ec) - { - self.complete(ec); - client_.running_.clear(); - return; - } + // If there are any coins left to request, request one. + return send_next_request(self); + }; - template - void send_next_request(Self& self) - { - if (!client_.requests_outstanding()) { - system::error_code ok{}; - return do_complete(self, ok); - } + template + void operator()( + Self& self + , on_write_request + , system::error_code ec + , std::size_t bytes_transferred) + { + boost::ignore_unused(bytes_transferred); - // Set up an HTTP GET request message - client_.request_.target(client_.next_request()); - - // Send the message - beast::http::async_write( - client_.ssl_stream_, - client_.request_, - asio::cancel_after( - std::chrono::seconds(30), - asio::prepend(std::move(self), on_write_request{})) - ); + if (ec) { + return do_complete(self, ec); } - private: - client_type& client_; - std::time_t start_of_day_; + // Read a message into our buffer + beast::http::async_read( + client_.ssl_stream_, + client_.buffer_, + client_.response_, + asio::cancel_after( + std::chrono::seconds(30), + asio::prepend(std::move(self), on_read_result{})) + ); }; - template - class historic_fetcher + template + void operator()( + Self& self + , on_read_result + , system::error_code ec + , std::size_t bytes_transferred) { - using resolver_type = asio::ip::basic_resolver< - asio::ip::tcp, Executor>; - - using tcp_stream_type = beast::basic_stream< - asio::ip::tcp, - Executor>; - - using ssl_stream_type = boost::asio::ssl::stream; - - using buffer_type = boost::beast::flat_buffer; - using request_type = beast::http::request; - using response_type = beast::http::response; - - friend class historic_fetcher_op; - - public: - - explicit - historic_fetcher( - Executor& exec - , boost::asio::ssl::context& ctx - , const boost::string_view host - , std::function receive_handler) - : receive_handler_(receive_handler) - , resolver_(exec) - , ssl_stream_(exec, ctx) - , host_(host) - { - running_.clear(); - } + boost::ignore_unused(bytes_transferred); - template< - BOOST_ASIO_COMPLETION_TOKEN_FOR(void( - system::error_code)) CompletionToken> - BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void( - system::error_code)) - async_historic_fetch( - const std::vector& coins, - CompletionToken&& token) - { - coins_ = coins; - - bool already_running = running_.test_and_set(); - BOOST_ASSERT(!already_running); - - return asio::async_compose< - CompletionToken, - void(system::error_code)>( - historic_fetcher_op( - *this) - , token - , ssl_stream_); + if (ec == beast::http::error::end_of_stream) { + system::error_code ok{}; + return do_complete(self, ok); } - - private: - const char* get_host() { - return host_.c_str(); - } - - bool requests_outstanding() - { - return coins_.size() != 0; + else if (ec) { + return do_complete(self, ec); } - std::string next_request() - { - current_coin_ = coins_.back(); - coins_.pop_back(); + // Process the received message - urls::url url = - urls::format( - "{}://api.coinbase.com/api/v3/brokerage/market/products/{}/candles", - "https", - current_coin_); + // Write the message to standard out + //std::cout << "Response: " << response_ << "\n" << std::endl; + std::cout << "Body: " << client_.response_.body() << "\n\n" << std::endl; - // Data - url.params().append({ "granularity", "ONE_MINUTE" }); - //url.params().append({ "start", std::to_string(start_of_day_) }); - url.params().append({ "limit", std::to_string(5) }); + // The response can be quite long, and we can avoid an allocation + // by swapping the body into an empty string. + std::string temp; + temp.swap(client_.response_.body()); - return url.buffer(); + client_.process_response(temp, start_of_day_, ec); + if (ec) { + return do_complete(self, ec); } - void process_response( - boost::core::string_view str, std::time_t start_of_day, boost::system::error_code& ec) - { - parser_.reset(); + send_next_request(self); - parser_.write(str, ec); + }; - if (ec) return; + template + void do_complete(Self& self, system::error_code ec) + { + self.complete(ec); + client_.running_.clear(); + return; + } - json::value jv(parser_.release()); + template + void send_next_request(Self& self) + { + if (!client_.requests_outstanding()) { + system::error_code ok{}; + return do_complete(self, ok); + } - // Design note: the json parsing could be done without using the exception - // interface, but the resulting code would be considerably more verbose. - try { - auto candle_list = jv.as_object().at("candles").as_array(); + // Set up an HTTP GET request message + client_.request_.target(client_.next_request()); + + // Send the message + beast::http::async_write( + client_.ssl_stream_, + client_.request_, + asio::cancel_after( + std::chrono::seconds(30), + asio::prepend(std::move(self), on_write_request{})) + ); + } + +private: + client_type& client_; + std::time_t start_of_day_; +}; + +template +class historic_fetcher +{ + using resolver_type = asio::ip::basic_resolver< + asio::ip::tcp, Executor>; + + using tcp_stream_type = beast::basic_stream< + asio::ip::tcp, + Executor>; + + using ssl_stream_type = boost::asio::ssl::stream; + + using buffer_type = boost::beast::flat_buffer; + using request_type = beast::http::request; + using response_type = beast::http::response; + + friend class historic_fetcher_op; + +public: + + explicit + historic_fetcher( + Executor& exec + , boost::asio::ssl::context& ctx + , const boost::string_view host + , std::function receive_handler) + : receive_handler_(receive_handler) + , resolver_(exec) + , ssl_stream_(exec, ctx) + , host_(host) + { + running_.clear(); + } + + template< + BOOST_ASIO_COMPLETION_TOKEN_FOR(void( + system::error_code)) CompletionToken> + BOOST_ASIO_INITFN_AUTO_RESULT_TYPE(CompletionToken, void( + system::error_code)) + async_historic_fetch( + const std::vector& coins, + CompletionToken&& token) + { + coins_ = coins; + + bool already_running = running_.test_and_set(); + BOOST_ASSERT(!already_running); + + return asio::async_compose< + CompletionToken, + void(system::error_code)>( + historic_fetcher_op( + *this) + , token + , ssl_stream_); + } + +private: + const char* get_host() { + return host_.c_str(); + } + + bool requests_outstanding() + { + return coins_.size() != 0; + } - if (candle_list.size() == 0) { - ec = json::make_error_code(boost::json::error::size_mismatch); - return; - } + std::string next_request() + { + current_coin_ = coins_.back(); + coins_.pop_back(); - // The coinbase API provides the most recent values first. - for (auto it = candle_list.crbegin(); it != candle_list.crend(); it++) { - core::string_view startstr = it->as_object().at("start").as_string(); - core::string_view openstr = it->as_object().at("open").as_string(); - core::string_view closestr = it->as_object().at("close").as_string(); + urls::url url = + urls::format( + "{}://api.coinbase.com/api/v3/brokerage/market/products/{}/candles", + "https", + current_coin_); - std::size_t str_size = 0; - std::time_t start_time = static_cast(std::stoll(startstr, &str_size)); - if (start_time > start_of_day) { - double open = std::stod(openstr, &str_size); + // Data + url.params().append({ "granularity", "ONE_MINUTE" }); + //url.params().append({ "start", std::to_string(start_of_day_) }); + url.params().append({ "limit", std::to_string(5) }); - if (receive_handler_) - receive_handler_(current_coin_, std::chrono::system_clock::from_time_t(start_time), open); + return url.buffer(); + } - std::cout << "Decoded historic " << current_coin_ << " price: " << open << " at " << std::chrono::system_clock::from_time_t(start_time) << std::endl; - } - } - } - catch (boost::system::system_error se) { - ec = se.code(); - } - catch (std::invalid_argument se) { - ec = json::make_error_code(boost::json::error::incomplete); - } - catch (std::out_of_range se) { - ec = json::make_error_code(boost::json::error::out_of_range); - } - } + void process_response( + boost::core::string_view str, std::time_t start_of_day, boost::system::error_code& ec) + { + parser_.reset(); - std::function receive_handler_; + parser_.write(str, ec); - resolver_type resolver_; - ssl_stream_type ssl_stream_; + if (ec) return; - buffer_type buffer_; - request_type request_; - response_type response_; + json::value jv(parser_.release()); - std::vector coins_; - std::string host_; - std::atomic_flag running_; + // Design note: the json parsing could be done without using the exception + // interface, but the resulting code would be considerably more verbose. + try { + auto candle_list = jv.as_object().at("candles").as_array(); - std::string current_coin_; + if (candle_list.size() == 0) { + ec = json::make_error_code(boost::json::error::size_mismatch); + return; + } - // It is more efficient to persist the json parser so that memory allocation does not need - // to be repeated each time we docode a message - json::parser parser_; - }; + // The coinbase API provides the most recent values first. + for (auto it = candle_list.crbegin(); it != candle_list.crend(); it++) { + core::string_view startstr = it->as_object().at("start").as_string(); + core::string_view openstr = it->as_object().at("open").as_string(); + core::string_view closestr = it->as_object().at("close").as_string(); + std::size_t str_size = 0; + std::time_t start_time = static_cast(std::stoll(startstr, &str_size)); + if (start_time > start_of_day) { + double open = std::stod(openstr, &str_size); - // The old non-executor version temporarily pasted below. -#if 0 -//using namespace boost; + if (receive_handler_) + receive_handler_(current_coin_, std::chrono::system_clock::from_time_t(start_time), open); -// Opens a websocket and subsscribes to price ticks - class historic_price_fetcher : public processor_base - { - std::function receive_handler_; - std::function error_handler_; - - boost::asio::strand strand_; - boost::asio::ip::tcp::resolver resolver_; - boost::asio::ssl::stream stream_; - - boost::beast::flat_buffer buffer_; - boost::beast::http::request req_; - boost::beast::http::response response_; - - std::string host_; - std::vector coins_; - - std::string current_coin_; - - std::time_t start_of_day_; - bool active_; - - // It is more efficient to persist the json parser so that memory allocation does not need - // to be repeated each time we docode a message - boost::json::parser parser_; - - public: - // Resolver and socket require an io_context - explicit - historic_price_fetcher( - boost::asio::io_context& ioc - , boost::asio::ssl::context& ctx - , const std::vector& coins - , std::function receive_handler - , std::function err_handler) - : receive_handler_(receive_handler) - , error_handler_(err_handler) - , strand_(boost::asio::make_strand(ioc)) - , resolver_(strand_) - , stream_(strand_, ctx) - , coins_(coins) - , start_of_day_(0) - , active_(false) - { + std::cout << "Decoded historic " << current_coin_ << " price: " << open << " at " << std::chrono::system_clock::from_time_t(start_time) << std::endl; + } + } } + catch (boost::system::system_error se) { + ec = se.code(); + } + catch (std::invalid_argument se) { + ec = json::make_error_code(boost::json::error::incomplete); + } + catch (std::out_of_range se) { + ec = json::make_error_code(boost::json::error::out_of_range); + } + } - // Start the asynchronous operation - void - run(); - - void - cancel() override; - - private: - void - on_resolve( - boost::system::error_code ec, - boost::asio::ip::tcp::resolver::results_type results); - - void - on_connect( - boost::system::error_code ec, - boost::asio::ip::tcp::resolver::results_type::endpoint_type ep); - - void - on_ssl_handshake(boost::system::error_code ec); + std::function receive_handler_; - void - next_request(); + resolver_type resolver_; + ssl_stream_type ssl_stream_; - void - on_write( - boost::system::error_code ec, - std::size_t bytes_transferred); + buffer_type buffer_; + request_type request_; + response_type response_; - void - on_read( - boost::system::error_code ec, - std::size_t bytes_transferred); + std::vector coins_; + std::string host_; + std::atomic_flag running_; - void parse_json( - boost::core::string_view str); + std::string current_coin_; - void - on_shutdown(boost::system::error_code ec); - }; -#endif + // It is more efficient to persist the json parser so that memory allocation does not need + // to be repeated each time we docode a message + json::parser parser_; +}; } // end namespace boost diff --git a/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp b/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp index eb4029ea3f..d913a28004 100644 --- a/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp +++ b/example/websocket/client/crypto-ai-ssl/live_price_listener.cpp @@ -30,6 +30,7 @@ #include #include +#if 0 using namespace boost; using namespace std::placeholders; @@ -334,8 +335,8 @@ void live_price_listener::on_read(system::error_code ec, std::size_t bytes_trans // unchanged, preventing the need for a reallocation each time a message is received. buffer_.clear(); - count++; - if (count > 20) cancel(); + testing_count++; + if (testing_count > 20) cancel(); // This is a very common idiom in async programming. As soon as a read completes, we // initiate another asynchronous read, almost like an infinite loop. @@ -416,3 +417,5 @@ void live_price_listener::on_close(system::error_code ec) // The make_printable() function helps print a ConstBufferSequence std::cout << "Final buffer content:" << beast::make_printable(buffer_.data()) << std::endl; } + +#endif diff --git a/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp b/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp index f3ce219998..e170865221 100644 --- a/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp +++ b/example/websocket/client/crypto-ai-ssl/live_price_listener.hpp @@ -16,15 +16,22 @@ #include #include #include +#include +#include #include +#include +#include +#include #include #include "processor_base.hpp" -//using namespace boost; +namespace boost +{ // Opens a websocket and subsscribes to price ticks +template class live_price_listener : public processor_base { // This holds the function called when a live price is received. @@ -34,22 +41,18 @@ class live_price_listener : public processor_base double)> receive_handler_; // This holds the function called when an error happens. - std::function error_handler_; + std::function error_handler_; - // We want to ensure that operations to set up the websocket are performed in order, - // and that we do not attempt to call async_read when another async_read is in progress. - // This strand needs to persist as long as the websocket is in use. - // Design note: we could "get away" without using a strand, because this class is structured - // so that the next async_* function is not called until *after* the previous asynchronous - // function is complete. However the strand is included since it makes our threading assumptions - // explicit for future developers. - boost::asio::strand strand_; + // Note that the websocket uses composed operations internally, as well as having internal + // timers. Thus this executor needs to be an (implicit or explicit) strand, which + // guarantees that no two completion handlers will be run simultaneously (from differnt threads). + Executor& exec_; // The resolver's role is to perform DNS lookups from a hostname to a set of ip addresses. - boost::asio::ip::tcp::resolver resolver_; + asio::ip::tcp::resolver resolver_; // The key structure in this listener is the websocket itself. - boost::beast::websocket::stream> ws_; + boost::beast::websocket::stream> ws_; // Any calls to async_read need to be passed a buffer into which the response will // be written. That buffer needs to persist until after the read completes and the @@ -75,26 +78,28 @@ class live_price_listener : public processor_base // to be repeated each time we docode a message boost::json::parser parser_; - int count = 0; + int testing_count = 0; public: // It is worth noting that we do not retain the reference to the passed-in io_context. // The reason for this is that we use a strand to ensure the explicit live_price_listener( - boost::asio::io_context& ioc - , boost::asio::ssl::context& ctx + Executor& exec + , asio::ssl::context& ctx + , string_view host , const std::vector& coins , std::function receive_handler - , std::function err_handler) + , std::function err_handler) : receive_handler_(receive_handler) , error_handler_(err_handler) - , strand_(boost::asio::make_strand(ioc)) - , resolver_(strand_) - , ws_(strand_, ctx) + , exec_(exec) + , resolver_(exec_) + , ws_(exec_, ctx) + , host_(host) , coins_(coins) , active_(false) { @@ -113,35 +118,422 @@ class live_price_listener : public processor_base private: void on_resolve( - boost::system::error_code ec, - boost::asio::ip::tcp::resolver::results_type results); + system::error_code ec, + asio::ip::tcp::resolver::results_type results); void on_connect( - boost::system::error_code ec, - boost::asio::ip::tcp::resolver::results_type::endpoint_type ep); + system::error_code ec, + asio::ip::tcp::resolver::results_type::endpoint_type ep); void - on_ssl_handshake(boost::system::error_code ec); + on_ssl_handshake(system::error_code ec); void - on_handshake(boost::system::error_code ec); + on_handshake(system::error_code ec); void on_write( - boost::system::error_code ec, + system::error_code ec, std::size_t bytes_transferred); void on_read( - boost::system::error_code ec, + system::error_code ec, std::size_t bytes_transferred); void parse_json( boost::core::string_view str); void - on_close(boost::system::error_code ec); + on_close(system::error_code ec); }; +// Start the asynchronous operation +template +void live_price_listener::run() +{ + + + // Set SNI Hostname (many hosts need this to handshake successfully) + // Note that ws_.next_layer() references the asio::ssl::stream object. + // Note, SSL_set_tlsext_host_name is an OpenSSL C function, not + // part of asio or beast. + if (!SSL_set_tlsext_host_name(ws_.next_layer().native_handle(), host_.c_str())) + { + system::error_code ec{ + static_cast(::ERR_get_error()), + asio::error::get_ssl_category() }; + return error_handler_(ec, "SNI"); + } + + // Set the expected hostname in the peer certificate for verification. + // OpenSSL will, whenever a certificate is received, call this function + // to check that the certificate's host matches what we think it should be. + ws_.next_layer().set_verify_callback(asio::ssl::host_name_verification(host_)); + + // Ensure any future callbacks do not early-exit. + // (design note: could also have been done at construction time). + active_ = true; + + // Request that ASIO lookup the domain name. For the sake of this example we have + // hard-coded the port to 443 (the usual port for this). + // For contrast with other examples, this has been written using a lambda, but + // `beast::bind_front_handler` is an equally viable alternative. + resolver_.async_resolve( + host_, + "https", + [this](system::error_code ec, asio::ip::tcp::resolver::results_type results) + { + // Note that `results` is actually an iterator into a container of + // endpoints representing all the IP addresses found by the DNS lookup. + on_resolve(ec, results); + } + ); +} + +template +void live_price_listener::cancel() +{ + // We set active_=false to rapidly consume all the pending + // completion handlers. + active_ = false; + + // We use asio::post to ensure the websocket closure takes place after + // all the currently pending completion handlers. + asio::post(exec_, [this]() + { + // If the websocket is still open, close it. + if (ws_.is_open()) + ws_.async_close(beast::websocket::close_code::normal, + [this](system::error_code ec) + { + on_close(ec); + } + ); + } + ); +} + +// This is the function called when hostname resolution completes. +template +void live_price_listener::on_resolve(system::error_code ec, asio::ip::tcp::resolver::results_type results) +{ + // In the event of an error call the `cancel` function which will drain + // any pending completion handlers. In this case the websocket is not yet + // open so the `cancel` function will not attempt to close it. + if (ec) { + cancel(); + return error_handler_(ec, "resolve"); + } + + // If we have been asked to shut down then do no processing. + if (!active_) + return; + + // Set the timeout for the operation. Note that this needs to be set + // each time to reset the countdown. + // This is applied on the underlying socket because neither the ssl + // layer nor the websocket layer have been started yet. + beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30)); + + // Make the connection on on of the IP address we got from the lookup. + // If multiple IP addresses were found then the first one to sucessfully + // connect is used. + // ws_ has 3 layers websocket->ssl->socket + // `get_lowest_layer` returns the bottom-most socket. + beast::get_lowest_layer(ws_).async_connect( + results, + [this](system::error_code ec, asio::ip::tcp::resolver::results_type::endpoint_type ep) + { + // Note that the endpoint `ep` represents the single IP to which + // we successfully connected (if any). + on_connect(ec, ep); + } + ); +} + +// Once the underlying socket is connected, this function performs the next step, +// namely getting the SSL layer running. +template +void live_price_listener::on_connect(system::error_code ec, asio::ip::tcp::resolver::results_type::endpoint_type ep) +{ + if (ec) { + // In the event of a connection error call the `cancel` function which will drain + // any pending completion handlers. In this case the websocket is not yet + // open so the `cancel` function will not attempt to close it. + cancel(); + return error_handler_(ec, "connect"); + } + + // If we have been asked to shut down then do no further processing. + if (!active_) + return; + + // Set the timeout for the operation. Note that this needs to be set + // each time to reset the countdown. + // This is applied on the underlying socket because neither the ssl + // layer nor the websocket layer have been started yet. + beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30)); + + // Update the host_ string to add the port. This will provide the value of the + // Host HTTP header during the WebSocket handshake. + // See https://tools.ietf.org/html/rfc7230#section-5.4 + host_ += ':' + std::to_string(ep.port()); + + // Perform the SSL handshake + // ws_ has 3 layers websocket->ssl->socket + // `get_next_layer` returns the ssl layer. + ws_.next_layer().async_handshake( + asio::ssl::stream_base::client, + [this](system::error_code ec) + { + on_ssl_handshake(ec); + } + ); +} + +template +void live_price_listener::on_ssl_handshake(system::error_code ec) +{ + if (ec) { + // In the event of an ssl error call the `cancel` function which will drain + // any pending completion handlers. In this case the websocket is not yet + // open so the `cancel` function will not attempt to close it. + cancel(); + return error_handler_(ec, "ssl_handshake"); + } + + // If we have been asked to shut down then do no further processing. + if (!active_) + return; + + // Turn off the timeout on the tcp_stream, because + // the websocket stream has its own timeout system. + beast::get_lowest_layer(ws_).expires_never(); + + // Set suggested timeout settings for the websocket + ws_.set_option( + beast::websocket::stream_base::timeout::suggested( + beast::role_type::client)); + + // We need to set the User-Agent of the handshake. Beast's websocket + // requires that this be done using a decorator. + ws_.set_option(beast::websocket::stream_base::decorator( + [](beast::websocket::request_type& req) + { + req.set(beast::http::field::user_agent, + std::string(BOOST_BEAST_VERSION_STRING) + + " websocket-client-async-ssl"); + }) + ); + + // The websocket should use deflate where possible, to reduce bandwidth + // by compressing the messages on the wire. + // This requires that zlib be included as a dependency. + beast::websocket::permessage_deflate opt; + opt.client_enable = true; // for clients + opt.server_enable = true; // for servers + ws_.set_option(opt); + + // Perform the websocket handshake + ws_.async_handshake(host_, "/", + [this](system::error_code ec) + { + on_handshake(ec); + } + ); +} + +// This is the function that is called when the websocket is up and usable. +// The previous steps were relatively generic across all websocket connections, +// and from this point on we need to include business logic. +template +void live_price_listener::on_handshake(system::error_code ec) +{ + if (ec) { + cancel(); + return error_handler_(ec, "handshake"); + } + + // If we have been asked to shut down then do no further processing. + if (!active_) + return; + + // Construct a coinbase json subscription message, using Boost::json + json::value jv = { + { "type", "subscribe" }, + { "product_ids", json::array(coins_.cbegin(), coins_.cend()) }, + { "channels", json::array{ + "heartbeat", + "ticker_batch" } + } + }; + + // Convert the json object into a string. + subscribe_json_str_ = serialize(jv); + + // Send the subscription message to the server. + ws_.async_write( + asio::buffer(subscribe_json_str_), + [this](system::error_code ec, std::size_t bytes_transferred) + { + on_write(ec, bytes_transferred); + } + ); +} + +template +void live_price_listener::on_write( + system::error_code ec + , std::size_t bytes_transferred) +{ + boost::ignore_unused(bytes_transferred); + + // Check for errors. + if (ec) { + cancel(); + return error_handler_(ec, "write"); + } + + // If we have been asked to shut down then do no further processing. + if (!active_) + return; + + // Read a message into our buffer. Note that buffer_ is a member variable as it has + // to persist for the life of the read and until the on_read completion handler is + // finished. + ws_.async_read( + buffer_, + [this](system::error_code ec, std::size_t bytes_transferred) + { + on_read(ec, bytes_transferred); + } + ); +} + +template +void live_price_listener::on_read(system::error_code ec, std::size_t bytes_transferred) +{ + boost::ignore_unused(bytes_transferred); + + // This indicates that the session was closed + if (ec == beast::websocket::error::closed) { + cancel(); + return; + } + else if (ec) { + cancel(); + if (active_) return error_handler_(ec, "read"); + } + + // If we have been asked to shut down then do no further processing. + if (!active_) + return; + + // The asynchronous read performs its own commit() on the dynamic buffer, thus the readable + // section of the dynamic buffer contains the message we want to decode. + asio::const_buffer buf(buffer_.cdata()); + + // We convert the const_buffer buf into a string_view, and then parse the json string itself. + // Note that an alternative would be to use beast::buffers_to_string but that would + // perform an additional allocation. + parse_json(core::string_view(static_cast(buf.data()), buf.size())); + + std::cout << "Interim: " << beast::make_printable(buffer_.data()) << "\n\n" << std::endl; + + //receive_handler_(beast::buffers_to_string(buffer_.data())); + + // Erase the const section of the dynamic buffer. + // Note: the clear() function does not deallocate so the capactity of the flat_buffer is + // unchanged, preventing the need for a reallocation each time a message is received. + buffer_.clear(); + + testing_count++; + if (testing_count > 20) cancel(); + + // This is a very common idiom in async programming. As soon as a read completes, we + // initiate another asynchronous read, almost like an infinite loop. + ws_.async_read( + buffer_, + [this](system::error_code ec, std::size_t bytes_transferred) + { + on_read(ec, bytes_transferred); + } + ); +} + + + +template +void live_price_listener::parse_json(core::string_view str) +{ + parser_.reset(); + + system::error_code ec; + parser_.write(str, ec); + + if (ec) + return error_handler_(ec, "json_price_decoder::parse_json"); + + json::value jv(parser_.release()); + + core::string_view productstr; + core::string_view timestr; + + double price = 0; + + // Design note: the json parsing could be done without using the exception + // interface, but the resulting code would be considerably more verbose. + try { + if (jv.as_object().at("type").as_string() != "ticker") + return; + + productstr = jv.as_object().at("product_id").as_string(); + + core::string_view pricestr = jv.as_object().at("price").as_string(); + + timestr = jv.as_object().at("time").as_string(); + + std::size_t str_size = 0; + price = std::stod(pricestr, &str_size); + } + catch (system::system_error se) { + return error_handler_(ec, "json_price_decoder::parse_json parse failure"); + } + catch (std::invalid_argument se) { + return error_handler_(ec, "json_price_decoder::parse_json parse failure"); + } + catch (std::out_of_range se) { + return error_handler_(ec, "json_price_decoder::parse_json parse failure"); + } + + // As timestr is a *UTC* string, we want to generate a chrono::system_clock::time_point + // representing the UTC time. + posix_time::ptime ptime = posix_time::from_iso_extended_string(timestr); + std::time_t epoch_time = posix_time::to_time_t(ptime); + const auto price_time = std::chrono::system_clock::from_time_t(epoch_time); + + if (receive_handler_) + receive_handler_(productstr, price_time, price); + + std::cout << "Decoded live " << productstr << " price: " << price << " at " << price_time << std::endl; + + //std::this_thread::sleep_for(std::chrono::milliseconds(10*1000)); +} + +template +void live_price_listener::on_close(system::error_code ec) +{ + if (ec && ec != asio::ssl::error::stream_truncated) + return error_handler_(ec, "close"); + + // If we get here then the connection is closed gracefully + + // The make_printable() function helps print a ConstBufferSequence + std::cout << "Final buffer content:" << beast::make_printable(buffer_.data()) << std::endl; +} + +} + #endif diff --git a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp index 0eff612c36..731bd5123c 100644 --- a/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp +++ b/example/websocket/client/crypto-ai-ssl/websocket_client_crypto_ai_ssl.cpp @@ -139,13 +139,16 @@ int main(int argc, char** argv) listen_ioc.run(); // Skip the live stuff until we get the changes to historic pricing working. - return EXIT_SUCCESS; + //return EXIT_SUCCESS; // Construct and start a the websocket listener. - live_price_listener listen_worker(listen_ioc, ssl_ctx, coins, live_input_recv, fail); + // For this example hard-code the host. + //host_ = "ws-feed-public.sandbox.exchange.coinbase.com"; + std::string ws_host = "ws-feed.exchange.coinbase.com"; + live_price_listener listen_worker(listen_ioc, ssl_ctx, ws_host, coins, live_input_recv, fail); listen_worker.run(); - // Restartr the event loop. The call will return when + // Restart the event loop. The run() call will return when // the socket is closed. listen_ioc.restart(); listen_ioc.run();