From e5f1d4d0101e2a17d6af7076799fa97875a2af39 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Fri, 23 Jun 2017 01:44:25 -0700 Subject: [PATCH] Add http-server example --- CHANGELOG.md | 1 + doc/0_main.qbk | 2 +- doc/2_examples.qbk | 12 + example/CMakeLists.txt | 1 + example/Jamfile | 1 + example/http-server/CMakeLists.txt | 17 ++ example/http-server/Jamfile | 13 ++ example/http-server/fields_alloc.hpp | 195 +++++++++++++++++ example/http-server/http_server.cpp | 314 +++++++++++++++++++++++++++ 9 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 example/http-server/CMakeLists.txt create mode 100644 example/http-server/Jamfile create mode 100644 example/http-server/fields_alloc.hpp create mode 100644 example/http-server/http_server.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index b482c87a..6e3351f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Version 66: * Tidy up message piecewise ctors * Add header aliases * basic_fields optimizations +* Add http-server example -------------------------------------------------------------------------------- diff --git a/doc/0_main.qbk b/doc/0_main.qbk index a185927e..7a3f687a 100644 --- a/doc/0_main.qbk +++ b/doc/0_main.qbk @@ -36,7 +36,7 @@ [def __asio_handler_allocate__ [@http://www.boost.org/doc/html/boost_asio/reference/asio_handler_allocate.html `asio_handler_allocate`]] [def __io_service__ [@http://www.boost.org/doc/html/boost_asio/reference/io_service.html `io_service`]] [def __socket__ [@http://www.boost.org/doc/html/boost_asio/reference/ip__tcp/socket.html `boost::asio::ip::tcp::socket`]] -[def __ssl_stream__ [@http://www.boost.org/doc/html/boost_asio/reference/ssl_stream.html `boost::asio::ssl::stream`]] +[def __ssl_stream__ [@http://www.boost.org/doc/html/boost_asio/reference/ssl__stream.html `boost::asio::ssl::stream`]] [def __streambuf__ [@http://www.boost.org/doc/html/boost_asio/reference/streambuf.html `boost::asio::streambuf`]] [def __use_future__ [@http://www.boost.org/doc/html/boost_asio/reference/use_future_t.html `boost::asio::use_future`]] [def __void_or_deduced__ [@http://www.boost.org/doc/html/boost_asio/reference/asynchronous_operations.html#boost_asio.reference.asynchronous_operations.return_type_of_an_initiating_function ['void-or-deduced]]] diff --git a/doc/2_examples.qbk b/doc/2_examples.qbk index 6022954a..a16a17da 100644 --- a/doc/2_examples.qbk +++ b/doc/2_examples.qbk @@ -66,6 +66,18 @@ over a TLS connection. Requires OpenSSL to build. +[section HTTP Server] + +This example implements a very simple HTTP server with +some optimizations suitable for calculating benchmarks. + +* [repo_file example/http-server/fields_alloc.cpp] +* [repo_file example/http-server/http_server.cpp] + +[endsect] + + + [section WebSocket Client (with SSL)] Establish a WebSocket connection over an encrypted TLS connection, diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index daff6085..3a823ab7 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -3,6 +3,7 @@ add_subdirectory (echo-op) add_subdirectory (http-client) add_subdirectory (http-crawl) +add_subdirectory (http-server) add_subdirectory (server-framework) add_subdirectory (websocket-client) diff --git a/example/Jamfile b/example/Jamfile index 00d289a1..d1ee2ab4 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -8,6 +8,7 @@ build-project echo-op ; build-project http-client ; build-project http-crawl ; +build-project http-server ; build-project server-framework ; build-project websocket-client ; diff --git a/example/http-server/CMakeLists.txt b/example/http-server/CMakeLists.txt new file mode 100644 index 00000000..93a5d4ff --- /dev/null +++ b/example/http-server/CMakeLists.txt @@ -0,0 +1,17 @@ +# Part of Beast + +GroupSources(include/beast beast) + +GroupSources(example/http-server "/") + +add_executable (http-server + ${BEAST_INCLUDES} + fields_alloc.hpp + http_server.cpp +) + +target_link_libraries(http-server + Beast + ${Boost_FILESYSTEM_LIBRARY} + ) + diff --git a/example/http-server/Jamfile b/example/http-server/Jamfile new file mode 100644 index 00000000..2b18598b --- /dev/null +++ b/example/http-server/Jamfile @@ -0,0 +1,13 @@ +# +# Copyright (c) 2013-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) +# + +exe http-server : + http_server.cpp + : + coverage:no + ubasan:no + ; diff --git a/example/http-server/fields_alloc.hpp b/example/http-server/fields_alloc.hpp new file mode 100644 index 00000000..a5d62acf --- /dev/null +++ b/example/http-server/fields_alloc.hpp @@ -0,0 +1,195 @@ +// +// Copyright (c) 2017 Christopher M. Kohlhoff (chris at kohlhoff 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) +// + +#ifndef BEAST_EXAMPLE_FIELDS_ALLOC_HPP +#define BEAST_EXAMPLE_FIELDS_ALLOC_HPP + +#include +#include +#include +#include + +namespace detail { + +struct static_pool +{ + std::size_t size_; + std::size_t refs_ = 1; + std::size_t count_ = 0; + char* p_; + + char* + end() + { + return reinterpret_cast(this+1) + size_; + } + + explicit + static_pool(std::size_t size) + : size_(size) + , p_(reinterpret_cast(this+1)) + { + } + +public: + static + static_pool& + construct(std::size_t size) + { + auto p = new char[sizeof(static_pool) + size]; + return *(new(p) static_pool{size}); + } + + static_pool& + share() + { + ++refs_; + return *this; + } + + void + destroy() + { + if(refs_--) + return; + this->~static_pool(); + delete[] reinterpret_cast(this); + } + + void* + alloc(std::size_t n) + { + auto last = p_ + n; + if(last >= end()) + BOOST_THROW_EXCEPTION(std::bad_alloc{}); + ++count_; + auto p = p_; + p_ = last; + return p; + } + + void + dealloc() + { + if(--count_) + return; + p_ = reinterpret_cast(this+1); + } +}; + +} // detail + +/** A non-thread-safe allocator optimized for @ref basic_fields. + + This allocator obtains memory from a pre-allocated memory block + of a given size. It does nothing in deallocate until all + previously allocated blocks are deallocated, upon which it + resets the internal memory block for re-use. + + To use this allocator declare an instance persistent to the + connection or session, and construct with the block size. + A good rule of thumb is 20% more than the maximum allowed + header size. For example if the application only allows up + to an 8,000 byte header, the block size could be 9,600. + + Then, for every instance of `message` construct the header + with a copy of the previously declared allocator instance. +*/ +template +struct fields_alloc +{ + detail::static_pool& pool_; + +public: + using value_type = T; + using is_always_equal = std::false_type; + using pointer = T*; + using reference = T&; + using const_pointer = T const*; + using const_reference = T const&; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + template + struct rebind + { + using other = fields_alloc; + }; + + explicit + fields_alloc(std::size_t size) + : pool_(detail::static_pool::construct(size)) + { + } + + fields_alloc(fields_alloc const& other) + : pool_(other.pool_.share()) + { + } + + template + fields_alloc(fields_alloc const& other) + : pool_(other.pool_.share()) + { + } + + ~fields_alloc() + { + pool_.destroy(); + } + + value_type* + allocate(size_type n) + { + return static_cast( + pool_.alloc(n * sizeof(T))); + } + + void + deallocate(value_type*, size_type) + { + pool_.dealloc(); + } + +#if defined(BOOST_LIBSTDCXX_VERSION) && BOOST_LIBSTDCXX_VERSION < 60000 + template + void + construct(U* ptr, Args&&... args) + { + ::new((void*)ptr) U(std::forward(args)...); + } + + template + void + destroy(U* ptr) + { + ptr->~U(); + } +#endif + + template + friend + bool + operator==( + fields_alloc const& lhs, + fields_alloc const& rhs) + { + return &lhs.pool_ == &rhs.pool_; + } + + template + friend + bool + operator!=( + fields_alloc const& lhs, + fields_alloc const& rhs) + { + return ! (lhs == rhs); + } +}; + +#endif diff --git a/example/http-server/http_server.cpp b/example/http-server/http_server.cpp new file mode 100644 index 00000000..3a614613 --- /dev/null +++ b/example/http-server/http_server.cpp @@ -0,0 +1,314 @@ +// +// Copyright (c) 2017 Christopher M. Kohlhoff (chris at kohlhoff 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) +// + +#include "fields_alloc.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ip = boost::asio::ip; // from +using tcp = boost::asio::ip::tcp; // from +namespace http = beast::http; // from + +// Return a reasonable mime type based on the extension of a file. +// +beast::string_view +mime_type(boost::filesystem::path const& path) +{ + using beast::iequals; + auto const ext = path.extension().string(); + if(iequals(ext, ".txt")) return "text/plain"; + 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, ".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"; +} + +class http_worker +{ +public: + http_worker(http_worker const&) = delete; + http_worker& operator=(http_worker const&) = delete; + + http_worker(tcp::acceptor& acceptor, const std::string& doc_root) : + acceptor_(acceptor), + doc_root_(doc_root), + socket_(acceptor.get_io_service()), + alloc_(8192), + request_deadline_(acceptor.get_io_service(), + std::chrono::steady_clock::time_point::max()) + { + } + + void start() + { + accept(); + check_deadline(); + } + +private: + using request_body_t = http::basic_dynamic_body>; + + // The acceptor used to listen for incoming connections. + tcp::acceptor& acceptor_; + + // The path to the root of the document directory. + std::string doc_root_; + + // The socket for the currently connected client. + tcp::socket socket_; + + // The buffer for performing reads + beast::static_buffer_n<8192> buffer_; + + // The parser for reading the requests + using alloc_type = fields_alloc; + alloc_type alloc_; + boost::optional> parser_; + + // The timer putting a time limit on requests. + boost::asio::basic_waitable_timer request_deadline_; + + // The response message. + http::response response_; + + // The response serializer. + boost::optional> serializer_; + + void accept() + { + // Clean up any previous connection. + beast::error_code ec; + socket_.close(ec); + buffer_.consume(buffer_.size()); + + acceptor_.async_accept( + socket_, + [this](beast::error_code ec) + { + if (ec) + { + accept(); + } + else + { + // Request must be fully processed within 60 seconds. + request_deadline_.expires_from_now( + std::chrono::seconds(60)); + + read_header(); + } + }); + } + + void read_header() + { + // On each read the parser needs to be destroyed and + // recreated. We store it in a boost::optional to + // achieve that. + // + // Arguments passed to the parser constructor are + // forwarded to the message object. A single argument + // is forwarded to the body constructor. + // + // We construct the dynamic body with a 1MB limit + // to prevent vulnerability to buffer attacks. + // + parser_.emplace( + std::piecewise_construct, + std::make_tuple(), + std::make_tuple(alloc_)); + + http::async_read_header( + socket_, + buffer_, + *parser_, + [this](beast::error_code ec) + { + if (ec) + accept(); + else + read_body(); + }); + } + + void read_body() + { + http::async_read( + socket_, + buffer_, + *parser_, + [this](beast::error_code ec) + { + if (ec) + accept(); + else + process_request(parser_->get()); + }); + } + + void process_request(http::request> const& req) + { + response_.version = 11; + response_.set(http::field::connection, "close"); + + switch (req.method()) + { + case http::verb::get: + response_.result(http::status::ok); + response_.set(http::field::server, "Beast"); + load_file(req.target()); + break; + + default: + // We return responses indicating an error if + // we do not recognize the request method. + response_.result(http::status::bad_request); + response_.set(http::field::content_type, "text/plain"); + response_.body = "Invalid request-method '" + req.method_string().to_string() + "'"; + response_.prepare_payload(); + break; + } + + write_response(); + } + + void load_file(beast::string_view target) + { + // Request path must be absolute and not contain "..". + if (target.empty() || target[0] != '/' || target.find("..") != std::string::npos) + { + response_.result(http::status::not_found); + response_.set(http::field::content_type, "text/plain"); + response_.body = "File not found\r\n"; + return; + } + + std::string full_path = doc_root_; + full_path.append(target.data(), target.size()); + + // Open the file to send back. + std::ifstream is(full_path.c_str(), std::ios::in | std::ios::binary); + if (!is) + { + response_.result(http::status::not_found); + response_.set(http::field::content_type, "text/plain"); + response_.body = "File not found\r\n"; + return; + } + + // Fill out the reply to be sent to the client. + response_.set(http::field::content_type, mime_type(target.to_string())); + response_.body.clear(); + for (char buf[512]; is.read(buf, sizeof(buf)).gcount() > 0;) + response_.body.append(buf, static_cast(is.gcount())); + response_.prepare_payload(); + } + + void write_response() + { + response_.set(http::field::content_length, response_.body.size()); + + serializer_.emplace(response_); + + http::async_write( + socket_, + *serializer_, + [this](beast::error_code ec) + { + socket_.shutdown(tcp::socket::shutdown_send, ec); + accept(); + }); + } + + void check_deadline() + { + // The deadline may have moved, so check it has really passed. + if (request_deadline_.expires_at() <= std::chrono::steady_clock::now()) + { + // Close socket to cancel any outstanding operation. + beast::error_code ec; + socket_.close(); + + // Sleep indefinitely until we're given a new deadline. + request_deadline_.expires_at( + std::chrono::steady_clock::time_point::max()); + } + + request_deadline_.async_wait( + [this](beast::error_code) + { + check_deadline(); + }); + } +}; + +int main(int argc, char* argv[]) +{ + try + { + // Check command line arguments. + if (argc != 5) + { + std::cerr << "Usage: http_server
\n"; + std::cerr << " For IPv4, try:\n"; + std::cerr << " receiver 0.0.0.0 80 . 100\n"; + std::cerr << " For IPv6, try:\n"; + std::cerr << " receiver 0::0 80 . 100\n"; + return EXIT_FAILURE; + } + + auto address = ip::address::from_string(argv[1]); + unsigned short port = static_cast(std::atoi(argv[2])); + std::string doc_root = argv[3]; + int num_workers = std::atoi(argv[4]); + + boost::asio::io_service ios{1}; + tcp::acceptor acceptor{ios, {address, port}}; + + std::list workers; + for (int i = 0; i < num_workers; ++i) + { + workers.emplace_back(acceptor, doc_root); + workers.back().start(); + } + + ios.run(); + } + catch (const std::exception& e) + { + std::cerr << "Exception: " << e.what() << std::endl; + return EXIT_FAILURE; + } +}