5 Commits

Author SHA1 Message Date
Ваше Имя 20640e766e refactor(cli): improve option handling with defaults and protected inheritance
Update OptionUtils to include name placeholders for default values, switch cli
classes to protected inheritance, and use at() for safer option retrieval.
2025-10-05 14:13:52 +04:00
Ваше Имя 75563091c1 feat(echo): add echo client and server with CLI support 2025-10-04 20:28:19 +04:00
Ваше Имя a4c84379ff chore: remove UDP server implementation and main example 2025-10-04 17:37:58 +04:00
Ваше Имя 827cc07175 feat(option_utils): add withDefault utility for Boost program options 2025-10-04 17:09:41 +04:00
Ваше Имя ba91359db3 feat(common): introduce common networking and CLI infrastructure 2025-09-30 18:33:06 +04:00
25 changed files with 569 additions and 26 deletions
+59
View File
@@ -0,0 +1,59 @@
#include "echo_client.hpp"
#include <boost/asio/connect.hpp>
EchoClient::EchoClient(IoContext &io, std::string host, unsigned short port)
: io_(io)
, host_(std::move(host))
, port_(port)
, socket_(io_)
{
}
bool EchoClient::run(const std::string &message)
{
if (!connect()) {
Logger::error("EchoClient: connect failed");
return false;
}
const bool ok = send(message);
close();
return ok;
}
bool EchoClient::connect()
{
try {
boost::asio::ip::tcp::resolver resolver(io_);
auto results = resolver.resolve(host_, std::to_string(port_));
boost::asio::connect(socket_, results);
Logger::info(
"EchoClient: connected to " + host_ + ":" + std::to_string(port_)
);
return true;
} catch (const std::exception &ex) {
Logger::error(
std::string("EchoClient: connect exception: ") + ex.what()
);
return false;
}
}
bool EchoClient::send(const std::string &message)
{
try {
auto bytes = boost::asio::write(socket_, boost::asio::buffer(message));
Logger::info("EchoClient: sent " + std::to_string(bytes) + " bytes");
return true;
} catch (const std::exception &ex) {
Logger::error(std::string("EchoClient: send exception: ") + ex.what());
return false;
}
}
void EchoClient::close()
{
ErrorCode ec;
socket_.shutdown(TcpSocket::shutdown_both, ec);
socket_.close(ec);
Logger::info("EchoClient: connection closed");
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "aliases.hpp"
#include "logger.hpp"
#include <string>
class EchoClient final
{
public:
EchoClient(IoContext &io, std::string host, unsigned short port);
bool run(const std::string &message);
private:
bool connect();
bool send(const std::string &message);
void close();
private:
IoContext &io_;
std::string host_;
unsigned short port_;
TcpSocket socket_{io_};
};
+34
View File
@@ -0,0 +1,34 @@
#include "echo_client_cli.hpp"
#include <boost/program_options/value_semantic.hpp>
EchoClientCli::EchoClientCli()
: CliBase("Echo Client Options")
, host_("127.0.0.1")
, port_(9000)
, message_("Hello from client")
{
}
void EchoClientCli::setupOptions()
{
// clang-format off
desc_.add_options()
("help,h", "Show help")
("host,H", withDefault(host_, "<HOST>"), "Server host (IP or DNS)")
("port,p", withDefault(port_, "<PORT>"), "Server port")
("message,m", withDefault(message_, "<TEXT>"), "Message to send");
// clang-format on
}
std::string EchoClientCli::getHost() const
{
return vm_.at("host").as<std::string>();
}
int EchoClientCli::getPort() const
{
return vm_.at("port").as<int>();
}
std::string EchoClientCli::getMessage() const
{
return vm_.at("message").as<std::string>();
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "cli_base.hpp"
#include "option_utils.hpp"
#include <string>
class EchoClientCli : public CliBase, protected OptionUtils
{
public:
EchoClientCli();
std::string getHost() const;
int getPort() const;
std::string getMessage() const;
protected:
void setupOptions() override;
private:
// defaults (used only to show defaults in help via withDefault)
std::string host_;
int port_;
std::string message_;
};
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <boost/asio.hpp>
#include <boost/program_options.hpp>
#include <boost/system/error_code.hpp>
// --- Error handling ---
using ErrorCode = boost::system::error_code;
// --- IO Context ---
using IoContext = boost::asio::io_context;
// --- TCP ---
using TcpSocket = boost::asio::ip::tcp::socket;
using TcpAcceptor = boost::asio::ip::tcp::acceptor;
using TcpEndpoint = boost::asio::ip::tcp::endpoint;
// --- UDP (для DNS) ---
using UdpSocket = boost::asio::ip::udp::socket;
using UdpEndpoint = boost::asio::ip::udp::endpoint;
// --- Таймеры ---
using SteadyTimer = boost::asio::steady_timer;
// --- Program options ---
using OptionsDescription = boost::program_options::options_description;
using VariablesMap = boost::program_options::variables_map;
+34
View File
@@ -0,0 +1,34 @@
#include "cli_base.hpp"
#include <boost/program_options/parsers.hpp>
#include <iostream>
CliBase::CliBase(const std::string &title)
: desc_(title)
, help_(false)
{
}
void CliBase::parse(int argc, char *argv[])
{
using namespace boost::program_options;
setupOptions(); // must be first
parsed_options parsed = parse_command_line(argc, argv, desc_);
store(parsed, vm_);
notify(vm_);
if (vm_.count("help")) {
help_ = true;
}
}
bool CliBase::isHelp() const
{
return help_;
}
void CliBase::printHelp() const
{
std::cout << desc_ << "\n";
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/variables_map.hpp>
#include <string>
class CliBase
{
public:
CliBase(const std::string &title);
virtual ~CliBase() = default;
// parse argc/argv
void parse(int argc, char *argv[]);
bool isHelp() const;
void printHelp() const;
protected:
// children add their specific options here
virtual void setupOptions() = 0;
boost::program_options::options_description desc_;
boost::program_options::variables_map vm_;
bool help_;
};
@@ -1,5 +1,4 @@
#include "logger.hpp" #include "logger.hpp"
#include <boost/date_time/posix_time/posix_time_io.hpp> #include <boost/date_time/posix_time/posix_time_io.hpp>
void Logger::init(bool toConsole, const std::string_view filePath) void Logger::init(bool toConsole, const std::string_view filePath)
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <boost/program_options.hpp>
class OptionUtils
{
public:
template <typename T>
using OptionValue = boost::program_options::typed_value<T> *;
template <typename T>
static OptionValue<T>
withDefault(const T &value, const char *name = nullptr);
protected:
OptionUtils() = default;
~OptionUtils() = default;
};
#include "option_utils.tpp"
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include "option_utils.hpp"
template <typename T>
OptionUtils::OptionValue<T>
OptionUtils::withDefault(const T &value, const char *name)
{
auto v = boost::program_options::value<T>()->default_value(value);
if (name) {
v->value_name(name); // Optional
}
return v;
}
+70
View File
@@ -0,0 +1,70 @@
#include "session_base.hpp"
#include "logger.hpp"
#include <boost/asio/write.hpp>
SessionBase::SessionBase(TcpSocket socket)
: socket_(std::move(socket))
{
}
void SessionBase::start()
{
doRead();
}
void SessionBase::doRead()
{
using namespace boost::asio;
auto self = shared_from_this();
auto handler = [this, self](ErrorCode ec, std::size_t bytesTransferred) {
onRead(ec, bytesTransferred);
};
socket_.async_read_some(buffer(buffer_), handler);
}
void SessionBase::onRead(ErrorCode ec, std::size_t bytesTransferred)
{
if (ec) {
Logger::error("Read error: " + ec.message());
return;
}
std::string rawRequest(buffer_.data(), bytesTransferred);
Logger::info("Received request:\n" + rawRequest);
try {
handleRequest(rawRequest);
} catch (const std::exception &ex) {
Logger::error(std::string("Request handling failed: ") + ex.what());
}
}
void SessionBase::doWrite(const std::string &response)
{
using namespace boost::asio;
auto self = shared_from_this();
auto onWriteHandler =
[this, self](ErrorCode ec, std::size_t /*bytesTransferred*/) {
if (ec) {
Logger::error("Write error: " + ec.message());
}
ErrorCode shutdownEc;
if (socket_.shutdown(TcpSocket::shutdown_both, shutdownEc)) {
Logger::warn("Shutdown error: " + shutdownEc.message());
}
ErrorCode closeEc;
if (socket_.close(closeEc)) {
Logger::warn("Close error: " + closeEc.message());
} else {
Logger::info("Connection closed");
}
};
async_write(socket_, buffer(response), onWriteHandler);
}
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include "aliases.hpp"
#include <array>
#include <memory>
#include <string>
class SessionBase : public std::enable_shared_from_this<SessionBase>
{
public:
explicit SessionBase(TcpSocket socket);
virtual ~SessionBase() = default;
void start();
protected:
virtual void handleRequest(const std::string_view rawRequest) = 0;
void doRead();
void onRead(ErrorCode ec, std::size_t bytesTransferred);
void doWrite(const std::string &response);
TcpSocket socket_;
std::array<char, 8192> buffer_;
};
+23
View File
@@ -0,0 +1,23 @@
#include "tcp_server_base.hpp"
#include "logger.hpp"
TcpServerBase::TcpServerBase(IoContext &io, unsigned short port)
: acceptor_(io, TcpEndpoint(boost::asio::ip::tcp::v4(), port))
{
}
void TcpServerBase::start()
{
auto doAccept = [this]() {
acceptor_.async_accept([this](ErrorCode ec, TcpSocket socket) {
if (!ec) {
Logger::info("New client connected");
createSession(std::move(socket));
} else {
Logger::error("Accept error: " + ec.message());
}
start(); // continue accepting
});
};
doAccept();
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include "aliases.hpp"
class TcpServerBase
{
public:
TcpServerBase(IoContext &io, unsigned short port);
virtual ~TcpServerBase() = default;
void start();
protected:
virtual void createSession(TcpSocket socket) = 0;
TcpAcceptor acceptor_;
};
-16
View File
@@ -1,16 +0,0 @@
#include <boost/asio.hpp>
#include <iostream>
int main()
{
std::cout << "Hello, Boost + C++23 + xmake!\n";
boost::asio::io_context io;
boost::asio::steady_timer timer(io, std::chrono::seconds(1));
timer.async_wait([](const boost::system::error_code &) {
std::cout << "Timer fired after 1 second!\n";
});
io.run();
return 0;
}
+26
View File
@@ -0,0 +1,26 @@
#include "aliases.hpp"
#include "echo_client.hpp"
#include "echo_client_cli.hpp"
#include "logger.hpp"
int main(int argc, char *argv[])
{
EchoClientCli cli;
cli.parse(argc, argv);
if (cli.isHelp()) {
cli.printHelp();
return 0;
}
Logger::init(true);
IoContext io;
const std::string host = cli.getHost();
const auto port = static_cast<unsigned short>(cli.getPort());
const std::string message = cli.getMessage();
EchoClient client(io, host, port);
const bool ok = client.run(message);
return ok ? 0 : 1;
}
+27
View File
@@ -0,0 +1,27 @@
#include "aliases.hpp"
#include "echo_server.hpp"
#include "echo_server_cli.hpp"
#include "logger.hpp"
int main(int argc, char *argv[])
{
EchoServerCli cli;
cli.parse(argc, argv);
if (cli.isHelp()) {
cli.printHelp();
return 0;
}
Logger::init(true);
IoContext io;
const auto port = static_cast<unsigned short>(cli.getPort());
EchoServer server(io, port);
Logger::info("EchoServer starting on port " + std::to_string(port));
server.start();
io.run();
return 0;
}
+8
View File
@@ -0,0 +1,8 @@
#include "echo_server.hpp"
#include "echo_session.hpp"
void EchoServer::createSession(TcpSocket socket)
{
auto session = std::make_shared<EchoSession>(std::move(socket));
session->start();
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include "tcp_server_base.hpp"
class EchoServer final : public TcpServerBase
{
public:
EchoServer(IoContext &io, unsigned short port)
: TcpServerBase(io, port)
{
}
protected:
void createSession(TcpSocket socket) override;
};
+27
View File
@@ -0,0 +1,27 @@
#include "echo_server_cli.hpp"
EchoServerCli::EchoServerCli()
: CliBase("Echo Server Options")
, port_(9000)
, address_("0.0.0.0")
{
}
void EchoServerCli::setupOptions()
{
// clang-format off
desc_.add_options()
("help,h", "Show help")
("port,p", withDefault(port_, "<PORT>"), "Port to listen on")
("address,a", withDefault(address_, "<ADDRESS>"), "Bind address (e.g. 0.0.0.0)");
// clang-format on
}
int EchoServerCli::getPort() const
{
return vm_.at("port").as<int>();
}
std::string EchoServerCli::getAddress() const
{
return vm_.at("address").as<std::string>();
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "cli_base.hpp"
#include "option_utils.hpp"
#include <string>
class EchoServerCli : public CliBase, protected OptionUtils
{
public:
EchoServerCli();
int getPort() const;
std::string getAddress() const;
protected:
void setupOptions() override;
private:
int port_;
std::string address_;
};
+15
View File
@@ -0,0 +1,15 @@
#include "echo_session.hpp"
#include "logger.hpp"
#include <utility> // std::move
EchoSession::EchoSession(TcpSocket socket)
: SessionBase(std::move(socket))
{
}
void EchoSession::handleRequest(const std::string_view rawRequest)
{
Logger::info(
std::string("EchoSession received: ") + std::string(rawRequest)
);
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "session_base.hpp"
class EchoSession final : public SessionBase
{
public:
explicit EchoSession(TcpSocket socket);
protected:
void handleRequest(const std::string_view rawRequest) override;
};
+21 -9
View File
@@ -2,16 +2,28 @@ set_languages "c++23"
add_rules("plugin.compile_commands.autoupdate", { outputdir = "." }) add_rules("plugin.compile_commands.autoupdate", { outputdir = "." })
add_requires("boost", { add_requires("cmake::Boost", {
system = true, -- sudo pacman -S boost alias = "boost",
system = true,
configs = { configs = {
all = false, components = { "program_options", "log" },
system = true, -- asio
}, },
}) })
target "app" -- Echo server target
set_kind "binary" target "echo_server"
add_files "src/*.cpp" do
add_links "boost_system" set_kind "binary"
add_files("src/main/echo_server_main.cpp", "src/server/*.cpp", "src/session/*.cpp", "src/common/*.cpp")
add_includedirs("src", "src/server", "src/session", "src/common", { public = true })
add_packages "boost"
end
-- Echo client target
target "echo_client"
do
set_kind "binary"
add_files("src/main/echo_client_main.cpp", "src/client/*.cpp", "src/session/*.cpp", "src/common/*.cpp")
add_includedirs("src", "src/client", "src/session", "src/common", { public = true })
add_packages "boost"
end