Add http-server example

This commit is contained in:
Vinnie Falco
2017-06-23 01:44:25 -07:00
parent 522d3bf378
commit e5f1d4d010
9 changed files with 555 additions and 1 deletions

View File

@ -9,6 +9,7 @@ Version 66:
* Tidy up message piecewise ctors
* Add header aliases
* basic_fields optimizations
* Add http-server example
--------------------------------------------------------------------------------

View File

@ -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]]]

View File

@ -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,

View File

@ -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)

View File

@ -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 ;

View File

@ -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}
)

View File

@ -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
:
<variant>coverage:<build>no
<variant>ubasan:<build>no
;

View File

@ -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 <boost/throw_exception.hpp>
#include <cstdlib>
#include <memory>
#include <stdexcept>
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<char*>(this+1) + size_;
}
explicit
static_pool(std::size_t size)
: size_(size)
, p_(reinterpret_cast<char*>(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<char*>(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<char*>(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<class T>
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<class U>
struct rebind
{
using other = fields_alloc<U>;
};
explicit
fields_alloc(std::size_t size)
: pool_(detail::static_pool::construct(size))
{
}
fields_alloc(fields_alloc const& other)
: pool_(other.pool_.share())
{
}
template<class U>
fields_alloc(fields_alloc<U> const& other)
: pool_(other.pool_.share())
{
}
~fields_alloc()
{
pool_.destroy();
}
value_type*
allocate(size_type n)
{
return static_cast<value_type*>(
pool_.alloc(n * sizeof(T)));
}
void
deallocate(value_type*, size_type)
{
pool_.dealloc();
}
#if defined(BOOST_LIBSTDCXX_VERSION) && BOOST_LIBSTDCXX_VERSION < 60000
template<class U, class... Args>
void
construct(U* ptr, Args&&... args)
{
::new((void*)ptr) U(std::forward<Args>(args)...);
}
template<class U>
void
destroy(U* ptr)
{
ptr->~U();
}
#endif
template<class U>
friend
bool
operator==(
fields_alloc const& lhs,
fields_alloc<U> const& rhs)
{
return &lhs.pool_ == &rhs.pool_;
}
template<class U>
friend
bool
operator!=(
fields_alloc const& lhs,
fields_alloc<U> const& rhs)
{
return ! (lhs == rhs);
}
};
#endif

View File

@ -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 <beast/core.hpp>
#include <beast/http.hpp>
#include <beast/version.hpp>
#include <boost/asio.hpp>
#include <boost/filesystem.hpp>
#include <chrono>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <list>
#include <memory>
#include <string>
namespace ip = boost::asio::ip; // from <boost/asio.hpp>
using tcp = boost::asio::ip::tcp; // from <boost/asio.hpp>
namespace http = beast::http; // from <beast/http.hpp>
// 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<beast::static_buffer_n<1024 * 1024>>;
// 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<char>;
alloc_type alloc_;
boost::optional<http::request_parser<request_body_t, alloc_type>> parser_;
// The timer putting a time limit on requests.
boost::asio::basic_waitable_timer<std::chrono::steady_clock> request_deadline_;
// The response message.
http::response<http::string_body> response_;
// The response serializer.
boost::optional<http::response_serializer<http::string_body>> 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<request_body_t, http::basic_fields<alloc_type>> 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<std::size_t>(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 <address> <port> <doc_root> <num_workers>\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<unsigned short>(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<http_worker> 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;
}
}