From 3081f52ad505d645dfefec0a8b28e22b28ff0259 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Mon, 21 Jan 2019 10:18:45 -0800 Subject: [PATCH] Add websocket-chat-multi example: This is the multi-threaded io_context version of the CppCon2018 WebSocket chat server and client example. --- CHANGELOG.md | 1 + doc/qbk/02_examples.qbk | 11 +- example/websocket/server/CMakeLists.txt | 1 + example/websocket/server/Jamfile | 1 + .../server/chat-multi/CMakeLists.txt | 37 ++ example/websocket/server/chat-multi/Jamfile | 19 + example/websocket/server/chat-multi/beast.hpp | 19 + .../server/chat-multi/chat_client.html | 57 +++ .../server/chat-multi/http_session.cpp | 354 ++++++++++++++++++ .../server/chat-multi/http_session.hpp | 57 +++ .../websocket/server/chat-multi/listener.cpp | 103 +++++ .../websocket/server/chat-multi/listener.hpp | 42 +++ example/websocket/server/chat-multi/main.cpp | 84 +++++ example/websocket/server/chat-multi/net.hpp | 18 + .../server/chat-multi/shared_state.cpp | 59 +++ .../server/chat-multi/shared_state.hpp | 48 +++ .../server/chat-multi/websocket_session.cpp | 132 +++++++ .../server/chat-multi/websocket_session.hpp | 70 ++++ 18 files changed, 1112 insertions(+), 1 deletion(-) create mode 100644 example/websocket/server/chat-multi/CMakeLists.txt create mode 100644 example/websocket/server/chat-multi/Jamfile create mode 100644 example/websocket/server/chat-multi/beast.hpp create mode 100644 example/websocket/server/chat-multi/chat_client.html create mode 100644 example/websocket/server/chat-multi/http_session.cpp create mode 100644 example/websocket/server/chat-multi/http_session.hpp create mode 100644 example/websocket/server/chat-multi/listener.cpp create mode 100644 example/websocket/server/chat-multi/listener.hpp create mode 100644 example/websocket/server/chat-multi/main.cpp create mode 100644 example/websocket/server/chat-multi/net.hpp create mode 100644 example/websocket/server/chat-multi/shared_state.cpp create mode 100644 example/websocket/server/chat-multi/shared_state.hpp create mode 100644 example/websocket/server/chat-multi/websocket_session.cpp create mode 100644 example/websocket/server/chat-multi/websocket_session.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 585207f3..ec423a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Version 206 * Clear error codes idiomatically * websocket stream uses shared_ptr +* Add websocket-chat-multi example -------------------------------------------------------------------------------- diff --git a/doc/qbk/02_examples.qbk b/doc/qbk/02_examples.qbk index 03eca696..b33bcce9 100644 --- a/doc/qbk/02_examples.qbk +++ b/doc/qbk/02_examples.qbk @@ -173,7 +173,7 @@ These servers offer both HTTP and WebSocket services on the same port, and illustrate the implementation of advanced features. [table -[[Description] [Features] [Source File]] +[[Description] [Features] [Sources]] [ [Advanced] [[itemized_list @@ -195,6 +195,15 @@ and illustrate the implementation of advanced features. [Clean exit via SIGINT (CTRL+C) or SIGTERM (kill)] ]] [[example_src example/advanced/server-flex/advanced_server_flex.cpp advanced_server_flex.cpp]] +][ + [Chat Server, multi-threaded] + [[itemized_list + [Multi-user Chat] + [Broadcasting Messages] + [Dual protocols: HTTP and WebSocket] + [Clean exit via SIGINT (CTRL+C) or SIGTERM (kill)] + ]] + [[source_file example/websocket/server/chat-multi]] ]] [endsect] diff --git a/example/websocket/server/CMakeLists.txt b/example/websocket/server/CMakeLists.txt index 23a8bdf1..c5b8da5d 100644 --- a/example/websocket/server/CMakeLists.txt +++ b/example/websocket/server/CMakeLists.txt @@ -8,6 +8,7 @@ # add_subdirectory (async) +add_subdirectory (chat-multi) add_subdirectory (coro) add_subdirectory (fast) add_subdirectory (stackless) diff --git a/example/websocket/server/Jamfile b/example/websocket/server/Jamfile index 9c7565c5..131185c4 100644 --- a/example/websocket/server/Jamfile +++ b/example/websocket/server/Jamfile @@ -8,6 +8,7 @@ # build-project async ; +build-project chat-multi ; build-project coro ; build-project fast ; build-project stackless ; diff --git a/example/websocket/server/chat-multi/CMakeLists.txt b/example/websocket/server/chat-multi/CMakeLists.txt new file mode 100644 index 00000000..4617821f --- /dev/null +++ b/example/websocket/server/chat-multi/CMakeLists.txt @@ -0,0 +1,37 @@ +# +# Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com) +# +# 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 +# + +GroupSources(include/boost/beast beast) +GroupSources(example/cppcon2018 "/") + +file (GLOB APP_FILES + beast.hpp + http_session.cpp + http_session.hpp + Jamfile + listener.cpp + listener.hpp + main.cpp + net.hpp + shared_state.cpp + shared_state.hpp + websocket_session.cpp + websocket_session.hpp + chat_client.html + README.md +) + +source_group ("" FILES ${APP_FILES}) + +add_executable (websocket-chat-multi + ${APP_FILES} + ${BOOST_BEAST_FILES} +) + +set_property(TARGET websocket-chat-multi PROPERTY FOLDER "example-websocket-server") diff --git a/example/websocket/server/chat-multi/Jamfile b/example/websocket/server/chat-multi/Jamfile new file mode 100644 index 00000000..a8d49bfd --- /dev/null +++ b/example/websocket/server/chat-multi/Jamfile @@ -0,0 +1,19 @@ +# +# Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com) +# +# 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-chat-multi : + http_session.cpp + listener.cpp + main.cpp + shared_state.cpp + websocket_session.cpp + : + coverage:no + ubasan:no + ; diff --git a/example/websocket/server/chat-multi/beast.hpp b/example/websocket/server/chat-multi/beast.hpp new file mode 100644 index 00000000..d85120b7 --- /dev/null +++ b/example/websocket/server/chat-multi/beast.hpp @@ -0,0 +1,19 @@ +// +// Copyright (c) 2018 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/vinniefalco/CppCon2018 +// + +#ifndef BOOST_BEAST_EXAMPLE_WEBSOCKET_CHAT_MULTI_BEAST_HPP +#define BOOST_BEAST_EXAMPLE_WEBSOCKET_CHAT_MULTI_BEAST_HPP + +#include + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace websocket = beast::websocket; // from + +#endif diff --git a/example/websocket/server/chat-multi/chat_client.html b/example/websocket/server/chat-multi/chat_client.html new file mode 100644 index 00000000..3913a069 --- /dev/null +++ b/example/websocket/server/chat-multi/chat_client.html @@ -0,0 +1,57 @@ + + + + + WebSocket Chat - CppCon2018 + + +

WebSocket Chat

+

Source code: https://github.com/vinniefalco/CppCon2018

+ + Server URI: + +
+ Your Name:
+ +

+
+  
+ Message
+ + +
+ + + diff --git a/example/websocket/server/chat-multi/http_session.cpp b/example/websocket/server/chat-multi/http_session.cpp new file mode 100644 index 00000000..43838bcb --- /dev/null +++ b/example/websocket/server/chat-multi/http_session.cpp @@ -0,0 +1,354 @@ +// +// Copyright (c) 2018 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/vinniefalco/CppCon2018 +// + +#include "http_session.hpp" +#include "websocket_session.hpp" +#include +#include + +#define BOOST_NO_CXX14_GENERIC_LAMBDAS + +//------------------------------------------------------------------------------ + +// 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 path.to_string(); + std::string result = base.to_string(); +#if 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; +} + +// This function produces an HTTP response for the given +// request. The type of the response object depends on the +// contents of the request, so the interface requires the +// caller to pass a generic lambda for receiving the response. +template< + class Body, class Allocator, + class Send> +void +handle_request( + beast::string_view doc_root, + http::request>&& req, + Send&& send) +{ + // 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() = why.to_string(); + 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 '" + target.to_string() + "' 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: '" + what.to_string() + "'"; + res.prepare_payload(); + return res; + }; + + // Make sure we can handle the method + if( req.method() != http::verb::get && + req.method() != http::verb::head) + return send(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 send(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 == boost::system::errc::no_such_file_or_directory) + return send(not_found(req.target())); + + // Handle an unknown error + if(ec) + return send(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 send(std::move(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 send(std::move(res)); +} + +//------------------------------------------------------------------------------ + +http_session:: +http_session( + tcp::socket socket, + boost::shared_ptr const& state) + : socket_(std::move(socket)) + , state_(state) + , strand_(socket_.get_executor()) +{ +} + +void +http_session:: +run() +{ + // Read a request + http::async_read(socket_, buffer_, req_, + net::bind_executor(strand_, + std::bind( + &http_session::on_read, + shared_from_this(), + std::placeholders::_1, + std::placeholders::_2))); +} + +// Report a failure +void +http_session:: +fail(beast::error_code ec, char const* what) +{ + // Don't report on canceled operations + if(ec == net::error::operation_aborted) + return; + + std::cerr << what << ": " << ec.message() << "\n"; +} + +template +void +http_session:: +send_lambda:: +operator()(http::message&& msg) const +{ + // The lifetime of the message has to extend + // for the duration of the async operation so + // we use a shared_ptr to manage it. + auto sp = boost::make_shared< + http::message>(std::move(msg)); + + // Write the response + auto self = self_.shared_from_this(); + http::async_write( + self_.socket_, + *sp, + net::bind_executor(self_.strand_, + [self, sp](beast::error_code ec, std::size_t bytes) + { + self->on_write(ec, bytes, sp->need_eof()); + })); +} + +void +http_session:: +on_read(beast::error_code ec, std::size_t) +{ + // This means they closed the connection + if(ec == http::error::end_of_stream) + { + socket_.shutdown(tcp::socket::shutdown_send, ec); + return; + } + + // Handle the error, if any + if(ec) + return fail(ec, "read"); + + // See if it is a WebSocket Upgrade + if(websocket::is_upgrade(req_)) + { + // Create a WebSocket session by transferring the socket + boost::make_shared( + std::move(socket_), state_)->run(std::move(req_)); + return; + } + + // Send the response +#ifndef BOOST_NO_CXX14_GENERIC_LAMBDAS + // + // The following code requires generic + // lambdas, available in C++14 and later. + // + handle_request( + state_->doc_root(), + std::move(req_), + [this](auto&& response) + { + // The lifetime of the message has to extend + // for the duration of the async operation so + // we use a shared_ptr to manage it. + using response_type = typename std::decay::type; + auto sp = boost::make_shared(std::forward(response)); + + #if 0 + // NOTE This causes an ICE in gcc 7.3 + // Write the response + http::async_write(this->socket_, *sp, + net::bind_executor(strand_, + [self = shared_from_this(), sp]( + beast::error_code ec, std::size_t bytes) + { + self->on_write(ec, bytes, sp->need_eof()); + })); + #else + // Write the response + auto self = shared_from_this(); + http::async_write(this->socket_, *sp, + net::bind_executor(strand_, + [self, sp]( + beast::error_code ec, std::size_t bytes) + { + self->on_write(ec, bytes, sp->need_eof()); + })); + #endif + }); +#else + // + // This code uses the function object type send_lambda in + // place of a generic lambda which is not available in C++11 + // + handle_request( + state_->doc_root(), + std::move(req_), + send_lambda(*this)); + +#endif +} + +void +http_session:: +on_write(beast::error_code ec, std::size_t, bool close) +{ + // Handle the error, if any + if(ec) + return fail(ec, "write"); + + if(close) + { + // This means we should close the connection, usually because + // the response indicated the "Connection: close" semantic. + socket_.shutdown(tcp::socket::shutdown_send, ec); + return; + } + + // Clear contents of the request message, + // otherwise the read behavior is undefined. + req_ = {}; + + // Read another request + http::async_read(socket_, buffer_, req_, + std::bind( + &http_session::on_read, + shared_from_this(), + std::placeholders::_1, + std::placeholders::_2)); +} diff --git a/example/websocket/server/chat-multi/http_session.hpp b/example/websocket/server/chat-multi/http_session.hpp new file mode 100644 index 00000000..f6d2b4b1 --- /dev/null +++ b/example/websocket/server/chat-multi/http_session.hpp @@ -0,0 +1,57 @@ +// +// Copyright (c) 2018 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/vinniefalco/CppCon2018 +// + +#ifndef BOOST_BEAST_EXAMPLE_WEBSOCKET_CHAT_MULTI_HTTP_SESSION_HPP +#define BOOST_BEAST_EXAMPLE_WEBSOCKET_CHAT_MULTI_HTTP_SESSION_HPP + +#include "net.hpp" +#include "beast.hpp" +#include "shared_state.hpp" +#include +#include +#include + +/** Represents an established HTTP connection +*/ +class http_session : public boost::enable_shared_from_this +{ + tcp::socket socket_; + beast::flat_buffer buffer_; + boost::shared_ptr state_; + http::request req_; + net::strand strand_; + + struct send_lambda + { + http_session& self_; + + explicit + send_lambda(http_session& self) + : self_(self) + { + } + + template + void + operator()(http::message&& msg) const; + }; + + void fail(beast::error_code ec, char const* what); + void on_read(beast::error_code ec, std::size_t); + void on_write(beast::error_code ec, std::size_t, bool close); + +public: + http_session( + tcp::socket socket, + boost::shared_ptr const& state); + + void run(); +}; + +#endif diff --git a/example/websocket/server/chat-multi/listener.cpp b/example/websocket/server/chat-multi/listener.cpp new file mode 100644 index 00000000..42c41b32 --- /dev/null +++ b/example/websocket/server/chat-multi/listener.cpp @@ -0,0 +1,103 @@ +// +// Copyright (c) 2018 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/vinniefalco/CppCon2018 +// + +#include "listener.hpp" +#include "http_session.hpp" +#include + +listener:: +listener( + net::io_context& ioc, + tcp::endpoint endpoint, + boost::shared_ptr const& state) + : acceptor_(ioc) + , socket_(ioc) + , state_(state) +{ + beast::error_code ec; + + // Open the acceptor + acceptor_.open(endpoint.protocol(), ec); + if(ec) + { + fail(ec, "open"); + return; + } + + // Allow address reuse + acceptor_.set_option(net::socket_base::reuse_address(true), ec); + if(ec) + { + fail(ec, "set_option"); + return; + } + + // Bind to the server address + acceptor_.bind(endpoint, ec); + if(ec) + { + fail(ec, "bind"); + return; + } + + // Start listening for connections + acceptor_.listen( + net::socket_base::max_listen_connections, ec); + if(ec) + { + fail(ec, "listen"); + return; + } +} + +void +listener:: +run() +{ + // Start accepting a connection + acceptor_.async_accept( + socket_, + std::bind( + &listener::on_accept, + shared_from_this(), + std::placeholders::_1)); +} + +// Report a failure +void +listener:: +fail(beast::error_code ec, char const* what) +{ + // Don't report on canceled operations + if(ec == net::error::operation_aborted) + return; + std::cerr << what << ": " << ec.message() << "\n"; +} + +// Handle a connection +void +listener:: +on_accept(beast::error_code ec) +{ + if(ec) + return fail(ec, "accept"); + else + // Launch a new session for this connection + boost::make_shared( + std::move(socket_), + state_)->run(); + + // Accept another connection + acceptor_.async_accept( + socket_, + std::bind( + &listener::on_accept, + shared_from_this(), + std::placeholders::_1)); +} diff --git a/example/websocket/server/chat-multi/listener.hpp b/example/websocket/server/chat-multi/listener.hpp new file mode 100644 index 00000000..279e258c --- /dev/null +++ b/example/websocket/server/chat-multi/listener.hpp @@ -0,0 +1,42 @@ +// +// Copyright (c) 2018 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/vinniefalco/CppCon2018 +// + +#ifndef BOOST_BEAST_EXAMPLE_WEBSOCKET_CHAT_MULTI_LISTENER_HPP +#define BOOST_BEAST_EXAMPLE_WEBSOCKET_CHAT_MULTI_LISTENER_HPP + +#include "beast.hpp" +#include "net.hpp" +#include +#include +#include + +// Forward declaration +class shared_state; + +// Accepts incoming connections and launches the sessions +class listener : public boost::enable_shared_from_this +{ + tcp::acceptor acceptor_; + tcp::socket socket_; + boost::shared_ptr state_; + + void fail(beast::error_code ec, char const* what); + void on_accept(beast::error_code ec); + +public: + listener( + net::io_context& ioc, + tcp::endpoint endpoint, + boost::shared_ptr const& state); + + // Start accepting incoming connections + void run(); +}; + +#endif diff --git a/example/websocket/server/chat-multi/main.cpp b/example/websocket/server/chat-multi/main.cpp new file mode 100644 index 00000000..7a45e8a6 --- /dev/null +++ b/example/websocket/server/chat-multi/main.cpp @@ -0,0 +1,84 @@ +// +// Copyright (c) 2018 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/vinniefalco/CppCon2018 +// + +//------------------------------------------------------------------------------ +/* + WebSocket chat server, multi-threaded + + This implements a multi-user chat room using WebSocket. The + `io_context` runs on any number of threads, specified at + the command line. + +*/ +//------------------------------------------------------------------------------ + +#include "listener.hpp" +#include "shared_state.hpp" + +#include +#include +#include +#include + +int +main(int argc, char* argv[]) +{ + // Check command line arguments. + if (argc != 5) + { + std::cerr << + "Usage: websocket-chat-multi
\n" << + "Example:\n" << + " websocket-chat-server 0.0.0.0 8080 .\n"; + return EXIT_FAILURE; + } + auto address = net::ip::make_address(argv[1]); + auto port = static_cast(std::atoi(argv[2])); + auto doc_root = argv[3]; + auto const threads = std::max(1, std::atoi(argv[4])); + + // The io_context is required for all I/O + net::io_context ioc; + + // Create and launch a listening port + boost::make_shared( + ioc, + tcp::endpoint{address, port}, + boost::make_shared(doc_root))->run(); + + // Capture SIGINT and SIGTERM to perform a clean shutdown + net::signal_set signals(ioc, SIGINT, SIGTERM); + signals.async_wait( + [&ioc](boost::system::error_code const&, int) + { + // Stop the io_context. This will cause run() + // to return immediately, eventually destroying the + // io_context and any remaining handlers in it. + ioc.stop(); + }); + + // 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(); + + // (If we get here, it means we got a SIGINT or SIGTERM) + + // Block until all the threads exit + for(auto& t : v) + t.join(); + + return EXIT_SUCCESS; +} diff --git a/example/websocket/server/chat-multi/net.hpp b/example/websocket/server/chat-multi/net.hpp new file mode 100644 index 00000000..bab689fc --- /dev/null +++ b/example/websocket/server/chat-multi/net.hpp @@ -0,0 +1,18 @@ +// +// Copyright (c) 2018 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/vinniefalco/CppCon2018 +// + +#ifndef BOOST_BEAST_EXAMPLE_WEBSOCKET_CHAT_MULTI_NET_HPP +#define BOOST_BEAST_EXAMPLE_WEBSOCKET_CHAT_MULTI_NET_HPP + +#include + +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + +#endif diff --git a/example/websocket/server/chat-multi/shared_state.cpp b/example/websocket/server/chat-multi/shared_state.cpp new file mode 100644 index 00000000..8b994a49 --- /dev/null +++ b/example/websocket/server/chat-multi/shared_state.cpp @@ -0,0 +1,59 @@ +// +// Copyright (c) 2018 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/vinniefalco/CppCon2018 +// + +#include "shared_state.hpp" +#include "websocket_session.hpp" + +shared_state:: +shared_state(std::string doc_root) + : doc_root_(std::move(doc_root)) +{ +} + +void +shared_state:: +join(websocket_session* session) +{ + std::lock_guard lock(mutex_); + sessions_.insert(session); +} + +void +shared_state:: +leave(websocket_session* session) +{ + std::lock_guard lock(mutex_); + sessions_.erase(session); +} + +// Broadcast a message to all websocket client sessions +void +shared_state:: +send(std::string message) +{ + // Put the message in a shared pointer so we can re-use it for each client + auto const ss = boost::make_shared(std::move(message)); + + // Make a local list of all the weak pointers representing + // the sessions, so we can do the actual sending without + // holding the mutex: + std::vector> v; + { + std::lock_guard lock(mutex_); + v.reserve(sessions_.size()); + for(auto p : sessions_) + v.emplace_back(p->weak_from_this()); + } + + // For each session in our local list, try to acquire a strong + // pointer. If successful, then send the message on that session. + for(auto const& wp : v) + if(auto sp = wp.lock()) + sp->send(ss); +} diff --git a/example/websocket/server/chat-multi/shared_state.hpp b/example/websocket/server/chat-multi/shared_state.hpp new file mode 100644 index 00000000..e2b44697 --- /dev/null +++ b/example/websocket/server/chat-multi/shared_state.hpp @@ -0,0 +1,48 @@ +// +// Copyright (c) 2018 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/vinniefalco/CppCon2018 +// + +#ifndef BOOST_BEAST_EXAMPLE_WEBSOCKET_CHAT_MULTI_SHARED_STATE_HPP +#define BOOST_BEAST_EXAMPLE_WEBSOCKET_CHAT_MULTI_SHARED_STATE_HPP + +#include +#include +#include +#include +#include + +// Forward declaration +class websocket_session; + +// Represents the shared server state +class shared_state +{ + std::string const doc_root_; + + // This mutex synchronizes all access to sessions_ + std::mutex mutex_; + + // Keep a list of all the connected clients + std::unordered_set sessions_; + +public: + explicit + shared_state(std::string doc_root); + + std::string const& + doc_root() const noexcept + { + return doc_root_; + } + + void join (websocket_session* session); + void leave (websocket_session* session); + void send (std::string message); +}; + +#endif diff --git a/example/websocket/server/chat-multi/websocket_session.cpp b/example/websocket/server/chat-multi/websocket_session.cpp new file mode 100644 index 00000000..7540544d --- /dev/null +++ b/example/websocket/server/chat-multi/websocket_session.cpp @@ -0,0 +1,132 @@ +// +// Copyright (c) 2018 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/vinniefalco/CppCon2018 +// + +#include "websocket_session.hpp" +#include + +websocket_session:: +websocket_session( + tcp::socket socket, + boost::shared_ptr const& state) + : ws_(std::move(socket)) + , state_(state) + , strand_(ws_.get_executor()) +{ +} + +websocket_session:: +~websocket_session() +{ + // Remove this session from the list of active sessions + state_->leave(this); +} + +void +websocket_session:: +fail(beast::error_code ec, char const* what) +{ + // Don't report these + if( ec == net::error::operation_aborted || + ec == websocket::error::closed) + return; + + std::cerr << what << ": " << ec.message() << "\n"; +} + +void +websocket_session:: +on_accept(beast::error_code ec) +{ + // Handle the error, if any + if(ec) + return fail(ec, "accept"); + + // Add this session to the list of active sessions + state_->join(this); + + // Read a message + ws_.async_read( + buffer_, + net::bind_executor(strand_, + std::bind( + &websocket_session::on_read, + shared_from_this(), + std::placeholders::_1, + std::placeholders::_2))); +} + +void +websocket_session:: +on_read(beast::error_code ec, std::size_t) +{ + // Handle the error, if any + if(ec) + return fail(ec, "read"); + + // Send to all connections + state_->send(beast::buffers_to_string(buffer_.data())); + + // Clear the buffer + buffer_.consume(buffer_.size()); + + // Read another message + ws_.async_read( + buffer_, + net::bind_executor(strand_, + std::bind( + &websocket_session::on_read, + shared_from_this(), + std::placeholders::_1, + std::placeholders::_2))); +} + +void +websocket_session:: +send(boost::shared_ptr const& ss) +{ + // Always add to queue + queue_.push_back(ss); + + // Are we already writing? + if(queue_.size() > 1) + return; + + // We are not currently writing, so send this immediately + ws_.async_write( + net::buffer(*queue_.front()), + net::bind_executor(strand_, + std::bind( + &websocket_session::on_write, + shared_from_this(), + std::placeholders::_1, + std::placeholders::_2))); +} + +void +websocket_session:: +on_write(beast::error_code ec, std::size_t) +{ + // Handle the error, if any + if(ec) + return fail(ec, "write"); + + // Remove the string from the queue + queue_.erase(queue_.begin()); + + // Send the next message if any + if(! queue_.empty()) + ws_.async_write( + net::buffer(*queue_.front()), + net::bind_executor(strand_, + std::bind( + &websocket_session::on_write, + shared_from_this(), + std::placeholders::_1, + std::placeholders::_2))); +} diff --git a/example/websocket/server/chat-multi/websocket_session.hpp b/example/websocket/server/chat-multi/websocket_session.hpp new file mode 100644 index 00000000..08f92bd3 --- /dev/null +++ b/example/websocket/server/chat-multi/websocket_session.hpp @@ -0,0 +1,70 @@ +// +// Copyright (c) 2018 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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/vinniefalco/CppCon2018 +// + +#ifndef BOOST_BEAST_EXAMPLE_WEBSOCKET_CHAT_MULTI_WEBSOCKET_SESSION_HPP +#define BOOST_BEAST_EXAMPLE_WEBSOCKET_CHAT_MULTI_WEBSOCKET_SESSION_HPP + +#include "net.hpp" +#include "beast.hpp" +#include "shared_state.hpp" + +#include +#include +#include +#include + +// Forward declaration +class shared_state; + +/** Represents an active WebSocket connection to the server +*/ +class websocket_session : public boost::enable_shared_from_this +{ + beast::flat_buffer buffer_; + websocket::stream ws_; + boost::shared_ptr state_; + std::vector> queue_; + net::strand strand_; + + void fail(beast::error_code ec, char const* what); + void on_accept(beast::error_code ec); + void on_read(beast::error_code ec, std::size_t bytes_transferred); + void on_write(beast::error_code ec, std::size_t bytes_transferred); + +public: + websocket_session( + tcp::socket socket, + boost::shared_ptr const& state); + + ~websocket_session(); + + template + void + run(http::request> req); + + // Send a message + void + send(boost::shared_ptr const& ss); +}; + +template +void +websocket_session:: +run(http::request> req) +{ + // Accept the websocket handshake + ws_.async_accept( + req, + std::bind( + &websocket_session::on_accept, + shared_from_this(), + std::placeholders::_1)); +} + +#endif