4 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
20 changed files with 346 additions and 104 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_;
};
-1
View File
@@ -1,5 +1,4 @@
#include "logger.hpp"
#include <boost/date_time/posix_time/posix_time_io.hpp>
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;
}
-9
View File
@@ -1,9 +0,0 @@
#pragma once
class Server
{
public:
virtual ~Server() = default;
virtual void start() = 0;
virtual void stop() = 0;
};
-46
View File
@@ -1,46 +0,0 @@
#include "udp_server_base.hpp"
#include "logger.hpp"
UdpServerBase::UdpServerBase(IoContext &io, unsigned short port)
: socket_(io, UdpEndpoint(boost::asio::ip::udp::v4(), port))
{
}
void UdpServerBase::start()
{
doReceive();
}
void UdpServerBase::doReceive()
{
using namespace boost::asio;
auto onReceive = [this](ErrorCode ec, std::size_t bytesReceived) {
if (!ec && bytesReceived > 0) {
std::string_view data(buffer_.data(), bytesReceived);
handleRequest(data, remoteEndpoint_);
} else if (ec) {
Logger::error("UDP receive error: " + ec.message());
}
doReceive(); // continue listening
};
socket_.async_receive_from(buffer(buffer_), remoteEndpoint_, onReceive);
}
void UdpServerBase::doSend(
const std::string &response, const UdpEndpoint &target
)
{
using namespace boost::asio;
auto bufferToSend = std::make_shared<std::string>(response);
auto onSend = [bufferToSend](ErrorCode ec, std::size_t /*bytesSent*/) {
if (ec) {
Logger::error("UDP send error: " + ec.message());
}
};
socket_.async_send_to(buffer(*bufferToSend), target, onSend);
}
-23
View File
@@ -1,23 +0,0 @@
#pragma once
#include "aliases.hpp"
class UdpServerBase
{
public:
UdpServerBase(IoContext &io, unsigned short port);
virtual ~UdpServerBase() = default;
void start();
protected:
virtual void
handleRequest(const std::string_view data, const UdpEndpoint &sender) = 0;
void doReceive();
void doSend(const std::string &response, const UdpEndpoint &target);
UdpSocket socket_;
std::array<char, 4096> buffer_;
UdpEndpoint remoteEndpoint_;
};
-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_requires("boost", {
system = true, -- sudo pacman -S boost
add_requires("cmake::Boost", {
alias = "boost",
system = true,
configs = {
all = false,
system = true, -- asio
components = { "program_options", "log" },
},
})
target "app"
set_kind "binary"
add_files "src/*.cpp"
add_links "boost_system"
-- Echo server target
target "echo_server"
do
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