#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "./helpers/helpers.h" #include "./endpoints_handlers/HandleRequest.h" namespace beast = boost::beast; namespace http = beast::http; namespace websocket = beast::websocket; namespace net = boost::asio; using tcp = boost::asio::ip::tcp; using namespace uad; //------------------------------------------------------------------------------ // Report a failure void fail(beast::error_code ec, char const *what) { std::cerr << what << ": " << ec.message() << "\n"; } // Echoes back all received WebSocket messages class websocket_session : public std::enable_shared_from_this { websocket::stream ws_; beast::flat_buffer buffer_; public: // Take ownership of the socket explicit websocket_session(tcp::socket &&socket) : ws_(std::move(socket)) {} // Start the asynchronous accept operation template void do_accept(http::request> 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) + " advanced-server"); })); // Accept the websocket handshake ws_.async_accept(req, beast::bind_front_handler(&websocket_session::on_accept, shared_from_this())); } private: void on_accept(beast::error_code ec) { if (ec) return fail(ec, "accept"); // Read a message do_read(); } void do_read() { // Read a message into our buffer ws_.async_read(buffer_, beast::bind_front_handler(&websocket_session::on_read, shared_from_this())); } void on_read(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); // This indicates that the websocket_session was closed if (ec == websocket::error::closed) return; if (ec) fail(ec, "read"); // Echo the message ws_.text(ws_.got_text()); ws_.async_write(buffer_.data(), beast::bind_front_handler(&websocket_session::on_write, shared_from_this())); } void on_write(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); if (ec) return fail(ec, "write"); // Clear the buffer buffer_.consume(buffer_.size()); // Do another read do_read(); } }; //------------------------------------------------------------------------------ // Handles an HTTP server connection class http_session : public std::enable_shared_from_this { // This queue is used for HTTP pipelining. class queue { enum { // Maximum number of responses we will queue limit = 8 }; // The type-erased, saved work item struct work { virtual ~work() = default; virtual void operator()() = 0; }; http_session &self_; std::vector> items_; public: explicit queue(http_session &self) : self_(self) { static_assert(limit > 0, "queue limit must be positive"); items_.reserve(limit); } // Returns `true` if we have reached the queue limit bool is_full() const { return items_.size() >= limit; } // Called when a message finishes sending // Returns `true` if the caller should initiate a read bool on_write() { BOOST_ASSERT(!items_.empty()); auto const was_full = is_full(); items_.erase(items_.begin()); if (!items_.empty()) (*items_.front())(); return was_full; } // Called by the HTTP handler to send a response. template void operator()(http::message &&msg) { // This holds a work item struct work_impl : work { http_session &self_; http::message msg_; work_impl(http_session &self, http::message &&msg) : self_(self), msg_(std::move(msg)) {} void operator()() { http::async_write(self_.stream_, msg_, beast::bind_front_handler(&http_session::on_write, self_.shared_from_this(), msg_.need_eof())); } }; // Allocate and store the work items_.push_back(boost::make_unique(self_, std::move(msg))); // If there was no previous work, start this one if (items_.size() == 1) (*items_.front())(); } }; beast::tcp_stream stream_; beast::flat_buffer buffer_; std::shared_ptr doc_root_; queue queue_; // 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> parser_; public: // Take ownership of the socket http_session(tcp::socket &&socket, std::shared_ptr const &doc_root) : stream_(std::move(socket)), doc_root_(doc_root), queue_(*this) {} // Start the session void run() { // We need to be executing within a strand to perform async operations // on the I/O objects in this session. Although not strictly necessary // for single-threaded contexts, this example code is written to be // thread-safe by default. net::dispatch(stream_.get_executor(), beast::bind_front_handler(&http_session::do_read, this->shared_from_this())); } private: void 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 using the parser-oriented interface http::async_read( stream_, buffer_, *parser_, beast::bind_front_handler(&http_session::on_read, shared_from_this())); } void on_read(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); // This means they closed the connection if (ec == http::error::end_of_stream) return do_close(); if (ec) return fail(ec, "read"); // 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. std::make_shared(stream_.release_socket()) ->do_accept(parser_->release()); return; } // Send the response HandleRequest(*doc_root_, parser_->release(), queue_); // If we aren't at the queue limit, try to pipeline another request if (!queue_.is_full()) do_read(); } void on_write(bool close, beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); if (ec) return fail(ec, "write"); if (close) { // This means we should close the connection, usually because // the response indicated the "Connection: close" semantic. return do_close(); } // Inform the queue that a write completed if (queue_.on_write()) { // Read another request do_read(); } } void do_close() { // Send a TCP shutdown beast::error_code ec; stream_.socket().shutdown(tcp::socket::shutdown_send, ec); // At this point the connection is closed gracefully } }; //------------------------------------------------------------------------------ // Accepts incoming connections and launches the sessions class listener : public std::enable_shared_from_this { net::io_context &ioc_; tcp::acceptor acceptor_; std::shared_ptr doc_root_; public: listener(net::io_context &ioc, tcp::endpoint endpoint, std::shared_ptr const &doc_root) : ioc_(ioc), acceptor_(net::make_strand(ioc)), doc_root_(doc_root) { 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; } } // Start accepting incoming connections void run() { // We need to be executing within a strand to perform async operations // on the I/O objects in this session. Although not strictly necessary // for single-threaded contexts, this example code is written to be // thread-safe by default. net::dispatch(acceptor_.get_executor(), beast::bind_front_handler(&listener::do_accept, this->shared_from_this())); } private: void do_accept() { // The new connection gets its own strand acceptor_.async_accept( net::make_strand(ioc_), beast::bind_front_handler(&listener::on_accept, shared_from_this())); } void on_accept(beast::error_code ec, tcp::socket socket) { if (ec) { fail(ec, "accept"); } else { // Create the http session and run it std::make_shared(std::move(socket), doc_root_)->run(); } // Accept another connection do_accept(); } }; //------------------------------------------------------------------------------ int main(int argc, char *argv[]) { // Check command line arguments. if (argc != 5) { std::cerr << "Usage: advanced-server
\n" << "Example:\n" << " advanced-server 0.0.0.0 8080 . 1\n"; return EXIT_FAILURE; } auto const address = net::ip::make_address(argv[1]); auto const port = static_cast(std::atoi(argv[2])); auto const doc_root = std::make_shared(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{threads}; // Create and launch a listening port std::make_shared(ioc, tcp::endpoint{address, port}, doc_root) ->run(); // Capture SIGINT and SIGTERM to perform a clean shutdown net::signal_set signals(ioc, SIGINT, SIGTERM); signals.async_wait([&](beast::error_code const &, int) { // Stop the `io_context`. This will cause `run()` // to return immediately, eventually destroying the // `io_context` and all of the sockets 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; }