aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/server
diff options
context:
space:
mode:
authorEgor Tensin <Egor.Tensin@gmail.com>2019-11-30 01:38:08 +0300
committerEgor Tensin <Egor.Tensin@gmail.com>2019-12-01 05:23:11 +0300
commit90bd600c5025ede4db99122f13dfb07b27de46ae (patch)
tree0b581b43e5fcb54114c6c373352ed6ab5fcd61dc /server
downloadmath-server-90bd600c5025ede4db99122f13dfb07b27de46ae.tar.gz
math-server-90bd600c5025ede4db99122f13dfb07b27de46ae.zip
initial commit
Diffstat (limited to '')
-rw-r--r--server/CMakeLists.txt11
-rw-r--r--server/error.hpp15
-rw-r--r--server/lexer.hpp261
-rw-r--r--server/log.hpp49
-rw-r--r--server/main.cpp34
-rw-r--r--server/parser.hpp168
-rw-r--r--server/server.cpp111
-rw-r--r--server/server.hpp34
-rw-r--r--server/session.cpp113
-rw-r--r--server/session.hpp42
-rw-r--r--server/session_manager.cpp37
-rw-r--r--server/session_manager.hpp30
-rw-r--r--server/settings.hpp87
13 files changed, 992 insertions, 0 deletions
diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt
new file mode 100644
index 0000000..7bf356a
--- /dev/null
+++ b/server/CMakeLists.txt
@@ -0,0 +1,11 @@
+find_package(Boost REQUIRED COMPONENTS filesystem program_options)
+
+option(DEBUG_ASIO "enable debug output for Boost.Asio" OFF)
+
+add_executable(server main.cpp server.cpp session.cpp session_manager.cpp)
+target_include_directories(server SYSTEM PRIVATE ${Boost_INCLUDE_DIRS})
+target_link_libraries(server PRIVATE ${Boost_LIBRARIES})
+
+if(DEBUG_ASIO)
+ target_compile_definitions(server PRIVATE BOOST_ASIO_ENABLE_HANDLER_TRACKING)
+endif()
diff --git a/server/error.hpp b/server/error.hpp
new file mode 100644
index 0000000..cbfbb1e
--- /dev/null
+++ b/server/error.hpp
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <stdexcept>
+#include <string>
+
+namespace math::server {
+
+class Error : public std::runtime_error {
+public:
+ explicit Error(const std::string& what)
+ : std::runtime_error{"server error: " + what}
+ { }
+};
+
+}
diff --git a/server/lexer.hpp b/server/lexer.hpp
new file mode 100644
index 0000000..8afe15c
--- /dev/null
+++ b/server/lexer.hpp
@@ -0,0 +1,261 @@
+#pragma once
+
+#include "error.hpp"
+
+#include <cmath>
+
+#include <exception>
+#include <functional>
+#include <limits>
+#include <optional>
+#include <regex>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+namespace math::server {
+namespace lexer {
+
+class Error : public server::Error {
+public:
+ explicit Error(const std::string &what)
+ : server::Error{"lexer error: " + what}
+ { }
+};
+
+class Token {
+public:
+ enum class Type {
+ LEFT_PAREN,
+ RIGHT_PAREN,
+ PLUS,
+ MINUS,
+ ASTERISK,
+ SLASH,
+ NUMBER,
+ };
+
+ explicit Token(Type type)
+ : m_type{type}, m_number_value{nan()}
+ { }
+
+ explicit Token(double number_value)
+ : m_type{Type::NUMBER}, m_number_value{number_value}
+ { }
+
+ bool operator==(const Token& other) const {
+ return m_type == other.m_type
+ && ((is_nan(m_number_value) && is_nan(other.m_number_value))
+ || m_number_value == other.m_number_value);
+ }
+
+ bool operator!=(const Token& other) const { return !(*this == other); }
+
+ Type get_type() const { return m_type; }
+
+ double get_number_value() const {
+ if (get_type() != Type::NUMBER) {
+ throw Error{"token must be a number to query its value"};
+ }
+ return m_number_value;
+ }
+
+private:
+ static constexpr double nan() { return std::numeric_limits<double>::quiet_NaN(); }
+
+ static bool is_nan(double x) { return std::isnan(x); }
+
+ Type m_type;
+ double m_number_value;
+};
+
+namespace details {
+
+inline std::string_view match_number(const std::string_view& input) {
+ static constexpr std::regex::flag_type flags =
+ std::regex_constants::ECMAScript |
+ std::regex_constants::icase;
+ // This is a hacky attempt to describe a C-like grammar for floating-point
+ // numbers using a regex (the tests seem to pass though).
+ // A proper NFA would be better, I guess.
+ static const std::regex number_regex{R"REGEX(^(?:\d+(?:\.\d*)?|\.\d+)(e[+-]?(\d*))?)REGEX", flags};
+
+ std::cmatch match;
+ if (!std::regex_search(input.cbegin(), input.cend(), match, number_regex)) {
+ return {};
+ }
+ // If we have the numeric part of a number followed by 'e' and no digits,
+ // 1) that 'e' definitely belongs to this number token,
+ // 2) the user forgot to type in the required digits.
+ const auto& exponent = match[1];
+ const auto& abs_power = match[2];
+ if (exponent.matched && abs_power.matched && abs_power.length() == 0) {
+ throw lexer::Error{"exponent has no digits: " + match[0].str()};
+ }
+ return {match[0].first, match[0].length()};
+}
+
+inline std::optional<double> parse_number(const std::string_view& input, std::string_view& token) {
+ const auto view = match_number(input);
+ if (!view.data()) {
+ return {};
+ }
+ try {
+ const auto result = std::stod(std::string{view});
+ token = view;
+ return result;
+ } catch (const std::exception& e) {
+ throw lexer::Error{"couldn't parse number from: " + std::string{view}};
+ }
+ return {};
+}
+
+inline std::optional<double> parse_number(const std::string_view& input) {
+ std::string_view token;
+ return parse_number(input, token);
+}
+
+inline bool starts_with(const std::string_view& a, const std::string_view& b) noexcept {
+ return a.length() >= b.length()
+ && a.compare(0, b.length(), b) == 0;
+}
+
+inline std::optional<Token::Type> parse_const_token(const std::string_view& input, std::string_view& token) {
+ // FIXME: Potentially error-prone if there's const token A which is a
+ // prefix of token B (if the map is not sorted, we'd parse token A, when it
+ // could've been token B).
+ // Can be solved by sorting the keys accordingly.
+
+ static const std::unordered_map<std::string_view, Token::Type> const_tokens{
+ {"(", Token::Type::LEFT_PAREN},
+ {")", Token::Type::RIGHT_PAREN},
+ {"+", Token::Type::PLUS},
+ {"-", Token::Type::MINUS},
+ {"*", Token::Type::ASTERISK},
+ {"/", Token::Type::SLASH},
+ };
+
+ for (const auto& it : const_tokens) {
+ const auto& str = it.first;
+ const auto& type = it.second;
+
+ if (starts_with(input, str)) {
+ token = input.substr(0, str.length());
+ return type;
+ }
+ }
+
+ return {};
+}
+
+inline std::optional<Token::Type> parse_const_token(const std::string_view& input) {
+ std::string_view token;
+ return parse_const_token(input, token);
+}
+
+inline std::string_view parse_whitespace(const std::string_view& input) {
+ static const std::regex ws_regex{R"(\s*)"};
+
+ std::cmatch match;
+ if (std::regex_search(input.cbegin(), input.cend(), match, ws_regex)) {
+ return {match[0].first, match[0].length()};
+ }
+ return {};
+}
+
+}
+
+}
+
+class Lexer {
+public:
+ explicit Lexer(const std::string_view& input)
+ : m_input{input} {
+ }
+
+ using TokenProcessor = std::function<bool (const lexer::Token&)>;
+
+ bool for_each_token(const TokenProcessor& process) {
+ parse_token();
+ for (auto token = peek_token(); token.has_value(); drop_token(), token = peek_token()) {
+ if (!process(*token)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ std::vector<lexer::Token> get_tokens() {
+ std::vector<lexer::Token> tokens;
+ for_each_token([&tokens] (const lexer::Token& token) {
+ tokens.emplace_back(token);
+ return true;
+ });
+ return tokens;
+ }
+
+ void parse_token() {
+ if (m_input.length() == 0) {
+ return;
+ }
+ std::string_view token_view;
+ m_token_buffer = parse_token(token_view);
+ if (m_token_buffer.has_value()) {
+ m_input.remove_prefix(token_view.length());
+ }
+ }
+
+ bool has_token() const {
+ return peek_token().has_value();
+ }
+
+ std::optional<lexer::Token> peek_token() const {
+ return m_token_buffer;
+ }
+
+ void drop_token() {
+ if (!has_token()) {
+ throw lexer::Error{"internal: no tokens to drop"};
+ }
+ m_token_buffer = {};
+ parse_token();
+ }
+
+ std::optional<lexer::Token> drop_token_if(lexer::Token::Type type) {
+ if (!has_token()) {
+ throw lexer::Error{"internal: no tokens to drop"};
+ }
+ if (m_token_buffer.value().get_type() != type) {
+ return {};
+ }
+ const auto result = m_token_buffer;
+ drop_token();
+ return result;
+ }
+
+private:
+ void consume_whitespace() {
+ const auto ws = lexer::details::parse_whitespace(m_input);
+ m_input.remove_prefix(ws.length());
+ }
+
+ std::optional<lexer::Token> parse_token(std::string_view& token_view) {
+ consume_whitespace();
+ if (m_input.length() == 0) {
+ return {};
+ }
+ if (const auto const_token = lexer::details::parse_const_token(m_input, token_view); const_token.has_value()) {
+ return lexer::Token{*const_token};
+ }
+ if (const auto number = lexer::details::parse_number(m_input, token_view); number.has_value()) {
+ return lexer::Token{*number};
+ }
+ throw lexer::Error{"invalid input at: " + std::string{m_input}};
+ }
+
+ std::string_view m_input;
+ std::optional<lexer::Token> m_token_buffer;
+};
+
+}
diff --git a/server/log.hpp b/server/log.hpp
new file mode 100644
index 0000000..ca0fafd
--- /dev/null
+++ b/server/log.hpp
@@ -0,0 +1,49 @@
+#pragma once
+
+#include <boost/format.hpp>
+#include <boost/system/error_code.hpp>
+
+#include <ctime>
+
+#include <iomanip>
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <string_view>
+#include <thread>
+#include <utility>
+
+namespace math::server::log {
+
+namespace details {
+
+inline std::thread::id get_tid() { return std::this_thread::get_id(); }
+
+inline std::string get_timestamp() {
+ const auto tt = std::time(nullptr);
+ std::ostringstream oss;
+ oss << std::put_time(std::gmtime(&tt), "%Y-%m-%d %H:%M:%S");
+ return oss.str();
+}
+
+inline void log(const std::string& msg) {
+ std::clog << get_timestamp() << " | " << get_tid() << " | " << msg << '\n';
+}
+
+}
+
+template <typename... Args>
+inline void log(const std::string_view& fmt, Args&&... args) {
+ details::log(boost::str((boost::format(fmt.data()) % ... % args)));
+}
+
+template <typename... Args>
+inline void error(const std::string_view& fmt, Args&&... args) {
+ details::log(boost::str((boost::format(fmt.data()) % ... % args)));
+}
+
+inline void error(const boost::system::error_code& ec) {
+ details::log(ec.message());
+}
+
+}
diff --git a/server/main.cpp b/server/main.cpp
new file mode 100644
index 0000000..2cf6d35
--- /dev/null
+++ b/server/main.cpp
@@ -0,0 +1,34 @@
+#include "server.hpp"
+#include "settings.hpp"
+
+#include <boost/program_options.hpp>
+
+#include <exception>
+#include <iostream>
+
+int main(int argc, char* argv[]) {
+ try {
+ math::server::SettingsParser parser{argv[0]};
+
+ try {
+ const auto settings = parser.parse(argc, argv);
+ if (settings.exit_with_usage()) {
+ parser.usage();
+ return 0;
+ }
+
+ math::server::Server server{settings};
+ server.run();
+ } catch (const boost::program_options::error& e) {
+ parser.usage_error(e);
+ return 1;
+ }
+ } catch (const std::exception& e) {
+ std::cerr << "An error occured: " << e.what() << "\n";
+ return 1;
+ } catch (...) {
+ std::cerr << "An unknown error occured\n";
+ return 1;
+ }
+ return 0;
+}
diff --git a/server/parser.hpp b/server/parser.hpp
new file mode 100644
index 0000000..a9e5f54
--- /dev/null
+++ b/server/parser.hpp
@@ -0,0 +1,168 @@
+#pragma once
+
+#include "error.hpp"
+#include "lexer.hpp"
+
+#include <optional>
+#include <string>
+#include <string_view>
+
+namespace math::server {
+namespace parser {
+
+class Error : public server::Error {
+public:
+ explicit Error(const std::string& what)
+ : server::Error{"parser error: " + what}
+ { }
+};
+
+class BinaryOp {
+public:
+ static bool is(const lexer::Token& token) {
+ using Type = lexer::Token::Type;
+ switch (token.get_type()) {
+ case Type::PLUS:
+ case Type::MINUS:
+ case Type::ASTERISK:
+ case Type::SLASH:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ static BinaryOp from_token(const lexer::Token& token) {
+ if (!is(token)) {
+ throw Error{"internal: token is not a binary operator"};
+ }
+ return BinaryOp{token};
+ }
+
+ static constexpr unsigned min_precedence() { return 0; }
+
+ unsigned get_precedence() const {
+ using Type = lexer::Token::Type;
+ switch (m_type) {
+ case Type::PLUS:
+ case Type::MINUS:
+ return min_precedence();
+
+ case Type::ASTERISK:
+ case Type::SLASH:
+ return min_precedence() + 1;
+
+ default:
+ throw Error{"internal: undefined operator precedence"};
+ }
+ }
+
+ double exec(double lhs, double rhs) const {
+ using Type = lexer::Token::Type;
+ switch (m_type) {
+ case Type::PLUS:
+ return lhs + rhs;
+ case Type::MINUS:
+ return lhs - rhs;
+ case Type::ASTERISK:
+ return lhs * rhs;
+ case Type::SLASH:
+ // Trapping the CPU would be better?
+ if (rhs == 0.) {
+ throw Error{"division by zero"};
+ }
+ return lhs / rhs;
+ default:
+ throw Error{"internal: unsupported operator"};
+ }
+ }
+
+private:
+ explicit BinaryOp(const lexer::Token& token)
+ : m_type{token.get_type()}
+ { }
+
+ lexer::Token::Type m_type;
+};
+
+}
+
+class Parser {
+public:
+ // I did simple recursive descent parsing a long time ago (see
+ // https://github.com/egor-tensin/simple-interpreter), this appears to be
+ // a finer algorithm for parsing arithmetic expressions.
+ // Reference: https://en.wikipedia.org/wiki/Operator-precedence_parser
+
+ explicit Parser(const std::string_view& input)
+ : m_lexer{input}
+ { }
+
+ double exec() {
+ m_lexer.parse_token();
+ const auto result = exec_expr();
+ if (m_lexer.has_token()) {
+ throw parser::Error{"expected a binary operator"};
+ }
+ return result;
+ }
+
+private:
+ double exec_expr() {
+ return exec_expr(exec_primary(), parser::BinaryOp::min_precedence());
+ }
+
+ double exec_expr(double lhs, unsigned min_prec) {
+ for (auto op = peek_operator(); op.has_value() && op->get_precedence() >= min_prec;) {
+ const auto lhs_op = *op;
+ m_lexer.drop_token();
+ auto rhs = exec_primary();
+
+ for (op = peek_operator(); op.has_value() && op->get_precedence() > lhs_op.get_precedence(); op = peek_operator()) {
+ rhs = exec_expr(rhs, op->get_precedence());
+ }
+
+ lhs = lhs_op.exec(lhs, rhs);
+ }
+ return lhs;
+ }
+
+ std::optional<parser::BinaryOp> peek_operator() {
+ const auto token = m_lexer.peek_token();
+ if (!token.has_value() || !parser::BinaryOp::is(*token)) {
+ return {};
+ }
+ return parser::BinaryOp::from_token(*token);
+ }
+
+ double exec_primary() {
+ if (!m_lexer.has_token()) {
+ throw parser::Error{"expected '-', '(' or a number"};
+ }
+
+ using Type = lexer::Token::Type;
+
+ if (m_lexer.drop_token_if(Type::MINUS).has_value()) {
+ return -exec_primary();
+ }
+
+ if (m_lexer.drop_token_if(Type::LEFT_PAREN).has_value()) {
+ const auto inner = exec_expr();
+ if (!m_lexer.has_token() || !m_lexer.drop_token_if(Type::RIGHT_PAREN).has_value()) {
+ throw parser::Error{"missing closing ')'"};
+ }
+ return inner;
+ }
+
+ if (const auto token = m_lexer.drop_token_if(Type::NUMBER); token.has_value()) {
+ return token.value().get_number_value();
+ }
+
+ throw parser::Error{"expected '-', '(' or a number"};
+ }
+
+ Lexer m_lexer;
+};
+
+}
diff --git a/server/server.cpp b/server/server.cpp
new file mode 100644
index 0000000..4dc672c
--- /dev/null
+++ b/server/server.cpp
@@ -0,0 +1,111 @@
+#include "error.hpp"
+#include "log.hpp"
+#include "server.hpp"
+#include "session.hpp"
+#include "session_manager.hpp"
+#include "settings.hpp"
+
+#include <boost/asio.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/system/system_error.hpp>
+
+#include <cstddef>
+
+#include <exception>
+#include <thread>
+#include <vector>
+
+namespace math::server {
+namespace {
+
+boost::asio::ip::tcp::endpoint make_endpoint(unsigned port) {
+ return {boost::asio::ip::tcp::v4(), port};
+}
+
+void configure_acceptor(boost::asio::ip::tcp::acceptor& acceptor, unsigned port) {
+ try {
+ const auto endpoint = make_endpoint(port);
+ acceptor.open(endpoint.protocol());
+ acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
+ acceptor.bind(endpoint);
+ acceptor.listen();
+ } catch (const boost::system::system_error& e) {
+ throw Error{e.what()};
+ }
+}
+
+}
+
+Server::Server(const Settings& settings)
+ : Server{settings.m_port, settings.m_threads}
+{ }
+
+Server::Server(unsigned port, unsigned threads)
+ : m_numof_threads{threads}
+ , m_signals{m_io_context}
+ , m_acceptor{m_io_context} {
+
+ wait_for_signal();
+ configure_acceptor(m_acceptor, port);
+
+ accept();
+}
+
+void Server::run() {
+ std::vector<std::thread> threads{m_numof_threads};
+ for (std::size_t i = 0; i < m_numof_threads; ++i) {
+ threads[i] = std::thread{[this] () { m_io_context.run(); }};
+ }
+
+ for (std::size_t i = 0; i < m_numof_threads; ++i) {
+ threads[i].join();
+ }
+}
+
+void Server::wait_for_signal() {
+ try {
+ m_signals.add(SIGINT);
+ m_signals.add(SIGTERM);
+
+ m_signals.async_wait([this] (const boost::system::error_code& ec, int signo) {
+ handle_signal(ec, signo);
+ });
+ } catch (const boost::system::system_error& e) {
+ throw Error{e.what()};
+ }
+}
+
+void Server::handle_signal(const boost::system::error_code& ec, int signo) {
+ if (ec) {
+ log::error("%1%: %2%", __func__, ec.message());
+ }
+
+ log::log("Caught signal %1%", signo);
+
+ try {
+ m_acceptor.close();
+ m_session_mgr.stop_all();
+ } catch (const std::exception& e) {
+ log::error(e.what());
+ }
+}
+
+void Server::accept() {
+ const auto session = m_session_mgr.make_session(m_io_context);
+ m_acceptor.async_accept(session->socket(),
+ [session, this] (const boost::system::error_code& ec) {
+ handle_accept(session, ec);
+ });
+}
+
+void Server::handle_accept(SessionPtr session, const boost::system::error_code& ec) {
+ if (ec) {
+ log::error("%1%: %2%", __func__, ec.message());
+ return;
+ }
+
+ m_session_mgr.start(session);
+ accept();
+}
+
+}
diff --git a/server/server.hpp b/server/server.hpp
new file mode 100644
index 0000000..5524f88
--- /dev/null
+++ b/server/server.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "session_manager.hpp"
+#include "settings.hpp"
+
+#include <boost/asio.hpp>
+#include <boost/system/error_code.hpp>
+
+namespace math::server {
+
+class Server {
+public:
+ Server(const Settings& settings);
+ Server(unsigned port, unsigned threads);
+
+ void run();
+
+private:
+ void wait_for_signal();
+ void handle_signal(const boost::system::error_code&, int);
+
+ void accept();
+ void handle_accept(SessionPtr session, const boost::system::error_code& ec);
+
+ const unsigned m_numof_threads;
+
+ boost::asio::io_context m_io_context;
+ boost::asio::signal_set m_signals;
+ boost::asio::ip::tcp::acceptor m_acceptor;
+
+ SessionManager m_session_mgr;
+};
+
+}
diff --git a/server/session.cpp b/server/session.cpp
new file mode 100644
index 0000000..409ca5a
--- /dev/null
+++ b/server/session.cpp
@@ -0,0 +1,113 @@
+#include "error.hpp"
+#include "log.hpp"
+#include "parser.hpp"
+#include "session.hpp"
+#include "session_manager.hpp"
+
+#include <boost/asio.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/system/system_error.hpp>
+
+#include <cstddef>
+
+#include <exception>
+#include <string>
+#include <utility>
+
+namespace math::server {
+namespace {
+
+std::string reply_to_string(double result) {
+ return boost::lexical_cast<std::string>(result);
+}
+
+std::string calc_reply(const std::string& input) {
+ std::string reply;
+ try {
+ reply = reply_to_string(Parser{input}.exec());
+ } catch (const std::exception& e) {
+ reply = e.what();
+ }
+ return reply;
+}
+
+}
+
+Session::Session(SessionManager& mgr, boost::asio::io_context& io_context)
+ : m_session_mgr{mgr}, m_strand{io_context}, m_socket{io_context}
+{ }
+
+boost::asio::ip::tcp::socket& Session::socket() {
+ return m_socket;
+}
+
+void Session::start() {
+ read();
+}
+
+void Session::stop() {
+ close();
+}
+
+void Session::close() {
+ try {
+ m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
+ m_socket.close();
+ } catch (const boost::system::system_error& e) {
+ throw Error{e.what()};
+ }
+}
+
+void Session::read() {
+ const auto self = shared_from_this();
+
+ // Stop at LF
+ boost::asio::async_read_until(m_socket, m_buffer, '\n', boost::asio::bind_executor(m_strand,
+ [this, self] (const boost::system::error_code& ec, std::size_t bytes) {
+ handle_read(ec, bytes);
+ }));
+}
+
+void Session::handle_read(const boost::system::error_code& ec, std::size_t bytes) {
+ if (ec) {
+ log::error("%1%: %2%", __func__, ec.message());
+ m_session_mgr.stop(shared_from_this());
+ return;
+ }
+
+ write(calc_reply(consume_input(bytes)));
+}
+
+std::string Session::consume_input(std::size_t bytes) {
+ const auto data = boost::asio::buffer_cast<const char*>(m_buffer.data());
+ const std::string input{data, bytes - 1};
+ m_buffer.consume(bytes);
+ return input;
+}
+
+void Session::write(std::string output) {
+ const auto self = shared_from_this();
+
+ // Include CR (so that Windows' telnet client works)
+ output += "\r\n";
+
+ boost::asio::const_buffer buffer{output.c_str(), output.length()};
+
+ boost::asio::async_write(m_socket, std::move(buffer), boost::asio::bind_executor(m_strand,
+ [this, self] (const boost::system::error_code& ec, std::size_t bytes) {
+ handle_write(ec, bytes);
+ }));
+}
+
+void Session::handle_write(const boost::system::error_code& ec, std::size_t bytes) {
+ if (ec) {
+ log::error("%1%: %2%", __func__, ec.message());
+ m_session_mgr.stop(shared_from_this());
+ return;
+ }
+
+ read();
+}
+
+}
diff --git a/server/session.hpp b/server/session.hpp
new file mode 100644
index 0000000..ace3755
--- /dev/null
+++ b/server/session.hpp
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <boost/asio.hpp>
+#include <boost/system/error_code.hpp>
+
+#include <cstddef>
+
+#include <memory>
+#include <string>
+
+namespace math::server {
+
+class SessionManager;
+
+class Session : public std::enable_shared_from_this<Session> {
+public:
+ Session(SessionManager& mgr, boost::asio::io_context& io_context);
+
+ boost::asio::ip::tcp::socket& socket();
+
+ void start();
+ void stop();
+
+private:
+ void close();
+
+ void read();
+ void write(std::string);
+
+ void handle_read(const boost::system::error_code&, std::size_t);
+ void handle_write(const boost::system::error_code&, std::size_t);
+
+ std::string consume_input(std::size_t);
+
+ SessionManager& m_session_mgr;
+
+ boost::asio::io_context::strand m_strand;
+ boost::asio::ip::tcp::socket m_socket;
+ boost::asio::streambuf m_buffer;
+};
+
+}
diff --git a/server/session_manager.cpp b/server/session_manager.cpp
new file mode 100644
index 0000000..a42fca4
--- /dev/null
+++ b/server/session_manager.cpp
@@ -0,0 +1,37 @@
+#include "log.hpp"
+#include "session.hpp"
+#include "session_manager.hpp"
+
+#include <memory>
+#include <mutex>
+
+namespace math::server {
+
+SessionPtr SessionManager::make_session(boost::asio::io_context& io_context) {
+ return std::make_shared<Session>(*this, io_context);
+}
+
+void SessionManager::start(const SessionPtr& session) {
+ std::lock_guard<std::mutex> lck{m_mtx};
+ m_sessions.emplace(session);
+ session->start();
+}
+
+void SessionManager::stop(const SessionPtr& session) {
+ std::lock_guard<std::mutex> lck{m_mtx};
+ const auto removed = m_sessions.erase(session) > 0;
+ if (removed) {
+ session->stop();
+ }
+}
+
+void SessionManager::stop_all() {
+ std::lock_guard<std::mutex> lck{m_mtx};
+ log::log("Closing the remaining %1% session(s)...", m_sessions.size());
+ for (const auto& session : m_sessions) {
+ session->stop();
+ }
+ m_sessions.clear();
+}
+
+}
diff --git a/server/session_manager.hpp b/server/session_manager.hpp
new file mode 100644
index 0000000..f0bec0b
--- /dev/null
+++ b/server/session_manager.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <boost/asio.hpp>
+
+#include <mutex>
+#include <memory>
+#include <unordered_set>
+
+namespace math::server {
+
+class Session;
+using SessionPtr = std::shared_ptr<Session>;
+
+class SessionManager {
+public:
+ SessionManager() = default;
+
+ SessionPtr make_session(boost::asio::io_context&);
+
+ void start(const SessionPtr&);
+ void stop(const SessionPtr&);
+
+ void stop_all();
+
+private:
+ std::mutex m_mtx;
+ std::unordered_set<SessionPtr> m_sessions;
+};
+
+}
diff --git a/server/settings.hpp b/server/settings.hpp
new file mode 100644
index 0000000..310163f
--- /dev/null
+++ b/server/settings.hpp
@@ -0,0 +1,87 @@
+#pragma once
+
+#include <boost/filesystem.hpp>
+#include <boost/program_options.hpp>
+
+#include <exception>
+#include <iostream>
+#include <string>
+#include <thread>
+#include <vector>
+
+namespace math::server {
+
+struct Settings {
+ static constexpr unsigned DEFAULT_PORT = 18000;
+
+ static unsigned default_threads() { return std::thread::hardware_concurrency(); }
+
+ unsigned m_port;
+ unsigned m_threads;
+
+ bool exit_with_usage() const { return m_vm.count("help"); }
+
+ boost::program_options::variables_map m_vm;
+};
+
+class SettingsParser {
+public:
+ explicit SettingsParser(const std::string& argv0)
+ : m_prog_name{extract_filename(argv0)}
+ {
+ m_visible.add_options()
+ ("help,h",
+ "show this message and exit")
+ ("port,p",
+ boost::program_options::value(&m_settings.m_port)->default_value(Settings::DEFAULT_PORT),
+ "server port number")
+ ("threads,n",
+ boost::program_options::value(&m_settings.m_threads)->default_value(Settings::default_threads()),
+ "number of threads");
+ }
+
+ static const char* get_short_description() {
+ return "[-h|--help] [-p|--port] [-n|--threads]";
+ }
+
+ Settings parse(int argc, char* argv[]) {
+ boost::program_options::store(
+ boost::program_options::command_line_parser{argc, argv}
+ .options(m_visible)
+ .run(),
+ m_settings.m_vm);
+ if (m_settings.exit_with_usage()) {
+ return m_settings;
+ }
+ boost::program_options::notify(m_settings.m_vm);
+ return m_settings;
+ }
+
+ void usage() const {
+ std::cout << *this;
+ }
+
+ void usage_error(const std::exception& e) const {
+ std::cerr << "usage error: " << e.what() << '\n';
+ std::cerr << *this;
+ }
+
+private:
+ static std::string extract_filename(const std::string& path) {
+ return boost::filesystem::path{path}.filename().string();
+ }
+
+ const std::string m_prog_name;
+
+ boost::program_options::options_description m_visible;
+
+ Settings m_settings;
+
+ friend std::ostream& operator<<(std::ostream& os, const SettingsParser& parser) {
+ os << "usage: " << parser.m_prog_name << ' ' << get_short_description() << '\n';
+ os << parser.m_visible;
+ return os;
+ }
+};
+
+}