diff --git a/doc/qbk/02_examples/_examples.qbk b/doc/qbk/02_examples/_examples.qbk index 0a842f31..9463db5c 100644 --- a/doc/qbk/02_examples/_examples.qbk +++ b/doc/qbk/02_examples/_examples.qbk @@ -32,6 +32,10 @@ used to evaluate robustness. All asynchronous clients support timeouts. [HTTP, asynchronous] [[path_link example/http/client/async/http_client_async.cpp http_client_async.cpp]] [[path_link example/http/client/async-ssl/http_client_async_ssl.cpp http_client_async_ssl.cpp]] +][ + [HTTP, asynchronous Unix domain sockets] + [[path_link example/http/client/async-local/http_client_async_local.cpp http_client_async_local.cpp]] + [] ][ [HTTP, asynchronous using __system_executor__] [] @@ -50,10 +54,10 @@ used to evaluate robustness. All asynchronous clients support timeouts. [] ][ [HTTP json_body (synchronous)] - [[path_link example/http/client/body/json_body.hpp example/http/client/body/json_client.cpp]] + [[path_link example/http/client/body/json_client.hpp json_client.cpp]] ][ [HTTP client for all methods (synchronous)] - [[path_link example/http/client/methods/http_client_methods.cpp example/http/client/methods/http_client_methods.cpp]] + [[path_link example/http/client/methods/http_client_methods.cpp http_client_methods.cpp]] ]] These WebSocket clients connect to a @@ -70,6 +74,10 @@ before disconnecting. All asynchronous clients support timeouts. [WebSocket, asynchronous] [[path_link example/websocket/client/async/websocket_client_async.cpp websocket_client_async.cpp]] [[path_link example/websocket/client/async-ssl/websocket_client_async_ssl.cpp websocket_client_async_ssl.cpp]] +][ + [WebSocket, asynchronous Unix domain sockets] + [[path_link example/websocket/client/async-local/websocket_client_async_local.cpp websocket_client_async_local.cpp]] + [] ][ [WebSocket, asynchronous using __system_executor__] [] @@ -103,6 +111,10 @@ command line. All asynchronous servers support timeouts. [HTTP, asynchronous] [[path_link example/http/server/async/http_server_async.cpp http_server_async.cpp]] [[path_link example/http/server/async-ssl/http_server_async_ssl.cpp http_server_async_ssl.cpp]] +][ + [HTTP, asynchronous Unix domain sockets] + [[path_link example/http/server/async-local/http_server_async_local.cpp http_server_async_local.cpp]] + [] ][ [HTTP, coroutine] [[path_link example/http/server/coro/http_server_coro.cpp http_server_coro.cpp]] @@ -142,6 +154,10 @@ support timeouts. [WebSocket, asynchronous] [[path_link example/websocket/server/async/websocket_server_async.cpp websocket_server_async.cpp]] [[path_link example/websocket/server/async-ssl/websocket_server_async_ssl.cpp websocket_server_async_ssl.cpp]] +][ + [WebSocket, asynchronous Unix domain sockets] + [[path_link example/websocket/server/async-local/websocket_server_async_local.cpp websocket_server_async_local.cpp]] + [] ][ [WebSocket, coroutine] [[path_link example/websocket/server/coro/websocket_server_coro.cpp websocket_server_coro.cpp]] diff --git a/example/http/client/CMakeLists.txt b/example/http/client/CMakeLists.txt index ab8fa187..e55b9a2c 100644 --- a/example/http/client/CMakeLists.txt +++ b/example/http/client/CMakeLists.txt @@ -8,6 +8,7 @@ # add_subdirectory(async) +add_subdirectory(async-local) add_subdirectory(awaitable) add_subdirectory(body) add_subdirectory(coro) diff --git a/example/http/client/Jamfile b/example/http/client/Jamfile index bfe3d7f3..a6aa3598 100644 --- a/example/http/client/Jamfile +++ b/example/http/client/Jamfile @@ -8,6 +8,7 @@ # build-project async ; +build-project async-local ; build-project coro ; build-project crawl ; build-project sync ; diff --git a/example/http/client/async-local/CMakeLists.txt b/example/http/client/async-local/CMakeLists.txt new file mode 100644 index 00000000..7f8758fc --- /dev/null +++ b/example/http/client/async-local/CMakeLists.txt @@ -0,0 +1,21 @@ +# +# Copyright (c) 2025 Mohammad Nejati +# +# 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(http-client-async-local + Jamfile + http_client_async_local.cpp) + +source_group("" FILES + Jamfile + http_client_async_local.cpp) + +target_link_libraries(http-client-async-local PRIVATE Boost::beast) + +set_target_properties(http-client-async-local + PROPERTIES FOLDER "example-http-client") diff --git a/example/http/client/async-local/Jamfile b/example/http/client/async-local/Jamfile new file mode 100644 index 00000000..272d857f --- /dev/null +++ b/example/http/client/async-local/Jamfile @@ -0,0 +1,15 @@ +# +# Copyright (c) 2025 Mohammad Nejati +# +# 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 +# + +exe http-client-async-local : + http_client_async_local.cpp + : + coverage:no + ubasan:no + ; diff --git a/example/http/client/async-local/http_client_async_local.cpp b/example/http/client/async-local/http_client_async_local.cpp new file mode 100644 index 00000000..39e40274 --- /dev/null +++ b/example/http/client/async-local/http_client_async_local.cpp @@ -0,0 +1,187 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Mohammad Nejati +// +// 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 +// + +//------------------------------------------------------------------------------ +// +// Example: HTTP client, asynchronous Unix domain sockets +// +//------------------------------------------------------------------------------ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS) + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using stream_protocol = net::local::stream_protocol; // from + +//------------------------------------------------------------------------------ + +// Report a failure +void +fail(beast::error_code ec, char const* what) +{ + std::cerr << what << ": " << ec.message() << "\n"; +} + +// Performs an HTTP GET and prints the response +class session : public std::enable_shared_from_this +{ + beast::basic_stream< + stream_protocol, + net::any_io_executor, + beast::unlimited_rate_policy> stream_; + beast::flat_buffer buffer_; // (Must persist between reads) + http::request req_; + http::response res_; + +public: + // Objects are constructed with a strand to + // ensure that handlers do not execute concurrently. + explicit + session(net::io_context& ioc) + : stream_(ioc) + { + } + + // Start the asynchronous operation + void + run( + char const* path, + char const* host, + char const* port, + char const* target, + int version) + { + // Set up an HTTP GET request message + req_.version(version); + req_.method(http::verb::get); + req_.target(target); + req_.set(http::field::host, std::string{host} + ":" + port); + req_.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + + // Make the connection on the IP address we get from a lookup + stream_.async_connect( + stream_protocol::endpoint{path}, + beast::bind_front_handler( + &session::on_connect, + shared_from_this())); + } + + void + on_connect(beast::error_code ec) + { + if(ec) + return fail(ec, "connect"); + + // Set a timeout on the operation + stream_.expires_after(std::chrono::seconds(30)); + + // Send the HTTP request to the remote host + http::async_write(stream_, req_, + 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"); + + // Receive the HTTP response + http::async_read(stream_, buffer_, res_, + 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"); + + // Write the message to standard out + std::cout << res_ << std::endl; + + // Gracefully close the socket + stream_.socket().shutdown(stream_protocol::socket::shutdown_both, ec); + + // not_connected happens sometimes so don't bother reporting it. + if(ec && ec != beast::errc::not_connected) + return fail(ec, "shutdown"); + + // If we get here then the connection is closed gracefully + } +}; + +//------------------------------------------------------------------------------ + +int main(int argc, char** argv) +{ + // Check command line arguments. + if(argc != 5 && argc != 6) + { + std::cerr << + "Usage: http-client-async-local []\n" << + "Example:\n" << + " http-client-async-local /tmp/http.sock localhost 80 /\n" << + " http-client-async-local /tmp/http.sock localhost 80 / 1.0\n"; + return EXIT_FAILURE; + } + auto const path = argv[1]; + auto const host = argv[2]; + auto const port = argv[3]; + auto const target = argv[4]; + int version = argc == 6 && !std::strcmp("1.0", argv[5]) ? 10 : 11; + + // The io_context is required for all I/O + net::io_context ioc; + + // Launch the asynchronous operation + std::make_shared(ioc)->run(path, host, port, target, version); + + // Run the I/O service. The call will return when + // the get operation is complete. + ioc.run(); + + return EXIT_SUCCESS; +} + +#else + +int +main(int, char*[]) +{ +std::cerr << + "Local sockets not available on this platform" << std::endl; +return EXIT_FAILURE; +} + +#endif diff --git a/example/http/server/CMakeLists.txt b/example/http/server/CMakeLists.txt index 90eceb11..79e80b8d 100644 --- a/example/http/server/CMakeLists.txt +++ b/example/http/server/CMakeLists.txt @@ -8,6 +8,7 @@ # add_subdirectory(async) +add_subdirectory(async-local) add_subdirectory(awaitable) add_subdirectory(coro) add_subdirectory(fast) diff --git a/example/http/server/Jamfile b/example/http/server/Jamfile index f0119e45..8a2df2bd 100644 --- a/example/http/server/Jamfile +++ b/example/http/server/Jamfile @@ -8,6 +8,7 @@ # build-project async ; +build-project async-local ; build-project coro ; build-project fast ; build-project small ; diff --git a/example/http/server/async-local/CMakeLists.txt b/example/http/server/async-local/CMakeLists.txt new file mode 100644 index 00000000..be555d84 --- /dev/null +++ b/example/http/server/async-local/CMakeLists.txt @@ -0,0 +1,21 @@ +# +# Copyright (c) 2025 Mohammad Nejati +# +# 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(http-server-async-local + Jamfile + http_server_async_local.cpp) + +source_group("" FILES + Jamfile + http_server_async_local.cpp) + +target_link_libraries(http-server-async-local PRIVATE Boost::beast) + +set_target_properties(http-server-async-local + PROPERTIES FOLDER "example-http-server") diff --git a/example/http/server/async-local/Jamfile b/example/http/server/async-local/Jamfile new file mode 100644 index 00000000..e3a8fee4 --- /dev/null +++ b/example/http/server/async-local/Jamfile @@ -0,0 +1,15 @@ +# +# Copyright (c) 2025 Mohammad Nejati +# +# 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 +# + +exe http-server-async-local : + http_server_async_local.cpp + : + coverage:no + ubasan:no + ; diff --git a/example/http/server/async-local/http_server_async_local.cpp b/example/http/server/async-local/http_server_async_local.cpp new file mode 100644 index 00000000..b958b89f --- /dev/null +++ b/example/http/server/async-local/http_server_async_local.cpp @@ -0,0 +1,454 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Mohammad Nejati +// +// 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 +// + +//------------------------------------------------------------------------------ +// +// Example: HTTP server, asynchronous Unix domain sockets +// +//------------------------------------------------------------------------------ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS) + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using stream_protocol = net::local::stream_protocol; // from + +// Return a reasonable mime type based on the extension of a file. +beast::string_view +mime_type(beast::string_view path) +{ + using beast::iequals; + auto const ext = [&path] + { + auto const pos = path.rfind("."); + if(pos == beast::string_view::npos) + return beast::string_view{}; + return path.substr(pos); + }(); + if(iequals(ext, ".htm")) return "text/html"; + if(iequals(ext, ".html")) return "text/html"; + if(iequals(ext, ".php")) return "text/html"; + if(iequals(ext, ".css")) return "text/css"; + if(iequals(ext, ".txt")) return "text/plain"; + if(iequals(ext, ".js")) return "application/javascript"; + if(iequals(ext, ".json")) return "application/json"; + if(iequals(ext, ".xml")) return "application/xml"; + if(iequals(ext, ".swf")) return "application/x-shockwave-flash"; + if(iequals(ext, ".flv")) return "video/x-flv"; + if(iequals(ext, ".png")) return "image/png"; + if(iequals(ext, ".jpe")) return "image/jpeg"; + if(iequals(ext, ".jpeg")) return "image/jpeg"; + if(iequals(ext, ".jpg")) return "image/jpeg"; + if(iequals(ext, ".gif")) return "image/gif"; + if(iequals(ext, ".bmp")) return "image/bmp"; + if(iequals(ext, ".ico")) return "image/vnd.microsoft.icon"; + if(iequals(ext, ".tiff")) return "image/tiff"; + if(iequals(ext, ".tif")) return "image/tiff"; + if(iequals(ext, ".svg")) return "image/svg+xml"; + if(iequals(ext, ".svgz")) return "image/svg+xml"; + return "application/text"; +} + +// Append an HTTP rel-path to a local filesystem path. +// The returned path is normalized for the platform. +std::string +path_cat( + beast::string_view base, + beast::string_view path) +{ + if(base.empty()) + return std::string(path); + std::string result(base); +#ifdef BOOST_MSVC + char constexpr path_separator = '\\'; + if(result.back() == path_separator) + result.resize(result.size() - 1); + result.append(path.data(), path.size()); + for(auto& c : result) + if(c == '/') + c = path_separator; +#else + char constexpr path_separator = '/'; + if(result.back() == path_separator) + result.resize(result.size() - 1); + result.append(path.data(), path.size()); +#endif + return result; +} + +// Return a response for the given request. +// +// The concrete type of the response message (which depends on the +// request), is type-erased in message_generator. +template +http::message_generator +handle_request( + beast::string_view doc_root, + http::request>&& req) +{ + // Returns a bad request response + auto const bad_request = + [&req](beast::string_view why) + { + http::response res{http::status::bad_request, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = std::string(why); + res.prepare_payload(); + return res; + }; + + // Returns a not found response + auto const not_found = + [&req](beast::string_view target) + { + http::response res{http::status::not_found, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "The resource '" + std::string(target) + "' was not found."; + res.prepare_payload(); + return res; + }; + + // Returns a server error response + auto const server_error = + [&req](beast::string_view what) + { + http::response res{http::status::internal_server_error, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "An error occurred: '" + std::string(what) + "'"; + res.prepare_payload(); + return res; + }; + + // Make sure we can handle the method + if( req.method() != http::verb::get && + req.method() != http::verb::head) + return bad_request("Unknown HTTP-method"); + + // Request path must be absolute and not contain "..". + if( req.target().empty() || + req.target()[0] != '/' || + req.target().find("..") != beast::string_view::npos) + return bad_request("Illegal request-target"); + + // Build the path to the requested file + std::string path = path_cat(doc_root, req.target()); + if(req.target().back() == '/') + path.append("index.html"); + + // Attempt to open the file + beast::error_code ec; + http::file_body::value_type body; + body.open(path.c_str(), beast::file_mode::scan, ec); + + // Handle the case where the file doesn't exist + if(ec == beast::errc::no_such_file_or_directory) + return not_found(req.target()); + + // Handle an unknown error + if(ec) + return server_error(ec.message()); + + // Cache the size since we need it after the move + auto const size = body.size(); + + // Respond to HEAD request + if(req.method() == http::verb::head) + { + http::response res{http::status::ok, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, mime_type(path)); + res.content_length(size); + res.keep_alive(req.keep_alive()); + return res; + } + + // Respond to GET request + http::response res{ + std::piecewise_construct, + std::make_tuple(std::move(body)), + std::make_tuple(http::status::ok, req.version())}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, mime_type(path)); + res.content_length(size); + res.keep_alive(req.keep_alive()); + return res; +} + +//------------------------------------------------------------------------------ + +// Report a failure +void +fail(beast::error_code ec, char const* what) +{ + std::cerr << what << ": " << ec.message() << "\n"; +} + +// Handles an HTTP server connection +class session : public std::enable_shared_from_this +{ + beast::basic_stream< + stream_protocol, + net::any_io_executor, + beast::unlimited_rate_policy> stream_; + beast::flat_buffer buffer_; + std::shared_ptr doc_root_; + http::request req_; + +public: + // Take ownership of the stream + session( + stream_protocol::socket&& socket, + std::shared_ptr const& doc_root) + : stream_(std::move(socket)) + , doc_root_(doc_root) + { + } + + // Start the asynchronous operation + void + run() + { + // We need to be executing within a strand to perform async operations + // on the I/O objects in this session. Although not strictly necessary + // for single-threaded contexts, this example code is written to be + // thread-safe by default. + net::dispatch(stream_.get_executor(), + beast::bind_front_handler( + &session::do_read, + shared_from_this())); + } + + void + do_read() + { + // Make the request empty before reading, + // otherwise the operation behavior is undefined. + req_ = {}; + + // Set the timeout. + stream_.expires_after(std::chrono::seconds(30)); + + // Read a request + http::async_read(stream_, buffer_, req_, + 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); + + // This means they closed the connection + if(ec == http::error::end_of_stream) + return do_close(); + + if(ec) + return fail(ec, "read"); + + // Send the response + send_response( + handle_request(*doc_root_, std::move(req_))); + } + + void + send_response(http::message_generator&& msg) + { + bool keep_alive = msg.keep_alive(); + + // Write the response + beast::async_write( + stream_, + std::move(msg), + beast::bind_front_handler( + &session::on_write, shared_from_this(), keep_alive)); + } + + void + on_write( + bool keep_alive, + beast::error_code ec, + std::size_t bytes_transferred) + { + boost::ignore_unused(bytes_transferred); + + if(ec) + return fail(ec, "write"); + + if(! keep_alive) + { + // This means we should close the connection, usually because + // the response indicated the "Connection: close" semantic. + return do_close(); + } + + // Read another request + do_read(); + } + + void + do_close() + { + // Send a TCP shutdown + beast::error_code ec; + stream_.socket().shutdown(stream_protocol::socket::shutdown_send, ec); + + // At this point the connection is closed gracefully + } +}; + +//------------------------------------------------------------------------------ + +// Accepts incoming connections and launches the sessions +class listener : public std::enable_shared_from_this +{ + net::io_context& ioc_; + stream_protocol::acceptor acceptor_; + std::shared_ptr doc_root_; + +public: + listener( + net::io_context& ioc, + stream_protocol::endpoint endpoint, + std::shared_ptr const& doc_root) + : ioc_(ioc) + , acceptor_(net::make_strand(ioc), std::move(endpoint)) + , doc_root_(doc_root) + { + } + + // Start accepting incoming connections + void + run() + { + do_accept(); + } + +private: + void + do_accept() + { + // The new connection gets its own strand + acceptor_.async_accept( + net::make_strand(ioc_), + beast::bind_front_handler( + &listener::on_accept, + shared_from_this())); + } + + void + on_accept(beast::error_code ec, stream_protocol::socket socket) + { + if(ec) + { + fail(ec, "accept"); + return; // To avoid infinite loop + } + else + { + // Create the session and run it + std::make_shared( + std::move(socket), + doc_root_)->run(); + } + + // Accept another connection + do_accept(); + } +}; + +//------------------------------------------------------------------------------ + +int main(int argc, char* argv[]) +{ + try + { + // Check command line arguments. + if (argc != 4) + { + std::cerr << + "Usage: http-server-async-local \n" << + "Example:\n" << + " http-server-async-local /tmp/http.sock . 1\n"; + return EXIT_FAILURE; + } + auto const path = argv[1]; + auto const doc_root = std::make_shared(argv[2]); + auto const threads = std::max(1, std::atoi(argv[3])); + + // The io_context is required for all I/O + net::io_context ioc{threads}; + + // Remove previous binding + std::remove(path); + + // Create and launch a listening port + std::make_shared( + ioc, + stream_protocol::endpoint{path}, + doc_root)->run(); + + // Run the I/O service on the requested number of threads + std::vector v; + v.reserve(threads - 1); + for(auto i = threads - 1; i > 0; --i) + v.emplace_back( + [&ioc] + { + ioc.run(); + }); + ioc.run(); + + return EXIT_SUCCESS; + } + catch(std::exception const& e) + { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } +} + +#else + +int +main(int, char*[]) +{ +std::cerr << + "Local sockets not available on this platform" << std::endl; +return EXIT_FAILURE; +} + +#endif diff --git a/example/websocket/client/CMakeLists.txt b/example/websocket/client/CMakeLists.txt index e925af83..e399c3da 100644 --- a/example/websocket/client/CMakeLists.txt +++ b/example/websocket/client/CMakeLists.txt @@ -8,6 +8,7 @@ # add_subdirectory(async) +add_subdirectory(async-local) add_subdirectory(awaitable) add_subdirectory(coro) add_subdirectory(sync) diff --git a/example/websocket/client/Jamfile b/example/websocket/client/Jamfile index 753b90d0..bd440218 100644 --- a/example/websocket/client/Jamfile +++ b/example/websocket/client/Jamfile @@ -8,6 +8,7 @@ # build-project async ; +build-project async-local ; build-project awaitable ; build-project coro ; build-project sync ; diff --git a/example/websocket/client/async-local/CMakeLists.txt b/example/websocket/client/async-local/CMakeLists.txt new file mode 100644 index 00000000..d028cd71 --- /dev/null +++ b/example/websocket/client/async-local/CMakeLists.txt @@ -0,0 +1,21 @@ +# +# Copyright (c) 2024 Mohammad Nejati +# +# 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-async-local + Jamfile + websocket_client_async_local.cpp) + +source_group("" FILES + Jamfile + websocket_client_async_local.cpp) + +target_link_libraries(websocket-client-async-local PRIVATE Boost::beast) + +set_target_properties(websocket-client-async-local + PROPERTIES FOLDER "example-websocket-client") diff --git a/example/websocket/client/async-local/Jamfile b/example/websocket/client/async-local/Jamfile new file mode 100644 index 00000000..e086952d --- /dev/null +++ b/example/websocket/client/async-local/Jamfile @@ -0,0 +1,15 @@ +# +# Copyright (c) 2025 Mohammad Nejati +# +# 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 +# + +exe websocket-client-async-local : + websocket_client_async_local.cpp + : + coverage:no + ubasan:no + ; diff --git a/example/websocket/client/async-local/websocket_client_async_local.cpp b/example/websocket/client/async-local/websocket_client_async_local.cpp new file mode 100644 index 00000000..0cfdf2fa --- /dev/null +++ b/example/websocket/client/async-local/websocket_client_async_local.cpp @@ -0,0 +1,210 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Mohammad Nejati +// +// 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 +// + +//------------------------------------------------------------------------------ +// +// Example: WebSocket client, asynchronous Unix domain sockets +// +//------------------------------------------------------------------------------ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS) + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using stream_protocol = net::local::stream_protocol; // from + +//------------------------------------------------------------------------------ + +// Report a failure +void +fail(beast::error_code ec, char const* what) +{ + std::cerr << what << ": " << ec.message() << "\n"; +} + +// Sends a WebSocket message and prints the response +class session : public std::enable_shared_from_this +{ + 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) + : ws_(net::make_strand(ioc)) + { + } + + // Start the asynchronous operation + void + run( + char const* path, + char const* host, + char const* port, + char const* text) + { + // Save for later + host_ = std::string{host} + ":" + port; + text_ = text; + + // Make the connection + beast::get_lowest_layer(ws_).async_connect( + stream_protocol::endpoint{path}, + beast::bind_front_handler( + &session::on_connect, + shared_from_this())); + } + + void + on_connect(beast::error_code ec) + { + if(ec) + return fail(ec, "connect"); + + // Set suggested timeout settings for the websocket + ws_.set_option( + websocket::stream_base::timeout::suggested( + beast::role_type::client)); + + // Set a decorator to change the User-Agent of the handshake + 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-local"); + })); + + // 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 != 5) + { + std::cerr << + "Usage: websocket-client-async-local \n" << + "Example:\n" << + " websocket-client-async-local /tmp/ws.sock localhost 80 \"Hello, world!\"\n"; + return EXIT_FAILURE; + } + auto const path = argv[1]; + auto const host = argv[2]; + auto const port = argv[3]; + auto const text = argv[4]; + + // The io_context is required for all I/O + net::io_context ioc; + + // Launch the asynchronous operation + std::make_shared(ioc)->run(path, host, port, text); + + // Run the I/O service. The call will return when + // the socket is closed. + ioc.run(); + + return EXIT_SUCCESS; +} + +#else + +int +main(int, char*[]) +{ +std::cerr << + "Local sockets not available on this platform" << std::endl; +return EXIT_FAILURE; +} + +#endif diff --git a/example/websocket/server/CMakeLists.txt b/example/websocket/server/CMakeLists.txt index b25017a9..a4ecc7e1 100644 --- a/example/websocket/server/CMakeLists.txt +++ b/example/websocket/server/CMakeLists.txt @@ -8,6 +8,7 @@ # add_subdirectory(async) +add_subdirectory(async-local) add_subdirectory(awaitable) add_subdirectory(chat-multi) add_subdirectory(coro) diff --git a/example/websocket/server/Jamfile b/example/websocket/server/Jamfile index 07a02b50..c1f80c6e 100644 --- a/example/websocket/server/Jamfile +++ b/example/websocket/server/Jamfile @@ -8,6 +8,7 @@ # build-project async ; +build-project async-local ; build-project awaitable ; build-project chat-multi ; build-project coro ; diff --git a/example/websocket/server/async-local/CMakeLists.txt b/example/websocket/server/async-local/CMakeLists.txt new file mode 100644 index 00000000..de64253f --- /dev/null +++ b/example/websocket/server/async-local/CMakeLists.txt @@ -0,0 +1,21 @@ +# +# Copyright (c) 2025 Mohammad Nejati +# +# 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-server-async-local + Jamfile + websocket_server_async_local.cpp) + +source_group("" FILES + Jamfile + websocket_server_async_local.cpp) + +target_link_libraries(websocket-server-async-local PRIVATE Boost::beast) + +set_target_properties(websocket-server-async-local + PROPERTIES FOLDER "example-websocket-server") diff --git a/example/websocket/server/async-local/Jamfile b/example/websocket/server/async-local/Jamfile new file mode 100644 index 00000000..3ac73a42 --- /dev/null +++ b/example/websocket/server/async-local/Jamfile @@ -0,0 +1,15 @@ +# +# Copyright (c) 2025 Mohammad Nejati +# +# 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 +# + +exe websocket-server-async-local : + websocket_server_async_local.cpp + : + coverage:no + ubasan:no + ; diff --git a/example/websocket/server/async-local/websocket_server_async_local.cpp b/example/websocket/server/async-local/websocket_server_async_local.cpp new file mode 100644 index 00000000..95dbc9f3 --- /dev/null +++ b/example/websocket/server/async-local/websocket_server_async_local.cpp @@ -0,0 +1,274 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// Copyright (c) 2025 Mohammad Nejati +// +// 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 +// + +//------------------------------------------------------------------------------ +// +// Example: WebSocket server, asynchronous Unix domain sockets +// +//------------------------------------------------------------------------------ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS) + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using stream_protocol = net::local::stream_protocol; // from + +//------------------------------------------------------------------------------ + +// Report a failure +void +fail(beast::error_code ec, char const* what) +{ + std::cerr << what << ": " << ec.message() << "\n"; +} + +// Echoes back all received WebSocket messages +class session : public std::enable_shared_from_this +{ + websocket::stream ws_; + beast::flat_buffer buffer_; + +public: + // Take ownership of the socket + explicit + session(stream_protocol::socket&& socket) + : ws_(std::move(socket)) + { + } + + // Get on the correct executor + void + run() + { + // We need to be executing within a strand to perform async operations + // on the I/O objects in this session. Although not strictly necessary + // for single-threaded contexts, this example code is written to be + // thread-safe by default. + net::dispatch(ws_.get_executor(), + beast::bind_front_handler( + &session::on_run, + shared_from_this())); + } + + // Start the asynchronous operation + void + on_run() + { + // Set suggested timeout settings for the websocket + ws_.set_option( + websocket::stream_base::timeout::suggested( + beast::role_type::server)); + + // Set a decorator to change the Server of the handshake + ws_.set_option(websocket::stream_base::decorator( + [](websocket::response_type& res) + { + res.set(http::field::server, + std::string(BOOST_BEAST_VERSION_STRING) + + " websocket-server-async-local"); + })); + // Accept the websocket handshake + ws_.async_accept( + beast::bind_front_handler( + &session::on_accept, + shared_from_this())); + } + + void + on_accept(beast::error_code ec) + { + if(ec) + return fail(ec, "accept"); + + // Read a message + do_read(); + } + + void + do_read() + { + // 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); + + // This indicates that the session was closed + if(ec == websocket::error::closed) + return; + + if(ec) + return fail(ec, "read"); + + // Echo the message + ws_.text(ws_.got_text()); + ws_.async_write( + buffer_.data(), + 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"); + + // Clear the buffer + buffer_.consume(buffer_.size()); + + // Do another read + do_read(); + } +}; + +//------------------------------------------------------------------------------ + +// Accepts incoming connections and launches the sessions +class listener : public std::enable_shared_from_this +{ + net::io_context& ioc_; + stream_protocol::acceptor acceptor_; + +public: + listener( + net::io_context& ioc, + stream_protocol::endpoint endpoint) + : ioc_(ioc) + , acceptor_(ioc, std::move(endpoint)) + { + } + + // Start accepting incoming connections + void + run() + { + do_accept(); + } + +private: + void + do_accept() + { + // The new connection gets its own strand + acceptor_.async_accept( + net::make_strand(ioc_), + beast::bind_front_handler( + &listener::on_accept, + shared_from_this())); + } + + void + on_accept(beast::error_code ec, stream_protocol::socket socket) + { + if(ec) + { + fail(ec, "accept"); + return; + } + else + { + // Create the session and run it + std::make_shared(std::move(socket))->run(); + } + + // Accept another connection + do_accept(); + } +}; + +//------------------------------------------------------------------------------ + +int main(int argc, char* argv[]) +{ + try + { + // Check command line arguments. + if (argc != 3) + { + std::cerr << + "Usage: websocket-server-async-local \n" << + "Example:\n" << + " websocket-server-async-local /tmp/ws.sock 1\n"; + return EXIT_FAILURE; + } + auto const path = argv[1]; + auto const threads = std::max(1, std::atoi(argv[2])); + + // The io_context is required for all I/O + net::io_context ioc{threads}; + + // Remove previous binding + std::remove(path); + + // Create and launch a listening port + std::make_shared(ioc, stream_protocol::endpoint{path})->run(); + + // Run the I/O service on the requested number of threads + std::vector v; + v.reserve(threads - 1); + for(auto i = threads - 1; i > 0; --i) + v.emplace_back( + [&ioc] + { + ioc.run(); + }); + ioc.run(); + + return EXIT_SUCCESS; + } + catch(std::exception const& e) + { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } +} + +#else + +int +main(int, char*[]) +{ + std::cerr << + "Local sockets not available on this platform" << std::endl; + return EXIT_FAILURE; +} + +#endif