Skip to content

Instantly share code, notes, and snippets.

@mq1n
Last active February 14, 2026 09:14
Show Gist options
  • Select an option

  • Save mq1n/69919d7d27838f19b85991c7e95a11c6 to your computer and use it in GitHub Desktop.

Select an option

Save mq1n/69919d7d27838f19b85991c7e95a11c6 to your computer and use it in GitHub Desktop.
soak tool
// 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