From 64a9a3b59dd43bfdf0d9f4318f957c234aea3849 Mon Sep 17 00:00:00 2001 From: jie Date: Sat, 13 Mar 2021 19:33:01 +0800 Subject: [PATCH] add request --- README.md | 13 +-- bserv.cpp | 13 +-- build/README.md | 6 - client.hpp | 283 ++++++++++++++++++++++++++++++++++++++++++++++++ common.hpp | 1 + handlers.hpp | 18 +++ logging.hpp | 4 + routing.hpp | 3 +- utils.hpp | 20 ++++ 9 files changed, 339 insertions(+), 22 deletions(-) delete mode 100644 build/README.md create mode 100644 client.hpp diff --git a/README.md b/README.md index 0ab404b..73bd119 100644 --- a/README.md +++ b/README.md @@ -54,15 +54,14 @@ Run in `shell`: ## Performance -This test is performed by Jmeter. +This test is performed by Jmeter. The unit for throughput is Transaction per second. -The unit for throughput is Transaction per second. +|URL|bserv| +|:-:|:-:| +|`/login`|139.55| +|`/find/`|958.77| - -|URL|bserv|Java Spring Boot| -|:-:|:-:|:-:| -|`/login`|139.55|| -|`/find/`|958.77|| +For `/login`, we must slow down the attacker's speed. In Java, plain password is stored, which results in a higher performance. ### Computer Hardware: diff --git a/bserv.cpp b/bserv.cpp index 9f2b38b..8ab81f8 100644 --- a/bserv.cpp +++ b/bserv.cpp @@ -22,13 +22,12 @@ #include #include -#include - #include "config.hpp" #include "logging.hpp" #include "utils.hpp" #include "routing.hpp" #include "database.hpp" +#include "client.hpp" namespace bserv { @@ -38,10 +37,6 @@ namespace asio = boost::asio; namespace json = boost::json; using asio::ip::tcp; -void fail(const beast::error_code& ec, const char* what) { - lgerror << what << ": " << ec.message() << std::endl; -} - // 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 @@ -328,6 +323,9 @@ int main(int argc, char* argv[]) { init_logging(); show_config(); + // io_context for all I/O + asio::io_context ioc{NUM_THREADS}; + // some initializations must be done after parsing the arguments // e.g. database connection try { @@ -339,8 +337,7 @@ int main(int argc, char* argv[]) { } session_mgr = std::make_shared(); - // io_context for all I/O - asio::io_context ioc{NUM_THREADS}; + client_ptr = std::make_shared(ioc); // creates and launches a listening port std::make_shared( diff --git a/build/README.md b/build/README.md deleted file mode 100644 index 8d91ad8..0000000 --- a/build/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Build - -``` -cmake .. -cmake --build . -``` diff --git a/client.hpp b/client.hpp new file mode 100644 index 0000000..f8203db --- /dev/null +++ b/client.hpp @@ -0,0 +1,283 @@ +#ifndef _CLIENT_HPP +#define _CLIENT_HPP + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "config.hpp" +#include "logging.hpp" + +namespace bserv { + +namespace beast = boost::beast; +namespace http = beast::http; +namespace asio = boost::asio; +namespace json = boost::json; +using asio::ip::tcp; + +class request_failed_exception + : public std::exception { +private: + std::string msg_; +public: + request_failed_exception(const std::string& msg) : msg_{msg} {} + const char* what() const noexcept { return msg_.c_str(); } +}; + +// https://www.boost.org/doc/libs/1_75_0/libs/beast/example/http/client/async/http_client_async.cpp + +// sends one async request to a remote server +template +class client_session + : public std::enable_shared_from_this< + client_session> { +private: + tcp::resolver resolver_; + beast::tcp_stream stream_; + // must persist between reads + beast::flat_buffer buffer_; + http::request req_; + http::response res_; + std::promise promise_; + void failed(const beast::error_code& ec, const std::string& what) { + promise_.set_exception( + std::make_exception_ptr( + request_failed_exception{what + ": " + ec.message()})); + } +public: + client_session( + asio::io_context& ioc, + const http::request& req) + : resolver_{asio::make_strand(ioc)}, + stream_{asio::make_strand(ioc)}, req_{req} {} + std::future send( + const std::string& host, + const std::string& port) { + resolver_.async_resolve( + host, port, + beast::bind_front_handler( + &client_session::on_resolve, + client_session::shared_from_this())); + return promise_.get_future(); + } + void on_resolve( + beast::error_code ec, + tcp::resolver::results_type results) { + if (ec) { + failed(ec, "client_session::resolver resolve"); + return; + } + // sets a timeout on the operation + stream_.expires_after(std::chrono::seconds(30)); + // makes the connection on the IP address we get from a lookup + stream_.async_connect( + results, + beast::bind_front_handler( + &client_session::on_connect, + client_session::shared_from_this())); + } + void on_connect( + beast::error_code ec, + tcp::resolver::results_type::endpoint_type) { + if (ec) { + failed(ec, "client_session::stream connect"); + return; + } + // sets a timeout on the operation + stream_.expires_after(std::chrono::seconds(30)); + // sends the HTTP request to the remote host + http::async_write( + stream_, req_, + beast::bind_front_handler( + &client_session::on_write, + client_session::shared_from_this())); + } + void on_write( + beast::error_code ec, + std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + if (ec) { + failed(ec, "client_session::stream write"); + return; + } + // receives the HTTP response + http::async_read( + stream_, buffer_, res_, + beast::bind_front_handler( + &client_session::on_read, + client_session::shared_from_this())); + } + static_assert(std::is_same>::value + || std::is_same::value, + "unsupported `ResponseType`"); + void on_read( + beast::error_code ec, + std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + if (ec) { + failed(ec, "client_session::stream read"); + return; + } + if constexpr (std::is_same>::value) { + promise_.set_value(std::move(res_)); + } else if constexpr (std::is_same::value) { + promise_.set_value(boost::json::parse(res_.body())); + } else { // this should never happen + promise_.set_exception( + std::make_exception_ptr( + request_failed_exception{"unsupported `ResponseType`"})); + } + // gracefully close the socket + stream_.socket().shutdown(tcp::socket::shutdown_both, ec); + // `not_connected` happens sometimes so don't bother reporting it + if (ec && ec != beast::errc::not_connected) { + // reports the error to the log! + fail(ec, "client_session::stream::socket shutdown"); + return; + } + // if we get here then the connection is closed gracefully + } +}; + +class client { +private: + asio::io_context& ioc_; +public: + client(asio::io_context& ioc) + : ioc_{ioc} {} + std::future> request( + const std::string& host, + const std::string& port, + const http::request& req) { + return std::make_shared< + client_session> + >(ioc_, req)->send(host, port); + } + std::future request_for_object( + const std::string& host, + const std::string& port, + const http::request& req) { + return std::make_shared< + client_session + >(ioc_, req)->send(host, port); + } +}; + +std::shared_ptr client_ptr; + +namespace request { + + request_type get_request( + const std::string& host, + const std::string& target, + const http::verb& method, + const boost::json::object& obj) { + request_type req; + req.method(method); + req.target(target); + req.set(http::field::host, host); + req.set(http::field::user_agent, NAME); + req.set(http::field::content_type, "application/json"); + req.body() = boost::json::serialize(obj); + req.prepare_payload(); + return req; + } + + std::future send( + const std::string& host, + const std::string& port, + const std::string& target, + const http::verb& method, + const boost::json::object& obj) { + request_type req = get_request(host, target, method, obj); + return client_ptr->request(host, port, req); + } + + std::future send_for_object( + const std::string& host, + const std::string& port, + const std::string& target, + const http::verb& method, + const boost::json::object& obj) { + request_type req = get_request(host, target, method, obj); + return client_ptr->request_for_object(host, port, req); + } + + std::future get( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::object& obj) { + return send(host, port, target, http::verb::get, obj); + } + + std::future get_for_object( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::object& obj) { + return send_for_object(host, port, target, http::verb::get, obj); + } + + std::future put( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::object& obj) { + return send(host, port, target, http::verb::put, obj); + } + + std::future put_for_object( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::object& obj) { + return send_for_object(host, port, target, http::verb::put, obj); + } + + std::future post( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::object& obj) { + return send(host, port, target, http::verb::post, obj); + } + + std::future post_for_object( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::object& obj) { + return send_for_object(host, port, target, http::verb::post, obj); + } + + std::future delete_( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::object& obj) { + return send(host, port, target, http::verb::delete_, obj); + } + + std::future delete_for_object( + const std::string& host, + const std::string& port, + const std::string& target, + const boost::json::object& obj) { + return send_for_object(host, port, target, http::verb::delete_, obj); + } + +} // request + +} // bserv + +#endif // _CLIENT_HPP \ No newline at end of file diff --git a/common.hpp b/common.hpp index ff810c1..99b3141 100644 --- a/common.hpp +++ b/common.hpp @@ -1,6 +1,7 @@ #ifndef _COMMON_HPP #define _COMMON_HPP +#include "client.hpp" #include "database.hpp" #include "session.hpp" #include "router.hpp" diff --git a/handlers.hpp b/handlers.hpp index 5224cf1..9fd353a 100644 --- a/handlers.hpp +++ b/handlers.hpp @@ -209,4 +209,22 @@ boost::json::object user_logout( }; } +boost::json::object send_request() { + // post for response: + // auto res = bserv::request::post( + // "localhost", "8081", "/test", {{"msg", "request"}} + // ).get(); + // return {{"response", boost::json::parse(res.body())}}; + // ------------------------------------------------------- + // - if it takes longer than 30 seconds (by default) to + // - get the response, this will raise a read timeout + // ------------------------------------------------------- + // post for json response (json value, rather than json + // object, is returned): + auto obj = bserv::request::post_for_object( + "localhost", "8081", "/test", {{"msg", "request"}} + ).get(); + return {{"response", obj}}; +} + #endif // _HANDLERS_HPP \ No newline at end of file diff --git a/logging.hpp b/logging.hpp index 24ecec1..253d4d3 100644 --- a/logging.hpp +++ b/logging.hpp @@ -37,6 +37,10 @@ void init_logging() { #define lgerror BOOST_LOG_TRIVIAL(error) #define lgfatal BOOST_LOG_TRIVIAL(fatal) +void fail(const boost::system::error_code& ec, const char* what) { + lgerror << what << ": " << ec.message() << std::endl; +} + } // bserv #endif // _LOGGING_HPP \ No newline at end of file diff --git a/routing.hpp b/routing.hpp index 1814ba9..7b6d713 100644 --- a/routing.hpp +++ b/routing.hpp @@ -24,7 +24,8 @@ bserv::router routes{ bserv::placeholders::session), bserv::make_path("/find/", &find_user, bserv::placeholders::transaction, - bserv::placeholders::_1) + bserv::placeholders::_1), + bserv::make_path("/send", &send_request) }; } // bserv diff --git a/utils.hpp b/utils.hpp index f135fe3..97a5d2f 100644 --- a/utils.hpp +++ b/utils.hpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include @@ -41,6 +43,11 @@ const std::string chars = "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "1234567890"; + +const std::string url_safe_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789-._~"; + } // internal // https://www.boost.org/doc/libs/1_75_0/libs/random/example/password.cpp @@ -132,6 +139,19 @@ std::string decode_url(const std::string& s) { return r; } +std::string encode_url(const std::string& s) { + std::ostringstream oss; + for (auto& c : s) { + if (internal::url_safe_characters.find(c) != std::string::npos) { + oss << c; + } else { + oss << '%' << std::setfill('0') << std::setw(2) << + std::uppercase << std::hex << (0xff & c); + } + } + return oss.str(); +} + // this function parses param list in the form of k1=v1&k2=v2..., // where '&' can be any delimiter. // ki and vi will be converted if they are percent-encoded,