diff options
Diffstat (limited to 'src/boost/libs/beast/example/websocket/server/chat-multi')
14 files changed, 1131 insertions, 0 deletions
diff --git a/src/boost/libs/beast/example/websocket/server/chat-multi/CMakeLists.txt b/src/boost/libs/beast/example/websocket/server/chat-multi/CMakeLists.txt new file mode 100644 index 000000000..a256b000f --- /dev/null +++ b/src/boost/libs/beast/example/websocket/server/chat-multi/CMakeLists.txt @@ -0,0 +1,41 @@ +# +# 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/websocket/server/chat-multi "/") + +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} +) + +target_link_libraries(websocket-chat-multi + lib-asio + lib-beast) + +set_property(TARGET websocket-chat-multi PROPERTY FOLDER "example-websocket-server") diff --git a/src/boost/libs/beast/example/websocket/server/chat-multi/Jamfile b/src/boost/libs/beast/example/websocket/server/chat-multi/Jamfile new file mode 100644 index 000000000..a8d49bfdd --- /dev/null +++ b/src/boost/libs/beast/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 + : + <variant>coverage:<build>no + <variant>ubasan:<build>no + ; diff --git a/src/boost/libs/beast/example/websocket/server/chat-multi/beast.hpp b/src/boost/libs/beast/example/websocket/server/chat-multi/beast.hpp new file mode 100644 index 000000000..0b7a9c61a --- /dev/null +++ b/src/boost/libs/beast/example/websocket/server/chat-multi/beast.hpp @@ -0,0 +1,19 @@ +// +// Copyright (c) 2016-2019 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 <boost/beast.hpp> + +namespace beast = boost::beast; // from <boost/beast.hpp> +namespace http = beast::http; // from <boost/beast/http.hpp> +namespace websocket = beast::websocket; // from <boost/beast/websocket.hpp> + +#endif diff --git a/src/boost/libs/beast/example/websocket/server/chat-multi/chat_client.html b/src/boost/libs/beast/example/websocket/server/chat-multi/chat_client.html new file mode 100644 index 000000000..2be69cdf0 --- /dev/null +++ b/src/boost/libs/beast/example/websocket/server/chat-multi/chat_client.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8" /> + <title>Boost.Beast WebSocket Chat Client</title> +</head> +<body> + <h1>Boost.Beast WebSocket Chat Client</h1> + <p> + <a href="http://www.boost.org/libs/beast">Boost.Beast</a> + <a href="http://www.boost.org/libs/beast/example/websocket/server/chat-multi">Source Code</a> + </p> + Server URI: <input class="draw-border" id="uri" size="47" value="ws://localhost:8080" style="margin-bottom: 5px;"> + <button class="echo-button" id="connect">Connect</button> + <button class="echo-button" id="disconnect">Disconnect</button><br> + Your Name: <input class="draw-border" id="userName" size=47 style="margin-bottom: 5px;"><br> + <pre id="messages" style="width: 600px; height: 400px; white-space: normal; overflow: auto; border: solid 1px #cccccc; margin-bottom: 5px;"></pre> + <div style="margin-bottom: 5px;"> + Message<br> + <input class="draw-border" id="sendMessage" size="74" value=""> + <button class="echo-button" id="send">Send</button> + </div> + <script> + var ws = null; + function showMessage(msg) { + messages.innerText += msg + "\n"; + messages.scrollTop = messages.scrollHeight - messages.clientHeight; + }; + connect.onclick = function() { + ws = new WebSocket(uri.value); + ws.onopen = function(ev) { + showMessage("[connection opened]"); + }; + ws.onclose = function(ev) { + showMessage("[connection closed]"); + }; + ws.onmessage = function(ev) { + showMessage(ev.data); + }; + ws.onerror = function(ev) { + showMessage("[error]"); + console.log(ev); + }; + }; + disconnect.onclick = function() { + ws.close(); + }; + send.onclick = function() { + ws.send(userName.value + ": " + sendMessage.value); + sendMessage.value = ""; + }; + sendMessage.onkeyup = function(ev) { + ev.preventDefault(); + if (ev.keyCode === 13) { + send.click(); + } + } + </script> +</body> +</html> diff --git a/src/boost/libs/beast/example/websocket/server/chat-multi/http_session.cpp b/src/boost/libs/beast/example/websocket/server/chat-multi/http_session.cpp new file mode 100644 index 000000000..9d4dee871 --- /dev/null +++ b/src/boost/libs/beast/example/websocket/server/chat-multi/http_session.cpp @@ -0,0 +1,371 @@ +// +// Copyright (c) 2016-2019 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 <boost/config.hpp> +#include <iostream> + +#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 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; +} + +// 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<Body, http::basic_fields<Allocator>>&& req, + Send&& send) +{ + // Returns a bad request response + auto const bad_request = + [&req](beast::string_view why) + { + http::response<http::string_body> 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<http::string_body> 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<http::string_body> 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 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<http::empty_body> 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<http::file_body> 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)); +} + +//------------------------------------------------------------------------------ + +struct http_session::send_lambda +{ + http_session& self_; + + explicit + send_lambda(http_session& self) + : self_(self) + { + } + + template<bool isRequest, class Body, class Fields> + void + operator()(http::message<isRequest, Body, Fields>&& 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<isRequest, Body, Fields>>(std::move(msg)); + + // Write the response + auto self = self_.shared_from_this(); + http::async_write( + self_.stream_, + *sp, + [self, sp](beast::error_code ec, std::size_t bytes) + { + self->on_write(ec, bytes, sp->need_eof()); + }); + } +}; + +//------------------------------------------------------------------------------ + +http_session:: +http_session( + tcp::socket&& socket, + boost::shared_ptr<shared_state> const& state) + : stream_(std::move(socket)) + , state_(state) +{ +} + +void +http_session:: +run() +{ + do_read(); +} + +// 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"; +} + +void +http_session:: +do_read() +{ + // Construct a new parser for each message + parser_.emplace(); + + // Apply a reasonable limit to the allowed size + // of the body in bytes to prevent abuse. + parser_->body_limit(10000); + + // Set the timeout. + stream_.expires_after(std::chrono::seconds(30)); + + // Read a request + http::async_read( + stream_, + buffer_, + parser_->get(), + beast::bind_front_handler( + &http_session::on_read, + shared_from_this())); +} + +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) + { + 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(parser_->get())) + { + // Create a websocket session, transferring ownership + // of both the socket and the HTTP request. + boost::make_shared<websocket_session>( + stream_.release_socket(), + state_)->run(parser_->release()); + 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<decltype(response)>::type; + auto sp = boost::make_shared<response_type>(std::forward<decltype(response)>(response)); + + #if 0 + // NOTE This causes an ICE in gcc 7.3 + // Write the response + http::async_write(this->stream_, *sp, + [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(stream_, *sp, + [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(), + parser_->release(), + 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. + stream_.socket().shutdown(tcp::socket::shutdown_send, ec); + return; + } + + // Read another request + do_read(); +} diff --git a/src/boost/libs/beast/example/websocket/server/chat-multi/http_session.hpp b/src/boost/libs/beast/example/websocket/server/chat-multi/http_session.hpp new file mode 100644 index 000000000..637a5d898 --- /dev/null +++ b/src/boost/libs/beast/example/websocket/server/chat-multi/http_session.hpp @@ -0,0 +1,48 @@ +// +// Copyright (c) 2016-2019 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 <boost/optional.hpp> +#include <boost/smart_ptr.hpp> +#include <cstdlib> +#include <memory> + +/** Represents an established HTTP connection +*/ +class http_session : public boost::enable_shared_from_this<http_session> +{ + beast::tcp_stream stream_; + beast::flat_buffer buffer_; + boost::shared_ptr<shared_state> state_; + + // The parser is stored in an optional container so we can + // construct it from scratch it at the beginning of each new message. + boost::optional<http::request_parser<http::string_body>> parser_; + + struct send_lambda; + + void fail(beast::error_code ec, char const* what); + void do_read(); + 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<shared_state> const& state); + + void run(); +}; + +#endif diff --git a/src/boost/libs/beast/example/websocket/server/chat-multi/listener.cpp b/src/boost/libs/beast/example/websocket/server/chat-multi/listener.cpp new file mode 100644 index 000000000..8bbecb49b --- /dev/null +++ b/src/boost/libs/beast/example/websocket/server/chat-multi/listener.cpp @@ -0,0 +1,101 @@ +// +// Copyright (c) 2016-2019 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 <iostream> + +listener:: +listener( + net::io_context& ioc, + tcp::endpoint endpoint, + boost::shared_ptr<shared_state> const& state) + : ioc_(ioc) + , acceptor_(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() +{ + // The new connection gets its own strand + acceptor_.async_accept( + net::make_strand(ioc_), + beast::bind_front_handler( + &listener::on_accept, + shared_from_this())); +} + +// 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, tcp::socket socket) +{ + if(ec) + return fail(ec, "accept"); + else + // Launch a new session for this connection + boost::make_shared<http_session>( + std::move(socket), + state_)->run(); + + // The new connection gets its own strand + acceptor_.async_accept( + net::make_strand(ioc_), + beast::bind_front_handler( + &listener::on_accept, + shared_from_this())); +} diff --git a/src/boost/libs/beast/example/websocket/server/chat-multi/listener.hpp b/src/boost/libs/beast/example/websocket/server/chat-multi/listener.hpp new file mode 100644 index 000000000..a0c536e2e --- /dev/null +++ b/src/boost/libs/beast/example/websocket/server/chat-multi/listener.hpp @@ -0,0 +1,42 @@ +// +// Copyright (c) 2016-2019 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 <boost/smart_ptr.hpp> +#include <memory> +#include <string> + +// Forward declaration +class shared_state; + +// Accepts incoming connections and launches the sessions +class listener : public boost::enable_shared_from_this<listener> +{ + net::io_context& ioc_; + tcp::acceptor acceptor_; + boost::shared_ptr<shared_state> state_; + + void fail(beast::error_code ec, char const* what); + void on_accept(beast::error_code ec, tcp::socket socket); + +public: + listener( + net::io_context& ioc, + tcp::endpoint endpoint, + boost::shared_ptr<shared_state> const& state); + + // Start accepting incoming connections + void run(); +}; + +#endif diff --git a/src/boost/libs/beast/example/websocket/server/chat-multi/main.cpp b/src/boost/libs/beast/example/websocket/server/chat-multi/main.cpp new file mode 100644 index 000000000..8645f1ded --- /dev/null +++ b/src/boost/libs/beast/example/websocket/server/chat-multi/main.cpp @@ -0,0 +1,84 @@ +// +// Copyright (c) 2016-2019 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 <boost/asio/signal_set.hpp> +#include <boost/smart_ptr.hpp> +#include <iostream> +#include <vector> + +int +main(int argc, char* argv[]) +{ + // Check command line arguments. + if (argc != 5) + { + std::cerr << + "Usage: websocket-chat-multi <address> <port> <doc_root> <threads>\n" << + "Example:\n" << + " websocket-chat-server 0.0.0.0 8080 . 5\n"; + return EXIT_FAILURE; + } + auto address = net::ip::make_address(argv[1]); + auto port = static_cast<unsigned short>(std::atoi(argv[2])); + auto doc_root = argv[3]; + auto const threads = std::max<int>(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<listener>( + ioc, + tcp::endpoint{address, port}, + boost::make_shared<shared_state>(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<std::thread> 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/src/boost/libs/beast/example/websocket/server/chat-multi/net.hpp b/src/boost/libs/beast/example/websocket/server/chat-multi/net.hpp new file mode 100644 index 000000000..48a215355 --- /dev/null +++ b/src/boost/libs/beast/example/websocket/server/chat-multi/net.hpp @@ -0,0 +1,18 @@ +// +// Copyright (c) 2016-2019 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 <boost/asio.hpp> + +namespace net = boost::asio; // from <boost/asio.hpp> +using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp> + +#endif diff --git a/src/boost/libs/beast/example/websocket/server/chat-multi/shared_state.cpp b/src/boost/libs/beast/example/websocket/server/chat-multi/shared_state.cpp new file mode 100644 index 000000000..9fceecc5d --- /dev/null +++ b/src/boost/libs/beast/example/websocket/server/chat-multi/shared_state.cpp @@ -0,0 +1,59 @@ +// +// Copyright (c) 2016-2019 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<std::mutex> lock(mutex_); + sessions_.insert(session); +} + +void +shared_state:: +leave(websocket_session* session) +{ + std::lock_guard<std::mutex> 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::string const>(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<boost::weak_ptr<websocket_session>> v; + { + std::lock_guard<std::mutex> 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/src/boost/libs/beast/example/websocket/server/chat-multi/shared_state.hpp b/src/boost/libs/beast/example/websocket/server/chat-multi/shared_state.hpp new file mode 100644 index 000000000..08f4b2d34 --- /dev/null +++ b/src/boost/libs/beast/example/websocket/server/chat-multi/shared_state.hpp @@ -0,0 +1,48 @@ +// +// Copyright (c) 2016-2019 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 <boost/smart_ptr.hpp> +#include <memory> +#include <mutex> +#include <string> +#include <unordered_set> + +// 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<websocket_session*> 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/src/boost/libs/beast/example/websocket/server/chat-multi/websocket_session.cpp b/src/boost/libs/beast/example/websocket/server/chat-multi/websocket_session.cpp new file mode 100644 index 000000000..4807a5f1e --- /dev/null +++ b/src/boost/libs/beast/example/websocket/server/chat-multi/websocket_session.cpp @@ -0,0 +1,135 @@ +// +// Copyright (c) 2016-2019 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 <iostream> + +websocket_session:: +websocket_session( + tcp::socket&& socket, + boost::shared_ptr<shared_state> const& state) + : ws_(std::move(socket)) + , state_(state) +{ +} + +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_, + beast::bind_front_handler( + &websocket_session::on_read, + shared_from_this())); +} + +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_, + beast::bind_front_handler( + &websocket_session::on_read, + shared_from_this())); +} + +void +websocket_session:: +send(boost::shared_ptr<std::string const> const& ss) +{ + // Post our work to the strand, this ensures + // that the members of `this` will not be + // accessed concurrently. + + net::post( + ws_.get_executor(), + beast::bind_front_handler( + &websocket_session::on_send, + shared_from_this(), + ss)); +} + +void +websocket_session:: +on_send(boost::shared_ptr<std::string const> 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()), + beast::bind_front_handler( + &websocket_session::on_write, + shared_from_this())); +} + +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()), + beast::bind_front_handler( + &websocket_session::on_write, + shared_from_this())); +} diff --git a/src/boost/libs/beast/example/websocket/server/chat-multi/websocket_session.hpp b/src/boost/libs/beast/example/websocket/server/chat-multi/websocket_session.hpp new file mode 100644 index 000000000..c47d48ac9 --- /dev/null +++ b/src/boost/libs/beast/example/websocket/server/chat-multi/websocket_session.hpp @@ -0,0 +1,86 @@ +// +// Copyright (c) 2016-2019 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 <cstdlib> +#include <memory> +#include <string> +#include <vector> + +// Forward declaration +class shared_state; + +/** Represents an active WebSocket connection to the server +*/ +class websocket_session : public boost::enable_shared_from_this<websocket_session> +{ + beast::flat_buffer buffer_; + websocket::stream<beast::tcp_stream> ws_; + boost::shared_ptr<shared_state> state_; + std::vector<boost::shared_ptr<std::string const>> queue_; + + 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<shared_state> const& state); + + ~websocket_session(); + + template<class Body, class Allocator> + void + run(http::request<Body, http::basic_fields<Allocator>> req); + + // Send a message + void + send(boost::shared_ptr<std::string const> const& ss); + +private: + void + on_send(boost::shared_ptr<std::string const> const& ss); +}; + +template<class Body, class Allocator> +void +websocket_session:: +run(http::request<Body, http::basic_fields<Allocator>> req) +{ + // 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-chat-multi"); + })); + + // Accept the websocket handshake + ws_.async_accept( + req, + beast::bind_front_handler( + &websocket_session::on_accept, + shared_from_this())); +} + +#endif |