diff --git a/CMakeLists.txt b/CMakeLists.txt index 03d9e5a..53b4201 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ set(CMAKE_CXX_FLAGS "-Wall -Wextra") set(CMAKE_CXX_FLAGS_DEBUG "-g") set(CMAKE_CXX_FLAGS_RELEASE "-O3") -add_executable(bserv bserv.cpp) +add_executable(bserv main.cpp) target_link_libraries(bserv pthread boost_thread diff --git a/client.hpp b/bserv/client.hpp similarity index 98% rename from client.hpp rename to bserv/client.hpp index f8203db..f00cbde 100644 --- a/client.hpp +++ b/bserv/client.hpp @@ -13,7 +13,7 @@ #include #include -#include "config.hpp" +#include "router.hpp" #include "logging.hpp" namespace bserv { @@ -77,7 +77,7 @@ public: return; } // sets a timeout on the operation - stream_.expires_after(std::chrono::seconds(30)); + stream_.expires_after(std::chrono::seconds(EXPIRY_TIME)); // makes the connection on the IP address we get from a lookup stream_.async_connect( results, @@ -93,7 +93,7 @@ public: return; } // sets a timeout on the operation - stream_.expires_after(std::chrono::seconds(30)); + stream_.expires_after(std::chrono::seconds(EXPIRY_TIME)); // sends the HTTP request to the remote host http::async_write( stream_, req_, diff --git a/common.hpp b/bserv/common.hpp similarity index 72% rename from common.hpp rename to bserv/common.hpp index 99b3141..e825308 100644 --- a/common.hpp +++ b/bserv/common.hpp @@ -2,10 +2,12 @@ #define _COMMON_HPP #include "client.hpp" +#include "config.hpp" #include "database.hpp" -#include "session.hpp" -#include "router.hpp" -#include "utils.hpp" #include "logging.hpp" +#include "router.hpp" +#include "server.hpp" +#include "session.hpp" +#include "utils.hpp" #endif // _COMMON_HPP \ No newline at end of file diff --git a/bserv/config.hpp b/bserv/config.hpp new file mode 100644 index 0000000..0074b41 --- /dev/null +++ b/bserv/config.hpp @@ -0,0 +1,48 @@ +#ifndef _CONFIG_HPP +#define _CONFIG_HPP + +#include +#include +#include +#include + +namespace bserv { + +const std::string NAME = "bserv"; + +const unsigned short PORT = 8080; +const int NUM_THREADS = 4; + +const std::size_t PAYLOAD_LIMIT = 1 * 1024 * 1024; +const int EXPIRY_TIME = 30; // seconds + +const std::size_t LOG_ROTATION_SIZE = 4 * 1024 * 1024; +const std::string LOG_PATH = "./log/" + NAME; + +const int NUM_DB_CONN = 10; +const std::string DB_CONN_STR = "dbname=bserv"; + +#define decl_field(type, name, default_value) \ +private: \ + std::optional name##_; \ +public: \ + void set_##name(std::optional&& name) { name##_ = std::move(name); } \ + type get_##name() const { return name##_.has_value() ? name##_.value() : default_value; } + +struct server_config { + decl_field(std::string, name, NAME) + decl_field(unsigned short, port, PORT) + decl_field(int, num_threads, NUM_THREADS) + decl_field(std::size_t, log_rotation_size, LOG_ROTATION_SIZE) + decl_field(std::string, log_path, LOG_PATH) + decl_field(int, num_db_conn, NUM_DB_CONN) + decl_field(std::string, db_conn_str, DB_CONN_STR) +public: + server_config() = default; +}; + +#undef decl_field + +} // bserv + +#endif // _CONFIG_HPP \ No newline at end of file diff --git a/database.hpp b/bserv/database.hpp similarity index 100% rename from database.hpp rename to bserv/database.hpp diff --git a/logging.hpp b/bserv/logging.hpp similarity index 76% rename from logging.hpp rename to bserv/logging.hpp index 253d4d3..b02bb71 100644 --- a/logging.hpp +++ b/bserv/logging.hpp @@ -8,6 +8,9 @@ #include #include +#include +#include + #include "config.hpp" namespace bserv { @@ -16,12 +19,11 @@ namespace logging = boost::log; namespace keywords = boost::log::keywords; namespace src = boost::log::sources; -// this function should be called in `main` -// right after the configurations are loaded. -void init_logging() { +// this function should be called before logging is used +void init_logging(const server_config& config) { logging::add_file_log( - keywords::file_name = LOG_PATH + NAME + "_%Y%m%d_%H-%M-%S.%N.log", - keywords::rotation_size = LOG_ROTATION_SIZE, + keywords::file_name = config.get_log_path() + "_%Y%m%d_%H-%M-%S.%N.log", + keywords::rotation_size = config.get_log_rotation_size(), keywords::format = "[%Severity%][%TimeStamp%][%ThreadID%]: %Message%" ); logging::core::get()->set_filter( diff --git a/router.hpp b/bserv/router.hpp similarity index 100% rename from router.hpp rename to bserv/router.hpp diff --git a/bserv.cpp b/bserv/server.hpp similarity index 72% rename from bserv.cpp rename to bserv/server.hpp index 8ab81f8..365cace 100644 --- a/bserv.cpp +++ b/bserv/server.hpp @@ -7,6 +7,9 @@ * */ +#ifndef _SERVER_HPP +#define _SERVER_HPP + #include #include #include @@ -25,8 +28,9 @@ #include "config.hpp" #include "logging.hpp" #include "utils.hpp" -#include "routing.hpp" +#include "router.hpp" #include "database.hpp" +#include "session.hpp" #include "client.hpp" namespace bserv { @@ -45,7 +49,7 @@ using asio::ip::tcp; template void handle_request( http::request>&& req, - Send&& send) { + Send&& send, router& routes) { const auto bad_request = [&req](beast::string_view why) { http::response res{ @@ -118,12 +122,12 @@ void handle_request( send(std::move(res)); } -// std::string get_address(const tcp::socket& socket) { -// tcp::endpoint end_point = socket.remote_endpoint(); -// std::string addr = end_point.address().to_string() -// + ':' + std::to_string(end_point.port()); -// return addr; -// } +std::string get_address(const tcp::socket& socket) { + tcp::endpoint end_point = socket.remote_endpoint(); + std::string addr = end_point.address().to_string() + + ':' + std::to_string(end_point.port()); + return addr; +} // handles an HTTP server connection class http_session @@ -162,7 +166,8 @@ private: boost::optional< http::request_parser> parser_; std::shared_ptr res_; - // const std::string address_; + router& routes_; + const std::string address_; void do_read() { // constructs a new parser for each message parser_.emplace(); @@ -170,7 +175,7 @@ private: // of the body in bytes to prevent abuse. parser_->body_limit(PAYLOAD_LIMIT); // sets the timeout. - stream_.expires_after(std::chrono::seconds(30)); + stream_.expires_after(std::chrono::seconds(EXPIRY_TIME)); // reads a request using the parser-oriented interface http::async_read( stream_, buffer_, *parser_, @@ -182,7 +187,7 @@ private: beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); - // lgtrace << "received " << bytes_transferred << " byte(s) from: " << address_; + lgtrace << "received " << bytes_transferred << " byte(s) from: " << address_; // this means they closed the connection if (ec == http::error::end_of_stream) { do_close(); @@ -193,7 +198,7 @@ private: return; } // handles the request and sends the response - handle_request(parser_->release(), lambda_); + handle_request(parser_->release(), lambda_, routes_); // at this point the parser can be reset } void on_write( @@ -206,7 +211,7 @@ private: fail(ec, "http_session async_write"); return; } - // lgtrace << "sent " << bytes_transferred << " byte(s) to: " << address_; + lgtrace << "sent " << bytes_transferred << " byte(s) to: " << address_; if (close) { // this means we should close the connection, usually because // the response indicated the "Connection: close" semantic. @@ -221,16 +226,16 @@ private: beast::error_code ec; stream_.socket().shutdown(tcp::socket::shutdown_send, ec); // at this point the connection is closed gracefully - // lgtrace << "socket connection closed: " << address_; + lgtrace << "socket connection closed: " << address_; } public: - http_session(tcp::socket&& socket) - : lambda_{*this}, stream_{std::move(socket)} /*, - address_{get_address(stream_.socket())} */ { - // lgtrace << "http session opened: " << address_; + http_session(tcp::socket&& socket, router& routes) + : lambda_{*this}, stream_{std::move(socket)}, routes_{routes}, + address_{get_address(stream_.socket())} { + lgtrace << "http session opened: " << address_; } ~http_session() { - // lgtrace << "http session closed: " << address_; + lgtrace << "http session closed: " << address_; } void run() { asio::dispatch( @@ -247,6 +252,7 @@ class listener private: asio::io_context& ioc_; tcp::acceptor acceptor_; + router& routes_; void do_accept() { acceptor_.async_accept( asio::make_strand(ioc_), @@ -258,39 +264,45 @@ private: if (ec) { fail(ec, "listener::acceptor async_accept"); } else { - // lgtrace << "listener accepts: " << get_address(socket); + lgtrace << "listener accepts: " << get_address(socket); std::make_shared( - std::move(socket))->run(); + std::move(socket), routes_)->run(); } do_accept(); } public: listener( asio::io_context& ioc, - tcp::endpoint endpoint) + tcp::endpoint endpoint, + router& routes) : ioc_{ioc}, - acceptor_{asio::make_strand(ioc)} { + acceptor_{asio::make_strand(ioc)}, + routes_{routes} { beast::error_code ec; acceptor_.open(endpoint.protocol(), ec); if (ec) { fail(ec, "listener::acceptor open"); + exit(EXIT_FAILURE); return; } acceptor_.set_option( asio::socket_base::reuse_address(true), ec); if (ec) { fail(ec, "listener::acceptor set_option"); + exit(EXIT_FAILURE); return; } acceptor_.bind(endpoint, ec); if (ec) { fail(ec, "listener::acceptor bind"); + exit(EXIT_FAILURE); return; } acceptor_.listen( asio::socket_base::max_listen_connections, ec); if (ec) { fail(ec, "listener::acceptor listen"); + exit(EXIT_FAILURE); return; } } @@ -303,69 +315,59 @@ public: } }; -void show_config() { - lginfo << NAME << " config:" - << "\nport: " << PORT - << "\nthreads: " << NUM_THREADS - << "\ndb-conn: " << NUM_DB_CONN - << "\npayload: " << PAYLOAD_LIMIT / 1024 / 1024 - << "\nrotation: " << LOG_ROTATION_SIZE / 1024 / 1024 - << "\nlog path: " << LOG_PATH - << "\nconn-str: " << DB_CONN_STR << std::endl; -} +class server { +private: + // io_context for all I/O + asio::io_context ioc_; + router routes_; +public: + server(const server_config& config, router&& routes) + : ioc_{config.get_num_threads()}, routes_{routes} { + init_logging(config); + + // database connection + try { + db_conn_mgr = std::make_shared< + db_connection_manager>(config.get_db_conn_str(), config.get_num_db_conn()); + } catch (const std::exception& e) { + lgfatal << "db connection initialization failed: " << e.what() << std::endl; + exit(EXIT_FAILURE); + } + session_mgr = std::make_shared(); + + client_ptr = std::make_shared(ioc_); + + // creates and launches a listening port + std::make_shared( + ioc_, tcp::endpoint{tcp::v4(), config.get_port()}, routes_)->run(); + + // captures SIGINT and SIGTERM to perform a clean shutdown + asio::signal_set signals{ioc_, SIGINT, SIGTERM}; + signals.async_wait( + [&](const boost::system::error_code&, int) { + // stops the `io_context`. This will cause `run()` + // to return immediately, eventually destroying the + // `io_context` and all of the sockets in it. + ioc_.stop(); + }); + + lginfo << config.get_name() << " started"; + + // runs the I/O service on the requested number of threads + std::vector v; + v.reserve(config.get_num_threads() - 1); + for (int i = 1; i < config.get_num_threads(); ++i) + v.emplace_back([&]{ ioc_.run(); }); + ioc_.run(); + + // if we get here, it means we got a SIGINT or SIGTERM + lginfo << "exiting " << config.get_name(); + + // blocks until all the threads exit + for (auto & t : v) t.join(); + } +}; } // bserv -int main(int argc, char* argv[]) { - using namespace bserv; - if (parse_arguments(argc, argv)) - return EXIT_FAILURE; - 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 { - db_conn_mgr = std::make_shared< - db_connection_manager>(DB_CONN_STR, NUM_DB_CONN); - } catch (const std::exception& e) { - lgfatal << "db connection initialization failed: " << e.what() << std::endl; - return EXIT_FAILURE; - } - session_mgr = std::make_shared(); - - client_ptr = std::make_shared(ioc); - - // creates and launches a listening port - std::make_shared( - ioc, tcp::endpoint{tcp::v4(), PORT})->run(); - - // captures SIGINT and SIGTERM to perform a clean shutdown - asio::signal_set signals{ioc, SIGINT, SIGTERM}; - signals.async_wait( - [&](const boost::system::error_code&, int) { - // stops the `io_context`. This will cause `run()` - // to return immediately, eventually destroying the - // `io_context` and all of the sockets in it. - ioc.stop(); - }); - - lginfo << NAME << " started"; - - // runs the I/O service on the requested number of threads - std::vector v; - v.reserve(NUM_THREADS - 1); - for (int i = 1; i < NUM_THREADS; ++i) - v.emplace_back([&]{ ioc.run(); }); - ioc.run(); - - // if we get here, it means we got a SIGINT or SIGTERM - lginfo << "exiting " << NAME; - - // blocks until all the threads exit - for (auto & t : v) t.join(); - return EXIT_SUCCESS; -} +#endif // _SERVER_HPP \ No newline at end of file diff --git a/session.hpp b/bserv/session.hpp similarity index 100% rename from session.hpp rename to bserv/session.hpp diff --git a/utils.hpp b/bserv/utils.hpp similarity index 100% rename from utils.hpp rename to bserv/utils.hpp diff --git a/config.hpp b/config.hpp deleted file mode 100644 index 924a8b2..0000000 --- a/config.hpp +++ /dev/null @@ -1,115 +0,0 @@ -#ifndef _CONFIG_HPP -#define _CONFIG_HPP - -#include -#include -#include -#include - -namespace bserv { - -const char* NAME = "bserv"; - -unsigned short PORT = 8080; -int NUM_THREADS = 4; -int NUM_DB_CONN = 10; - -std::size_t PAYLOAD_LIMIT = 1 * 1024 * 1024; - -std::size_t LOG_ROTATION_SIZE = 4 * 1024 * 1024; -std::string LOG_PATH = "./log/"; -std::string DB_CONN_STR = "dbname=bserv"; - -void show_usage() { - std::cout << "Usage: " << NAME << " [OPTION...]\n" - << NAME << " is a C++ Boost-based HTTP server.\n\n" - "Example:\n" - << " " << NAME << " -p 8081 --threads 2\n\n" - "Option:\n" - " -h, --help show help and exit\n" - " -p, --port port (default: 8080)\n" - " --threads number of threads (default: 4)\n" - " --num-conn number of database connections (default: 10)\n" - " --payload payload limit for request in mega bytes (default: 1)\n" - " --rotation log rotation size in mega bytes (default: 4)\n" - " --log-path log path (default: ./log/)\n" - " -c, --conn-str connection string (default: dbname=bserv)" - << std::endl; -} - -// returns `true` if error occurs -bool parse_arguments(int argc, char* argv[]) { - for (int i = 1; i < argc; ++i) { - if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { - show_usage(); - return true; - } else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) { - if (i + 1 < argc) { - PORT = atoi(argv[i + 1]); - ++i; - } else { - std::cerr << "Missing value after: " << argv[i] << std::endl; - return true; - } - } else if (strcmp(argv[i], "--threads") == 0) { - if (i + 1 < argc) { - NUM_THREADS = atoi(argv[i + 1]); - ++i; - } else { - std::cerr << "Missing value after: " << argv[i] << std::endl; - return true; - } - } else if (strcmp(argv[i], "--num-conn") == 0) { - if (i + 1 < argc) { - NUM_DB_CONN = atoi(argv[i + 1]); - ++i; - } else { - std::cerr << "Missing value after: " << argv[i] << std::endl; - return true; - } - } else if (strcmp(argv[i], "--payload") == 0) { - if (i + 1 < argc) { - PAYLOAD_LIMIT = atoi(argv[i + 1]) * 1024 * 1024; - ++i; - } else { - std::cerr << "Missing value after: " << argv[i] << std::endl; - return true; - } - } else if (strcmp(argv[i], "--rotation") == 0) { - if (i + 1 < argc) { - LOG_ROTATION_SIZE = atoi(argv[i + 1]) * 1024 * 1024; - ++i; - } else { - std::cerr << "Missing value after: " << argv[i] << std::endl; - return true; - } - } else if (strcmp(argv[i], "--log-path") == 0) { - if (i + 1 < argc) { - LOG_PATH = argv[i + 1]; - if (LOG_PATH.back() != '/') - LOG_PATH += '/'; - ++i; - } else { - std::cerr << "Missing value after: " << argv[i] << std::endl; - return true; - } - } else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--conn-str") == 0) { - if (i + 1 < argc) { - DB_CONN_STR = argv[i + 1]; - ++i; - } else { - std::cerr << "Missing value after: " << argv[i] << std::endl; - return true; - } - } else { - std::cerr << "Unrecognized option: " << argv[i] << '\n' << std::endl; - show_usage(); - return true; - } - } - return false; -} - -} // bserv - -#endif // _CONFIG_HPP \ No newline at end of file diff --git a/handlers.hpp b/handlers.hpp index 9fd353a..7fc10ed 100644 --- a/handlers.hpp +++ b/handlers.hpp @@ -10,7 +10,7 @@ #include -#include "common.hpp" +#include "bserv/common.hpp" // register an orm mapping (to convert the db query results into // json objects). diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..8e26114 --- /dev/null +++ b/main.cpp @@ -0,0 +1,126 @@ +#include +#include + +#include "bserv/common.hpp" +#include "handlers.hpp" + +void show_usage(const bserv::server_config& config) { + std::cout << "Usage: " << config.get_name() << " [OPTION...]\n" + << config.get_name() << " is a C++ Boost-based HTTP server.\n\n" + "Example:\n" + << " " << config.get_name() << " -p 8081 --threads 2\n\n" + "Option:\n" + " -h, --help show help and exit\n" + " -p, --port port (default: 8080)\n" + " --threads number of threads (default: 4)\n" + " --rotation log rotation size in mega bytes (default: 4)\n" + " --log-path log path (default: ./log/bserv)\n" + " --num-conn number of database connections (default: 10)\n" + " -c, --conn-str connection string (default: dbname=bserv)" + << std::endl; +} + +// returns `true` if error occurs +bool parse_arguments(int argc, char* argv[], bserv::server_config& config) { + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + show_usage(config); + return true; + } else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) { + if (i + 1 < argc) { + config.set_port(atoi(argv[i + 1])); + ++i; + } else { + std::cerr << "Missing value after: " << argv[i] << std::endl; + return true; + } + } else if (strcmp(argv[i], "--threads") == 0) { + if (i + 1 < argc) { + config.set_num_threads(atoi(argv[i + 1])); + ++i; + } else { + std::cerr << "Missing value after: " << argv[i] << std::endl; + return true; + } + } else if (strcmp(argv[i], "--num-conn") == 0) { + if (i + 1 < argc) { + config.set_num_db_conn(atoi(argv[i + 1])); + ++i; + } else { + std::cerr << "Missing value after: " << argv[i] << std::endl; + return true; + } + } else if (strcmp(argv[i], "--rotation") == 0) { + if (i + 1 < argc) { + config.set_log_rotation_size(atoi(argv[i + 1]) * 1024 * 1024); + ++i; + } else { + std::cerr << "Missing value after: " << argv[i] << std::endl; + return true; + } + } else if (strcmp(argv[i], "--log-path") == 0) { + if (i + 1 < argc) { + config.set_log_path(argv[i + 1]); + ++i; + } else { + std::cerr << "Missing value after: " << argv[i] << std::endl; + return true; + } + } else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--conn-str") == 0) { + if (i + 1 < argc) { + config.set_db_conn_str(argv[i + 1]); + ++i; + } else { + std::cerr << "Missing value after: " << argv[i] << std::endl; + return true; + } + } else { + std::cerr << "Unrecognized option: " << argv[i] << '\n' << std::endl; + show_usage(config); + return true; + } + } + return false; +} + +void show_config(const bserv::server_config& config) { + std::cout << config.get_name() << " config:" + << "\nport: " << config.get_port() + << "\nthreads: " << config.get_num_threads() + << "\nrotation: " << config.get_log_rotation_size() / 1024 / 1024 + << "\nlog path: " << config.get_log_path() + << "\ndb-conn: " << config.get_num_db_conn() + << "\nconn-str: " << config.get_db_conn_str() << std::endl; +} + +int main(int argc, char* argv[]) { + bserv::server_config config; + + if (parse_arguments(argc, argv, config)) + return EXIT_FAILURE; + + show_config(config); + + bserv::server{config, { + bserv::make_path("/", &hello, + bserv::placeholders::response, + bserv::placeholders::session), + bserv::make_path("/register", &user_register, + bserv::placeholders::request, + bserv::placeholders::json_params, + bserv::placeholders::transaction), + bserv::make_path("/login", &user_login, + bserv::placeholders::request, + bserv::placeholders::json_params, + bserv::placeholders::transaction, + bserv::placeholders::session), + bserv::make_path("/logout", &user_logout, + bserv::placeholders::session), + bserv::make_path("/find/", &find_user, + bserv::placeholders::transaction, + bserv::placeholders::_1), + bserv::make_path("/send", &send_request) + }}; + + return EXIT_SUCCESS; +} diff --git a/routing.hpp b/routing.hpp deleted file mode 100644 index 7b6d713..0000000 --- a/routing.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef _ROUTING_HPP -#define _ROUTING_HPP - -#include "router.hpp" - -#include "handlers.hpp" - -namespace bserv { - -bserv::router routes{ - bserv::make_path("/", &hello, - bserv::placeholders::response, - bserv::placeholders::session), - bserv::make_path("/register", &user_register, - bserv::placeholders::request, - bserv::placeholders::json_params, - bserv::placeholders::transaction), - bserv::make_path("/login", &user_login, - bserv::placeholders::request, - bserv::placeholders::json_params, - bserv::placeholders::transaction, - bserv::placeholders::session), - bserv::make_path("/logout", &user_logout, - bserv::placeholders::session), - bserv::make_path("/find/", &find_user, - bserv::placeholders::transaction, - bserv::placeholders::_1), - bserv::make_path("/send", &send_request) -}; - -} // bserv - -#endif // _ROUTING_HPP \ No newline at end of file