diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a29059a..50720440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +Version 189: + +* Add CppCon2018 chat server example and video + +-------------------------------------------------------------------------------- + +Version 188: + +* Remove extraneous strand from example +* Add missing include in http/read.ipp +* Test for gcc warning bug +* Fix a spurious gcc warning + +-------------------------------------------------------------------------------- + Version 187: * Add experimental timeout_socket diff --git a/README.md b/README.md index d56ad696..23d5b620 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,11 @@ This library is designed for: ## Appearances -| Bishop Fox 2018 | -| ------------ | -| Beast Security Review | +| CppCon 2018 | Bishop Fox 2018 | +| ------------ | ------------ | +| Beast | Beast Security Review | -| CppCon 2017 | CppCast 2017 | CppCon 2016 | +| CppCon 2017 | CppCast 2017 | CppCon 2016 | | ------------ | ------------ | ----------- | | Beast | Vinnie Falco | Beast | diff --git a/doc/qbk/02_examples.qbk b/doc/qbk/02_examples.qbk index 55363bd9..03eca696 100644 --- a/doc/qbk/02_examples.qbk +++ b/doc/qbk/02_examples.qbk @@ -201,6 +201,28 @@ and illustrate the implementation of advanced features. +[section CppCon 2018] + +This talk was given at [@https://cppcon.org CppCon 2018]. In this +presentation, we develop a multi-user chat server written in C++ using +Beast WebSocket, which uses a provided chat client written in HTML and +JavaScript. The source files for this example are located at +[source_file example/cppcon2018]. + + +[block''' + + + + + +'''] + +[endsect] + + + [section Common Files] Some of the examples use one or more shared header files, they are diff --git a/doc/qbk/06_websocket.qbk b/doc/qbk/06_websocket.qbk index a5413a8d..5d00e645 100644 --- a/doc/qbk/06_websocket.qbk +++ b/doc/qbk/06_websocket.qbk @@ -29,13 +29,13 @@ Boost.Asio with a consistent asynchronous model using a modern C++ approach. [ws_snippet_1] ] -[include 06_websocket/1_streams.qbk] -[include 06_websocket/2_connect.qbk] -[include 06_websocket/3_client.qbk] -[include 06_websocket/4_server.qbk] -[include 06_websocket/5_messages.qbk] -[include 06_websocket/6_control.qbk] -[include 06_websocket/7_teardown.qbk] -[include 06_websocket/8_notes.qbk] +[include 06_websocket/01_streams.qbk] +[include 06_websocket/02_connect.qbk] +[include 06_websocket/03_client.qbk] +[include 06_websocket/04_server.qbk] +[include 06_websocket/05_messages.qbk] +[include 06_websocket/06_control.qbk] +[include 06_websocket/07_teardown.qbk] +[include 06_websocket/08_notes.qbk] [endsect] diff --git a/doc/qbk/06_websocket/1_streams.qbk b/doc/qbk/06_websocket/01_streams.qbk similarity index 100% rename from doc/qbk/06_websocket/1_streams.qbk rename to doc/qbk/06_websocket/01_streams.qbk diff --git a/doc/qbk/06_websocket/2_connect.qbk b/doc/qbk/06_websocket/02_connect.qbk similarity index 100% rename from doc/qbk/06_websocket/2_connect.qbk rename to doc/qbk/06_websocket/02_connect.qbk diff --git a/doc/qbk/06_websocket/3_client.qbk b/doc/qbk/06_websocket/03_client.qbk similarity index 100% rename from doc/qbk/06_websocket/3_client.qbk rename to doc/qbk/06_websocket/03_client.qbk diff --git a/doc/qbk/06_websocket/4_server.qbk b/doc/qbk/06_websocket/04_server.qbk similarity index 100% rename from doc/qbk/06_websocket/4_server.qbk rename to doc/qbk/06_websocket/04_server.qbk diff --git a/doc/qbk/06_websocket/5_messages.qbk b/doc/qbk/06_websocket/05_messages.qbk similarity index 100% rename from doc/qbk/06_websocket/5_messages.qbk rename to doc/qbk/06_websocket/05_messages.qbk diff --git a/doc/qbk/06_websocket/6_control.qbk b/doc/qbk/06_websocket/06_control.qbk similarity index 100% rename from doc/qbk/06_websocket/6_control.qbk rename to doc/qbk/06_websocket/06_control.qbk diff --git a/doc/qbk/06_websocket/7_teardown.qbk b/doc/qbk/06_websocket/07_teardown.qbk similarity index 100% rename from doc/qbk/06_websocket/7_teardown.qbk rename to doc/qbk/06_websocket/07_teardown.qbk diff --git a/doc/qbk/06_websocket/8_notes.qbk b/doc/qbk/06_websocket/08_notes.qbk similarity index 100% rename from doc/qbk/06_websocket/8_notes.qbk rename to doc/qbk/06_websocket/08_notes.qbk diff --git a/doc/qbk/09_releases.qbk b/doc/qbk/09_releases.qbk index 0684b18e..8dbba163 100644 --- a/doc/qbk/09_releases.qbk +++ b/doc/qbk/09_releases.qbk @@ -9,14 +9,27 @@ [section Release Notes] - - [heading Boost 1.69] +[* New Videos] + +[block''' + + + + + +'''] + [* New Features] * ([issue 1133]) Add `BOOST_BEAST_USE_STD_STRING_VIEW` +[* Examples] + +* New WebSocket server and browser-based client: [source_file example/cppcon2018] + [*Fixes] * ([issue 1245]) Fix a rare case of incorrect UTF8 validation diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 950c29db..c2704007 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -8,6 +8,7 @@ # add_subdirectory (advanced) +add_subdirectory (cppcon2018) add_subdirectory (http) add_subdirectory (websocket) diff --git a/example/Jamfile b/example/Jamfile index cccbfe4b..be63be5f 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -8,6 +8,7 @@ # build-project advanced ; +build-project cppcon2018 ; build-project http ; build-project websocket ; diff --git a/example/cppcon2018/CMakeLists.txt b/example/cppcon2018/CMakeLists.txt new file mode 100644 index 00000000..c4d938e8 --- /dev/null +++ b/example/cppcon2018/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-server + ${APP_FILES} + ${BOOST_BEAST_FILES} +) + +set_property(TARGET websocket-chat-server PROPERTY FOLDER "example-cppcon2018") diff --git a/example/cppcon2018/Jamfile b/example/cppcon2018/Jamfile new file mode 100644 index 00000000..a22014e9 --- /dev/null +++ b/example/cppcon2018/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-server : + http_session.cpp + listener.cpp + main.cpp + shared_state.cpp + websocket_session.cpp + : + coverage:no + ubasan:no + ; diff --git a/example/cppcon2018/README.md b/example/cppcon2018/README.md new file mode 100644 index 00000000..4e62b87a --- /dev/null +++ b/example/cppcon2018/README.md @@ -0,0 +1,23 @@ +*This repository contains the presentation file and compiling +source code for the CppCon2018 talk.* + +# Get Rich Quick! Using Boost.Beast WebSockets and Networking TS + +Do you want to make a lot of money? You'll see some examples of free +browser and server based WebSocket programs which have earned their +respective individual authors tens of millions of dollars in no time +at all. Perhaps after seeing this talk in person, you'll write the +next massively successful WebSocket app! + +The WebSocket protocol powers the interactive web by enabling two-way +messaging between the browser and the web server. The Boost.Beast +library implements this protocol on top of the industry standard +Boost.Asio library which models the Networking Technical Specification +proposed for the ISO C++ Standard. + +This presentation introduces Networking TS concepts and algorithms, +how to read their requirements, and how to use them in your programs. +We will build from scratch a multi-user chat server in C++11 using +Beast, and the corresponding browser-based chat client in HTML and +JavaScript. No prior knowledge or understanding of Beast or Asio is +required, the talk is suited for everyone. diff --git a/example/cppcon2018/beast.hpp b/example/cppcon2018/beast.hpp new file mode 100644 index 00000000..d24e24a5 --- /dev/null +++ b/example/cppcon2018/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 CPPCON2018_BEAST_HPP +#define CPPCON2018_BEAST_HPP + +#include + +namespace beast = boost::beast; +namespace http = boost::beast::http; // from +namespace websocket = boost::beast::websocket; // from + +#endif diff --git a/example/cppcon2018/chat_client.html b/example/cppcon2018/chat_client.html new file mode 100644 index 00000000..3913a069 --- /dev/null +++ b/example/cppcon2018/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/cppcon2018/http_session.cpp b/example/cppcon2018/http_session.cpp new file mode 100644 index 00000000..2c554b60 --- /dev/null +++ b/example/cppcon2018/http_session.cpp @@ -0,0 +1,349 @@ +// +// 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. +boost::beast::string_view +mime_type(boost::beast::string_view path) +{ + using boost::beast::iequals; + auto const ext = [&path] + { + auto const pos = path.rfind("."); + if(pos == boost::beast::string_view::npos) + return boost::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( + boost::beast::string_view base, + boost::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( + boost::beast::string_view doc_root, + http::request>&& req, + Send&& send) +{ + // Returns a bad request response + auto const bad_request = + [&req](boost::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](boost::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](boost::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("..") != boost::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 + boost::beast::error_code ec; + http::file_body::value_type body; + body.open(path.c_str(), boost::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, + std::shared_ptr const& state) + : socket_(std::move(socket)) + , state_(state) +{ +} + +void +http_session:: +run() +{ + // Read a request + http::async_read(socket_, buffer_, req_, + [self = shared_from_this()] + (error_code ec, std::size_t bytes) + { + self->on_read(ec, bytes); + }); +} + +// Report a failure +void +http_session:: +fail(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 = std::make_shared< + http::message>(std::move(msg)); + + // Write the response + auto self = self_.shared_from_this(); + http::async_write( + self_.socket_, + *sp, + [self, sp](error_code ec, std::size_t bytes) + { + self->on_write(ec, bytes, sp->need_eof()); + }); +} + +void +http_session:: +on_read(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 + std::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 = std::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, + [self = shared_from_this(), sp]( + 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, + [self, sp]( + 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(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_, + [self = shared_from_this()] + (error_code ec, std::size_t bytes) + { + self->on_read(ec, bytes); + }); +} diff --git a/example/cppcon2018/http_session.hpp b/example/cppcon2018/http_session.hpp new file mode 100644 index 00000000..530d2fa8 --- /dev/null +++ b/example/cppcon2018/http_session.hpp @@ -0,0 +1,56 @@ +// +// 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 CPPCON2018_HTTP_SESSION_HPP +#define CPPCON2018_HTTP_SESSION_HPP + +#include "net.hpp" +#include "beast.hpp" +#include "shared_state.hpp" +#include +#include + +/** Represents an established HTTP connection +*/ +class http_session : public std::enable_shared_from_this +{ + tcp::socket socket_; + beast::flat_buffer buffer_; + std::shared_ptr state_; + http::request req_; + + struct send_lambda + { + http_session& self_; + + explicit + send_lambda(http_session& self) + : self_(self) + { + } + + template + void + operator()(http::message&& msg) const; + }; + + void fail(error_code ec, char const* what); + void on_read(error_code ec, std::size_t); + void on_write( + error_code ec, std::size_t, bool close); + +public: + http_session( + tcp::socket socket, + std::shared_ptr const& state); + + void run(); +}; + +#endif diff --git a/example/cppcon2018/listener.cpp b/example/cppcon2018/listener.cpp new file mode 100644 index 00000000..a6087260 --- /dev/null +++ b/example/cppcon2018/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, + std::shared_ptr const& state) + : acceptor_(ioc) + , socket_(ioc) + , state_(state) +{ + 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)); + 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_, + [self = shared_from_this()](error_code ec) + { + self->on_accept(ec); + }); +} + +// Report a failure +void +listener:: +fail(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(error_code ec) +{ + if(ec) + return fail(ec, "accept"); + else + // Launch a new session for this connection + std::make_shared( + std::move(socket_), + state_)->run(); + + // Accept another connection + acceptor_.async_accept( + socket_, + [self = shared_from_this()](error_code ec) + { + self->on_accept(ec); + }); +} diff --git a/example/cppcon2018/listener.hpp b/example/cppcon2018/listener.hpp new file mode 100644 index 00000000..3ee1a898 --- /dev/null +++ b/example/cppcon2018/listener.hpp @@ -0,0 +1,40 @@ +// +// 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 CPPCON2018_LISTENER_HPP +#define CPPCON2018_LISTENER_HPP + +#include "net.hpp" +#include +#include + +// Forward declaration +class shared_state; + +// Accepts incoming connections and launches the sessions +class listener : public std::enable_shared_from_this +{ + tcp::acceptor acceptor_; + tcp::socket socket_; + std::shared_ptr state_; + + void fail(error_code ec, char const* what); + void on_accept(error_code ec); + +public: + listener( + net::io_context& ioc, + tcp::endpoint endpoint, + std::shared_ptr const& state); + + // Start accepting incoming connections + void run(); +}; + +#endif diff --git a/example/cppcon2018/main.cpp b/example/cppcon2018/main.cpp new file mode 100644 index 00000000..79c4d9de --- /dev/null +++ b/example/cppcon2018/main.cpp @@ -0,0 +1,65 @@ +// +// 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 + + This implements a multi-user chat room using WebSocket. +*/ +//------------------------------------------------------------------------------ + +#include "listener.hpp" +#include "shared_state.hpp" +#include +#include + +int +main(int argc, char* argv[]) +{ + // Check command line arguments. + if (argc != 4) + { + std::cerr << + "Usage: websocket-chat-server
\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]; + + // The io_context is required for all I/O + net::io_context ioc; + + // Create and launch a listening port + std::make_shared( + ioc, + tcp::endpoint{address, port}, + std::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 main thread + ioc.run(); + + // (If we get here, it means we got a SIGINT or SIGTERM) + + return EXIT_SUCCESS; +} diff --git a/example/cppcon2018/net.hpp b/example/cppcon2018/net.hpp new file mode 100644 index 00000000..4c925d05 --- /dev/null +++ b/example/cppcon2018/net.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 CPPCON2018_ASIO_HPP +#define CPPCON2018_ASIO_HPP + +#include + +namespace net = boost::asio; // namespace asio +using tcp = net::ip::tcp; // from +using error_code = boost::system::error_code; // from + +#endif diff --git a/example/cppcon2018/shared_state.cpp b/example/cppcon2018/shared_state.cpp new file mode 100644 index 00000000..a31d0e1a --- /dev/null +++ b/example/cppcon2018/shared_state.cpp @@ -0,0 +1,41 @@ +// +// 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) +{ + sessions_.insert(&session); +} + +void +shared_state:: +leave(websocket_session& session) +{ + sessions_.erase(&session); +} + +void +shared_state:: +send(std::string message) +{ + auto const ss = std::make_shared(std::move(message)); + + for(auto session : sessions_) + session->send(ss); +} diff --git a/example/cppcon2018/shared_state.hpp b/example/cppcon2018/shared_state.hpp new file mode 100644 index 00000000..b00bf11a --- /dev/null +++ b/example/cppcon2018/shared_state.hpp @@ -0,0 +1,45 @@ +// +// 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 CPPCON2018_SHARED_STATE_HPP +#define CPPCON2018_SHARED_STATE_HPP + +#include +#include +#include + +// Forward declaration +class websocket_session; + +// Represents the shared server state +class shared_state +{ + std::string doc_root_; + + // This simple method of tracking + // sessions only works with an implicit + // strand (i.e. a single-threaded server) + 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/cppcon2018/websocket_session.cpp b/example/cppcon2018/websocket_session.cpp new file mode 100644 index 00000000..e87cee58 --- /dev/null +++ b/example/cppcon2018/websocket_session.cpp @@ -0,0 +1,126 @@ +// +// 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" + +websocket_session:: +websocket_session( + tcp::socket socket, + std::shared_ptr 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(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(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_, + [sp = shared_from_this()]( + error_code ec, std::size_t bytes) + { + sp->on_read(ec, bytes); + }); +} + +void +websocket_session:: +on_read(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_, + [sp = shared_from_this()]( + error_code ec, std::size_t bytes) + { + sp->on_read(ec, bytes); + }); +} + +void +websocket_session:: +send(std::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()), + [sp = shared_from_this()]( + error_code ec, std::size_t bytes) + { + sp->on_write(ec, bytes); + }); +} + +void +websocket_session:: +on_write(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()), + [sp = shared_from_this()]( + error_code ec, std::size_t bytes) + { + sp->on_write(ec, bytes); + }); +} diff --git a/example/cppcon2018/websocket_session.hpp b/example/cppcon2018/websocket_session.hpp new file mode 100644 index 00000000..a8cd5be9 --- /dev/null +++ b/example/cppcon2018/websocket_session.hpp @@ -0,0 +1,69 @@ +// +// 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 CPPCON2018_WEBSOCKET_SESSION_HPP +#define CPPCON2018_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 std::enable_shared_from_this +{ + beast::flat_buffer buffer_; + websocket::stream ws_; + std::shared_ptr state_; + std::vector> queue_; + + void fail(error_code ec, char const* what); + void on_accept(error_code ec); + void on_read(error_code ec, std::size_t bytes_transferred); + void on_write(error_code ec, std::size_t bytes_transferred); + +public: + websocket_session( + tcp::socket socket, + std::shared_ptr const& state); + + ~websocket_session(); + + template + void + run(http::request> req); + + // Send a message + void + send(std::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