Last active
February 14, 2026 09:14
-
-
Save mq1n/69919d7d27838f19b85991c7e95a11c6 to your computer and use it in GitHub Desktop.
soak tool
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // net_soak/main.cpp – Soak test for metin2_net library | |
| #include <net/Acceptor.hpp> | |
| #include <net/ClientSocketAdapter.hpp> | |
| #include <net/Socket.hpp> | |
| #include <net/Type.hpp> | |
| #include <SpdLog.hpp> | |
| #include <asio/io_service.hpp> | |
| #include <asio/steady_timer.hpp> | |
| #include <random> | |
| #include <thread> | |
| #include <vector> | |
| #include <mutex> | |
| #include <chrono> | |
| #include <csignal> | |
| #include <memory> | |
| #include <algorithm> | |
| #include <atomic> | |
| // ----------------------------------------------------------------------------- | |
| // Simple packet definitions used for the soak test | |
| // ----------------------------------------------------------------------------- | |
| namespace soak { | |
| enum PacketIds : PacketId { | |
| Move = 1, | |
| Chat = 2, | |
| }; | |
| struct MovePacket { | |
| float x{0}; | |
| float y{0}; | |
| float z{0}; | |
| }; | |
| struct ChatPacket { | |
| std::string message; | |
| }; | |
| } // namespace soak | |
| #include <base/Serialization.hpp> | |
| REFLECT_STRUCT(soak::MovePacket, | |
| MEMBER(soak::MovePacket, x), | |
| MEMBER(soak::MovePacket, y), | |
| MEMBER(soak::MovePacket, z) | |
| ) | |
| REFLECT_STRUCT(soak::ChatPacket, | |
| MEMBER(soak::ChatPacket, message) | |
| ) | |
| // ----------------------------------------------------------------------------- | |
| // Forward declarations | |
| // ----------------------------------------------------------------------------- | |
| class SoakServer; | |
| class ServerSocket; | |
| // ----------------------------------------------------------------------------- | |
| // Server-side socket implementation | |
| // ----------------------------------------------------------------------------- | |
| class ServerSocket : public Socket { | |
| public: | |
| using Pointer = std::shared_ptr<ServerSocket>; | |
| static Pointer Create(asio::ip::tcp::socket sock, SoakServer& owner) { | |
| auto sp = Pointer(new ServerSocket(std::move(sock), owner)); | |
| sp->StartReadSome(); | |
| return sp; | |
| } | |
| ~ServerSocket() override = default; | |
| protected: | |
| uint32_t ProcessData(const asio::const_buffer& data) override; | |
| bool HandlePacket(const PacketHeader& header, const asio::const_buffer& data) override; | |
| void OnDisconnect() override; | |
| private: | |
| ServerSocket(asio::ip::tcp::socket sock, SoakServer& owner) | |
| : Socket(std::move(sock)), owner_(owner) { | |
| } | |
| SoakServer& owner_; | |
| }; | |
| // ----------------------------------------------------------------------------- | |
| // Soak server that accepts connections and holds onto sockets | |
| // ----------------------------------------------------------------------------- | |
| class SoakServer : public std::enable_shared_from_this<SoakServer> { | |
| public: | |
| SoakServer(asio::io_service& io, uint16_t port) | |
| : io_(io), acceptor_(io) | |
| { | |
| std::error_code ec; | |
| acceptor_.Bind("0.0.0.0", std::to_string(port), ec, | |
| [this](asio::ip::tcp::socket&& s) { | |
| HandleAccept(std::move(s)); | |
| }); | |
| if (ec) { | |
| spdlog::critical("Failed to bind server: {}", ec.message()); | |
| throw std::runtime_error("Server bind failed"); | |
| } | |
| } | |
| void BroadcastChat(const soak::ChatPacket& pkt, const ServerSocket* except = nullptr) | |
| { | |
| // Snapshot without holding the mutex for long. | |
| std::vector<ServerSocket::Pointer> snapshot; | |
| { | |
| std::lock_guard<std::mutex> lock(mutex_); | |
| snapshot.reserve(clients_.size()); | |
| for (auto it = clients_.begin(); it != clients_.end(); ) { | |
| if (auto spt = it->lock()) { | |
| snapshot.push_back(std::move(spt)); | |
| ++it; | |
| } else { | |
| it = clients_.erase(it); | |
| } | |
| } | |
| } | |
| // Dispatch – rely on per-socket Send which sets sequence id & encryption. | |
| for (auto& spt : snapshot) { | |
| if (except && spt.get() == except) continue; | |
| // Build & queue; Send already degrades to TrySend inside if buffer full. | |
| spt->Send(soak::PacketIds::Chat, pkt); | |
| } | |
| } | |
| void AddClient(const ServerSocket::Pointer& sock) | |
| { | |
| std::lock_guard<std::mutex> lock(mutex_); | |
| clients_.push_back(sock); | |
| } | |
| void RemoveClient(const ServerSocket* sock) | |
| { | |
| std::lock_guard<std::mutex> lock(mutex_); | |
| clients_.erase(std::remove_if(clients_.begin(), clients_.end(), [sock](const std::weak_ptr<ServerSocket>& w){ | |
| return w.expired() || w.lock().get() == sock; | |
| }), clients_.end()); | |
| } | |
| private: | |
| void HandleAccept(asio::ip::tcp::socket socket) { | |
| auto client = ServerSocket::Create(std::move(socket), *this); | |
| AddClient(client); | |
| } | |
| asio::io_service& io_; | |
| Acceptor acceptor_; | |
| std::vector<std::weak_ptr<ServerSocket>> clients_; | |
| std::mutex mutex_; | |
| }; | |
| // Implementation of ServerSocket methods | |
| uint32_t ServerSocket::ProcessData(const asio::const_buffer& data) { | |
| return Socket::ProcessData(data); | |
| } | |
| bool ServerSocket::HandlePacket(const PacketHeader& header, const asio::const_buffer& data) | |
| { | |
| switch(header.id) { | |
| case soak::PacketIds::Move: { | |
| // Ignore move packets (no shared state updated for test) | |
| ( void )::ReadPacket<soak::MovePacket>(data); | |
| return true; | |
| } | |
| case soak::PacketIds::Chat: { | |
| auto pkt = ::ReadPacket<soak::ChatPacket>(data); | |
| owner_.BroadcastChat(pkt, this); | |
| return true; | |
| } | |
| default: | |
| spdlog::warn("Server received unknown packet id {}", header.id); | |
| return false; // Treat as error | |
| } | |
| } | |
| void ServerSocket::OnDisconnect() { | |
| owner_.RemoveClient(this); | |
| } | |
| // ----------------------------------------------------------------------------- | |
| // Client-side bot implementation | |
| // ----------------------------------------------------------------------------- | |
| class BotClient : public ClientSocketAdapter<Socket> { // Remove enable_shared_from_this<BotClient> | |
| public: | |
| using Pointer = std::shared_ptr<BotClient>; | |
| BotClient(asio::io_service& io, std::mt19937& rng) | |
| : ClientSocketAdapter<Socket>(io), timer_(io), rng_(rng), dist_pos_(-500.0f, 500.0f) | |
| { | |
| // Extra config if needed | |
| } | |
| void Start(const std::string& host, const std::string& port) { | |
| Connect(host, port); | |
| } | |
| protected: | |
| void OnConnectSuccess() override { | |
| spdlog::trace("Bot connected"); | |
| StartReadSome(); | |
| ScheduleAction(); | |
| } | |
| void OnConnectFailure(const std::error_code& ec) override { | |
| spdlog::error("Bot failed to connect: {}", ec.message()); | |
| } | |
| uint32_t ProcessData(const asio::const_buffer& data) override { | |
| return Socket::ProcessData(data); | |
| } | |
| bool HandlePacket(const PacketHeader& header, const asio::const_buffer& data) override { | |
| // For now bots ignore all incoming packets. | |
| return true; | |
| } | |
| private: | |
| void ScheduleAction() { | |
| // Cast the base class shared_ptr to BotClient | |
| auto self = std::static_pointer_cast<BotClient>( | |
| ClientSocketAdapter<Socket>::shared_from_this() | |
| ); | |
| constexpr int intervalMs = 100; | |
| timer_.expires_after(std::chrono::milliseconds(intervalMs)); | |
| timer_.async_wait([self](const std::error_code& ec) { | |
| if (ec) return; | |
| self->PerformAction(); | |
| self->ScheduleAction(); | |
| }); | |
| } | |
| void PerformAction() { | |
| // 50/50 move vs chat | |
| std::uniform_int_distribution<int> dist_action(0, 1); | |
| if (dist_action(rng_) == 0) { | |
| SendMove(); | |
| } else { | |
| SendChat(); | |
| } | |
| } | |
| void SendMove() { | |
| soak::MovePacket pkt{dist_pos_(rng_), dist_pos_(rng_), dist_pos_(rng_)}; | |
| Send(soak::PacketIds::Move, pkt); | |
| } | |
| void SendChat() { | |
| soak::ChatPacket pkt{"Hello from bot"}; | |
| Send(soak::PacketIds::Chat, pkt); | |
| } | |
| asio::steady_timer timer_; | |
| std::mt19937& rng_; | |
| std::uniform_real_distribution<float> dist_pos_; | |
| }; | |
| // ----------------------------------------------------------------------------- | |
| // Utility: graceful shutdown on CTRL+C | |
| // ----------------------------------------------------------------------------- | |
| static std::atomic_bool g_stop{false}; | |
| void SignalHandler(int /*signal*/) { | |
| g_stop = true; | |
| } | |
| int main(int argc, char* argv[]) | |
| { | |
| const size_t botCount = (argc > 1) ? static_cast<size_t>(std::stoul(argv[1])) : 10000; | |
| const int duration_s = (argc > 2) ? std::stoi(argv[2]) : 60; // default 60 seconds | |
| spdlog::info("Starting soak test with {} bots for {} seconds", botCount, duration_s); | |
| asio::io_service io; | |
| // Create server | |
| uint16_t port = 14444; | |
| auto server = std::make_shared<SoakServer>(io, port); | |
| // Start IO service threads BEFORE creating bots | |
| const unsigned threadCount = std::max(2u, std::thread::hardware_concurrency()); | |
| std::vector<std::thread> workers; | |
| for (unsigned t = 0; t < threadCount; ++t) { | |
| workers.emplace_back([&io]() { | |
| io.run(); | |
| }); | |
| } | |
| // Give server time to start listening | |
| std::this_thread::sleep_for(std::chrono::milliseconds(100)); | |
| spdlog::info("Server started, creating bots..."); | |
| // RNG shared among bots | |
| std::random_device rd; | |
| std::mt19937 rng(rd()); | |
| // Create bots gradually to avoid overwhelming the system | |
| std::vector<BotClient::Pointer> bots; | |
| bots.reserve(botCount); | |
| for (size_t i = 0; i < botCount; ++i) { | |
| auto bot = std::make_shared<BotClient>(io, rng); | |
| bot->Start("127.0.0.1", std::to_string(port)); | |
| bots.emplace_back(std::move(bot)); | |
| // Add small delay every 100 connections to prevent overwhelming | |
| if ((i + 1) % 100 == 0) { | |
| std::this_thread::sleep_for(std::chrono::milliseconds(10)); | |
| spdlog::info("Created {} bots", i + 1); | |
| } | |
| } | |
| // Register signal handler | |
| std::signal(SIGINT, SignalHandler); | |
| #ifdef SIGTERM | |
| std::signal(SIGTERM, SignalHandler); | |
| #endif | |
| // Wait for duration or until user stops | |
| for (int i = 0; i < duration_s && !g_stop; ++i) { | |
| std::this_thread::sleep_for(std::chrono::seconds(1)); | |
| } | |
| io.stop(); | |
| for (auto& th : workers) th.join(); | |
| spdlog::info("Soak test finished."); | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment