add request

This commit is contained in:
jie 2021-03-13 19:33:01 +08:00
parent a6f745a787
commit 64a9a3b59d
9 changed files with 339 additions and 22 deletions

View File

@ -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/<user>`|958.77|
|URL|bserv|Java Spring Boot|
|:-:|:-:|:-:|
|`/login`|139.55||
|`/find/<user>`|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:

View File

@ -22,13 +22,12 @@
#include <thread>
#include <chrono>
#include <pqxx/pqxx>
#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<memory_session>();
// io_context for all I/O
asio::io_context ioc{NUM_THREADS};
client_ptr = std::make_shared<client>(ioc);
// creates and launches a listening port
std::make_shared<listener>(

View File

@ -1,6 +0,0 @@
# Build
```
cmake ..
cmake --build .
```

283
client.hpp Normal file
View File

@ -0,0 +1,283 @@
#ifndef _CLIENT_HPP
#define _CLIENT_HPP
#include <boost/beast.hpp>
#include <boost/asio.hpp>
#include <boost/json/src.hpp>
#include <iostream>
#include <string>
#include <cstddef>
#include <future>
#include <memory>
#include <chrono>
#include <exception>
#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 <typename ResponseType>
class client_session
: public std::enable_shared_from_this<
client_session<ResponseType>> {
private:
tcp::resolver resolver_;
beast::tcp_stream stream_;
// must persist between reads
beast::flat_buffer buffer_;
http::request<http::string_body> req_;
http::response<http::string_body> res_;
std::promise<ResponseType> 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<http::string_body>& req)
: resolver_{asio::make_strand(ioc)},
stream_{asio::make_strand(ioc)}, req_{req} {}
std::future<ResponseType> send(
const std::string& host,
const std::string& port) {
resolver_.async_resolve(
host, port,
beast::bind_front_handler(
&client_session::on_resolve,
client_session<ResponseType>::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<ResponseType>::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<ResponseType>::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<ResponseType>::shared_from_this()));
}
static_assert(std::is_same<ResponseType, http::response<http::string_body>>::value
|| std::is_same<ResponseType, boost::json::value>::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<ResponseType, http::response<http::string_body>>::value) {
promise_.set_value(std::move(res_));
} else if constexpr (std::is_same<ResponseType, boost::json::value>::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<http::response<http::string_body>> request(
const std::string& host,
const std::string& port,
const http::request<http::string_body>& req) {
return std::make_shared<
client_session<http::response<http::string_body>>
>(ioc_, req)->send(host, port);
}
std::future<boost::json::value> request_for_object(
const std::string& host,
const std::string& port,
const http::request<http::string_body>& req) {
return std::make_shared<
client_session<boost::json::value>
>(ioc_, req)->send(host, port);
}
};
std::shared_ptr<client> 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<response_type> 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<boost::json::value> 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<response_type> 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<boost::json::value> 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<response_type> 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<boost::json::value> 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<response_type> 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<boost::json::value> 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<response_type> 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<boost::json::value> 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

View File

@ -1,6 +1,7 @@
#ifndef _COMMON_HPP
#define _COMMON_HPP
#include "client.hpp"
#include "database.hpp"
#include "session.hpp"
#include "router.hpp"

View File

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

View File

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

View File

@ -24,7 +24,8 @@ bserv::router routes{
bserv::placeholders::session),
bserv::make_path("/find/<str>", &find_user,
bserv::placeholders::transaction,
bserv::placeholders::_1)
bserv::placeholders::_1),
bserv::make_path("/send", &send_request)
};
} // bserv

View File

@ -8,6 +8,8 @@
#include <map>
#include <random>
#include <mutex>
#include <sstream>
#include <iomanip>
#include <cryptopp/cryptlib.h>
#include <cryptopp/pwdbased.h>
@ -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,