From 0077d733984f84a2a85e216ced29f73b173ae472 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 3 Mar 2024 14:21:02 +0100 Subject: [PATCH] doc(examples): add websocket client examples --- doc/async_websocket_client.rst | 9 ++ doc/examples.rst | 2 + doc/websocket_client.rst | 9 ++ examples/CMakeLists.txt | 4 + examples/async_websocket_client.cpp | 180 ++++++++++++++++++++++++++++ examples/websocket_client.cpp | 104 ++++++++++++++++ 6 files changed, 308 insertions(+) create mode 100644 doc/async_websocket_client.rst create mode 100644 doc/websocket_client.rst create mode 100644 examples/async_websocket_client.cpp create mode 100644 examples/websocket_client.cpp diff --git a/doc/async_websocket_client.rst b/doc/async_websocket_client.rst new file mode 100644 index 00000000..4e353d55 --- /dev/null +++ b/doc/async_websocket_client.rst @@ -0,0 +1,9 @@ +Asynchronous WebSocket Client +------------------------- +This example demonstrates a basic asynchronous WebSocket client using +`boost::beast`_. + +.. literalinclude:: ../examples/async_websocket_client.cpp + :lines: 7- + +.. _boost::beast: https://github.com/boostorg/beast diff --git a/doc/examples.rst b/doc/examples.rst index 230e818f..7cb5efcf 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -12,3 +12,5 @@ Examples async_https_client echo_client echo_server + websocket_client + async_websocket_client diff --git a/doc/websocket_client.rst b/doc/websocket_client.rst new file mode 100644 index 00000000..6ed592bc --- /dev/null +++ b/doc/websocket_client.rst @@ -0,0 +1,9 @@ +WebSocket Client +------------ +This example demonstrates a basic synchronous WebSocket client using +`boost::beast`_. + +.. literalinclude:: ../examples/websocket_client.cpp + :lines: 7- + +.. _boost::beast: https://github.com/boostorg/beast diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index e9d58482..c0fa9d08 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -21,6 +21,8 @@ if(NOT ENABLE_WINTLS_STANDALONE_ASIO) if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") # Temporary workaround issue https://github.com/boostorg/beast/issues/1582 target_compile_options(${name} PRIVATE "-wd4702") + # Object files get quite big when using async and beast + target_compile_options(${name} PRIVATE "/bigobj") endif() if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") @@ -37,4 +39,6 @@ if(NOT ENABLE_WINTLS_STANDALONE_ASIO) add_wintls_example(https_client) add_wintls_example(async_https_client) + add_wintls_example(websocket_client) + add_wintls_example(async_websocket_client) endif() diff --git a/examples/async_websocket_client.cpp b/examples/async_websocket_client.cpp new file mode 100644 index 00000000..53264791 --- /dev/null +++ b/examples/async_websocket_client.cpp @@ -0,0 +1,180 @@ +// +// Copyright (c) 2024 Kasper Laudrup (laudrup at stacktrace dot dk) +// +// 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) +// + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +namespace ssl = wintls; // from +using tcp = boost::asio::ip::tcp; // from + +//------------------------------------------------------------------------------ + +// Report a failure +void fail(beast::error_code ec, const char* what) { + std::cerr << what << ": " << ec.message() << "\n"; +} + +// Sends a WebSocket message and prints the response +class session : public std::enable_shared_from_this { + tcp::resolver resolver_; + websocket::stream> ws_; + beast::flat_buffer buffer_; + std::string host_; + std::string text_; + +public: + // Resolver and socket require an io_context + explicit session(net::io_context& ioc, ssl::context& ctx) + : resolver_(net::make_strand(ioc)) + , ws_(net::make_strand(ioc), ctx) { + } + + // Start the asynchronous operation + void run(const char* host, const char* port, const char* text) { + // Set SNI hostname (many hosts need this to handshake successfully) + ws_.next_layer().set_server_hostname(host); + + // Enable Check whether the Server Certificate was revoked + ws_.next_layer().set_certificate_revocation_check(true); + + // Save these for later + host_ = host; + text_ = text; + + // Look up the domain name + resolver_.async_resolve(host, port, beast::bind_front_handler(&session::on_resolve, shared_from_this())); + } + + void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { + if (ec) + return fail(ec, "resolve"); + + // Set a timeout on the operation + beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30)); + + // Make the connection on the IP address we get from a lookup + beast::get_lowest_layer(ws_).async_connect(results, + beast::bind_front_handler(&session::on_connect, shared_from_this())); + } + + void on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type ep) { + if (ec) + return fail(ec, "connect"); + + // Set a timeout on the operation + beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30)); + + // Update the host_ string. 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_.next_layer().async_handshake(wintls::handshake_type::client, + beast::bind_front_handler(&session::on_ssl_handshake, shared_from_this())); + } + + void on_ssl_handshake(beast::error_code ec) { + if (ec) + return fail(ec, "ssl_handshake"); + + // 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)); + + // Perform the websocket handshake + ws_.async_handshake(host_, "/", beast::bind_front_handler(&session::on_handshake, shared_from_this())); + } + + void on_handshake(beast::error_code ec) { + if (ec) + return fail(ec, "handshake"); + + // Send the message + ws_.async_write(net::buffer(text_), beast::bind_front_handler(&session::on_write, shared_from_this())); + } + + void on_write(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + + if (ec) + return fail(ec, "write"); + + // Read a message into our buffer + ws_.async_read(buffer_, beast::bind_front_handler(&session::on_read, shared_from_this())); + } + + void on_read(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + + if (ec) + return fail(ec, "read"); + + // Close the WebSocket connection + ws_.async_close(websocket::close_code::normal, beast::bind_front_handler(&session::on_close, shared_from_this())); + } + + void on_close(beast::error_code ec) { + if (ec) + return fail(ec, "close"); + + // If we get here then the connection is closed gracefully + + // The make_printable() function helps print a ConstBufferSequence + std::cout << beast::make_printable(buffer_.data()) << std::endl; + } +}; + +//------------------------------------------------------------------------------ + +int main(int argc, char** argv) { + // Check command line arguments. + if (argc != 4) { + std::cerr << "Usage: " << argv[0] << " \n\n" + << "Example: " << argv[0] << " echo.websocket.org 443 \"Hello, world!\"\n"; + return EXIT_FAILURE; + } + const auto host = argv[1]; + const auto port = argv[2]; + const auto text = argv[3]; + + // The io_context is required for all I/O + net::io_context ioc; + + // The SSL context is required, and holds certificates + ssl::context ctx{wintls::method::system_default}; + + // Use the operating systems default certificates for verification + ctx.use_default_certificates(true); + + // Verify the remote server's certificate + ctx.verify_server_certificate(true); + + // Launch the asynchronous operation + std::make_shared(ioc, ctx)->run(host, port, text); + + // Run the I/O service. The call will return when + // the socket is closed. + ioc.run(); + + return EXIT_SUCCESS; +} diff --git a/examples/websocket_client.cpp b/examples/websocket_client.cpp new file mode 100644 index 00000000..321ca998 --- /dev/null +++ b/examples/websocket_client.cpp @@ -0,0 +1,104 @@ +// +// Copyright (c) 2024 Kasper Laudrup (laudrup at stacktrace dot dk) +// +// 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) +// + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +namespace ssl = wintls; // from +using tcp = boost::asio::ip::tcp; // from + +//------------------------------------------------------------------------------ + +// Sends a WebSocket message and prints the response +int main(int argc, char** argv) { + try { + // Check command line arguments. + if (argc != 4) { + std::cerr << "Usage: " << argv[0] << " \n\n"; + std::cerr << "Example: " << argv[0] << " echo.websocket.org 443 \"Hello, world!\"\n"; + return EXIT_FAILURE; + } + std::string host = argv[1]; + const auto port = argv[2]; + const auto text = argv[3]; + + // The io_context is required for all I/O + net::io_context ioc; + + // The SSL context is required, and holds certificates + ssl::context ctx{wintls::method::system_default}; + + // Use the operating systems default certificates for verification + ctx.use_default_certificates(true); + + // Verify the remote server's certificate + ctx.verify_server_certificate(true); + + // Construct the TLS stream with the parameters from the context + // These objects perform our I/O + tcp::resolver resolver{ioc}; + websocket::stream> ws{ioc, ctx}; + + // Set SNI hostname (many hosts need this to handshake successfully) + ws.next_layer().set_server_hostname(host); + + // Enable Check whether the Server Certificate was revoked + ws.next_layer().set_certificate_revocation_check(true); + + // Look up the domain name + const auto results = resolver.resolve(host, port); + + // Make the connection on the IP address we get from a lookup + auto ep = net::connect(beast::get_lowest_layer(ws), results); + + // Set SNI Hostname (many hosts need this to handshake successfully) + ws.next_layer().set_server_hostname(host.c_str()); + + // Update the host_ string. 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.next_layer().handshake(wintls::handshake_type::client); + + // Perform the websocket handshake + ws.handshake(host, "/"); + + // Send the message + ws.write(net::buffer(std::string(text))); + + // This buffer will hold the incoming message + beast::flat_buffer buffer; + + // Read a message into our buffer + ws.read(buffer); + + // Close the WebSocket connection + ws.close(websocket::close_code::normal); + + // If we get here then the connection is closed gracefully + + std::cout << beast::make_printable(buffer.data()) << std::endl; + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} \ No newline at end of file