refactor(Core/Network): Port TrinityCore socket optimizations (#24384)

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
Co-authored-by: Shauren <shauren@users.noreply.github.com>
This commit is contained in:
blinkysc
2026-01-15 07:47:58 -06:00
committed by GitHub
parent a8ce95ad71
commit d908b4c2fc
16 changed files with 242 additions and 75 deletions

View File

@@ -162,7 +162,7 @@ void AccountInfo::LoadResult(Field* fields)
Utf8ToUpperOnlyLatin(Login);
}
AuthSession::AuthSession(tcp::socket&& socket) :
AuthSession::AuthSession(IoContextTcpSocket&& socket) :
Socket(std::move(socket)), _status(STATUS_CHALLENGE), _build(0), _expversion(0) { }
void AuthSession::Start()
@@ -216,7 +216,7 @@ void AuthSession::CheckIpCallback(PreparedQueryResult result)
AsyncRead();
}
void AuthSession::ReadHandler()
SocketReadCallbackResult AuthSession::ReadHandler()
{
MessageBuffer& packet = GetReadBuffer();
@@ -234,7 +234,7 @@ void AuthSession::ReadHandler()
if (_status != itr->second.status)
{
CloseSocket();
return;
return SocketReadCallbackResult::Stop;
}
uint16 size = uint16(itr->second.packetSize);
@@ -248,7 +248,7 @@ void AuthSession::ReadHandler()
if (size > MAX_ACCEPTED_CHALLENGE_SIZE)
{
CloseSocket();
return;
return SocketReadCallbackResult::Stop;
}
}
@@ -258,13 +258,13 @@ void AuthSession::ReadHandler()
if (!(*this.*itr->second.handler)())
{
CloseSocket();
return;
return SocketReadCallbackResult::Stop;
}
packet.ReadCompleted(size);
}
AsyncRead();
return SocketReadCallbackResult::KeepReading;
}
void AuthSession::SendPacket(ByteBuffer& packet)

View File

@@ -60,22 +60,22 @@ struct AccountInfo
AccountTypes SecurityLevel = SEC_PLAYER;
};
class AuthSession : public Socket<AuthSession>
class AuthSession final : public Socket<AuthSession>
{
typedef Socket<AuthSession> AuthSocket;
public:
static std::unordered_map<uint8, AuthHandler> InitHandlers();
AuthSession(tcp::socket&& socket);
AuthSession(IoContextTcpSocket&& socket);
void Start() override;
bool Update() override;
bool Update() final;
void SendPacket(ByteBuffer& packet);
protected:
void ReadHandler() override;
SocketReadCallbackResult ReadHandler() final;
private:
bool HandleLogonChallenge();

View File

@@ -54,9 +54,9 @@ protected:
return threads;
}
static void OnSocketAccept(tcp::socket&& sock, uint32 threadIndex)
static void OnSocketAccept(IoContextTcpSocket&& sock, uint32 threadIndex)
{
Instance().OnSocketOpen(std::forward<tcp::socket>(sock), threadIndex);
Instance().OnSocketOpen(std::move(sock), threadIndex);
}
};

View File

@@ -18,18 +18,16 @@
#ifndef __RASESSION_H__
#define __RASESSION_H__
#include <boost/asio/ip/tcp.hpp>
#include "Socket.h"
#include <boost/asio/streambuf.hpp>
#include <future>
using boost::asio::ip::tcp;
const std::size_t bufferSize = 4096;
class RASession : public std::enable_shared_from_this<RASession>
{
public:
RASession(tcp::socket&& socket) :
RASession(IoContextTcpSocket&& socket) :
_socket(std::move(socket)), _commandExecuting(nullptr) { }
void Start();
@@ -47,7 +45,7 @@ private:
static void CommandPrint(void* callbackArg, std::string_view text);
static void CommandFinished(void* callbackArg, bool);
tcp::socket _socket;
IoContextTcpSocket _socket;
boost::asio::streambuf _readBuffer;
boost::asio::streambuf _writeBuffer;
std::promise<void>* _commandExecuting;

View File

@@ -29,14 +29,14 @@ void ScriptMgr::OnNetworkStop()
CALL_ENABLED_HOOKS(ServerScript, SERVERHOOK_ON_NETWORK_STOP, script->OnNetworkStop());
}
void ScriptMgr::OnSocketOpen(std::shared_ptr<WorldSocket> socket)
void ScriptMgr::OnSocketOpen(std::shared_ptr<WorldSocket> const& socket)
{
ASSERT(socket);
CALL_ENABLED_HOOKS(ServerScript, SERVERHOOK_ON_SOCKET_OPEN, script->OnSocketOpen(socket));
}
void ScriptMgr::OnSocketClose(std::shared_ptr<WorldSocket> socket)
void ScriptMgr::OnSocketClose(std::shared_ptr<WorldSocket> const& socket)
{
ASSERT(socket);

View File

@@ -46,11 +46,11 @@ public:
virtual void OnNetworkStop() { }
// Called when a remote socket establishes a connection to the server. Do not store the socket object.
virtual void OnSocketOpen(std::shared_ptr<WorldSocket> /*socket*/) { }
virtual void OnSocketOpen(std::shared_ptr<WorldSocket> const& /*socket*/) { }
// Called when a socket is closed. Do not store the socket object, and do not rely on the connection
// being open; it is not.
virtual void OnSocketClose(std::shared_ptr<WorldSocket> /*socket*/) { }
virtual void OnSocketClose(std::shared_ptr<WorldSocket> const& /*socket*/) { }
/**
* @brief This hook called when a packet is sent to a client. The packet object is a copy of the original packet, so reading and modifying it is safe.

View File

@@ -155,8 +155,8 @@ public: /* SpellScriptLoader */
public: /* ServerScript */
void OnNetworkStart();
void OnNetworkStop();
void OnSocketOpen(std::shared_ptr<WorldSocket> socket);
void OnSocketClose(std::shared_ptr<WorldSocket> socket);
void OnSocketOpen(std::shared_ptr<WorldSocket> const& socket);
void OnSocketClose(std::shared_ptr<WorldSocket> const& socket);
bool CanPacketReceive(WorldSession* session, WorldPacket const& packet);
bool CanPacketSend(WorldSession* session, WorldPacket const& packet);

View File

@@ -116,7 +116,7 @@ void EncryptableAndCompressiblePacket::CompressIfNeeded()
SetOpcode(SMSG_COMPRESSED_UPDATE_OBJECT);
}
WorldSocket::WorldSocket(tcp::socket&& socket)
WorldSocket::WorldSocket(IoContextTcpSocket&& socket)
: Socket(std::move(socket)), _OverSpeedPings(0), _worldSession(nullptr), _authed(false), _sendBufferSize(4096), _loggingPackets(false)
{
Acore::Crypto::GetRandomBytes(_authSeed);
@@ -238,10 +238,10 @@ void WorldSocket::OnClose()
}
}
void WorldSocket::ReadHandler()
SocketReadCallbackResult WorldSocket::ReadHandler()
{
if (!IsOpen())
return;
return SocketReadCallbackResult::Stop;
MessageBuffer& packet = GetReadBuffer();
while (packet.GetActiveSize() > 0)
@@ -264,7 +264,7 @@ void WorldSocket::ReadHandler()
if (!ReadHeaderHandler())
{
CloseSocket();
return;
return SocketReadCallbackResult::Stop;
}
}
@@ -295,11 +295,11 @@ void WorldSocket::ReadHandler()
CloseSocket();
}
return;
return SocketReadCallbackResult::Stop;
}
}
AsyncRead();
return SocketReadCallbackResult::KeepReading;
}
bool WorldSocket::ReadHeaderHandler()

View File

@@ -67,19 +67,19 @@ struct ClientPktHeader
struct AuthSession;
class AC_GAME_API WorldSocket : public Socket<WorldSocket>
class AC_GAME_API WorldSocket final : public Socket<WorldSocket>
{
typedef Socket<WorldSocket> BaseSocket;
public:
WorldSocket(tcp::socket&& socket);
WorldSocket(IoContextTcpSocket&& socket);
~WorldSocket();
WorldSocket(WorldSocket const& right) = delete;
WorldSocket& operator=(WorldSocket const& right) = delete;
void Start() override;
bool Update() override;
bool Update() final;
void SendPacket(WorldPacket const& packet);
@@ -90,7 +90,7 @@ public:
protected:
void OnClose() override;
void ReadHandler() override;
SocketReadCallbackResult ReadHandler() final;
bool ReadHeaderHandler();
enum class ReadDataHandlerResult

View File

@@ -25,13 +25,13 @@
class WorldSocketThread : public NetworkThread<WorldSocket>
{
public:
void SocketAdded(std::shared_ptr<WorldSocket> sock) override
void SocketAdded(std::shared_ptr<WorldSocket> const& sock) override
{
sock->SetSendBufferSize(sWorldSocketMgr.GetApplicationSendBufferSize());
sScriptMgr->OnSocketOpen(sock);
}
void SocketRemoved(std::shared_ptr<WorldSocket> sock) override
void SocketRemoved(std::shared_ptr<WorldSocket> const& sock) override
{
sScriptMgr->OnSocketClose(sock);
}
@@ -81,7 +81,7 @@ void WorldSocketMgr::StopNetwork()
sScriptMgr->OnNetworkStop();
}
void WorldSocketMgr::OnSocketOpen(tcp::socket&& sock, uint32 threadIndex)
void WorldSocketMgr::OnSocketOpen(IoContextTcpSocket&& sock, uint32 threadIndex)
{
// set some options here
if (_socketSystemSendBufferSize >= 0)
@@ -109,7 +109,7 @@ void WorldSocketMgr::OnSocketOpen(tcp::socket&& sock, uint32 threadIndex)
}
}
BaseSocketMgr::OnSocketOpen(std::forward<tcp::socket>(sock), threadIndex);
BaseSocketMgr::OnSocketOpen(std::move(sock), threadIndex);
}
NetworkThread<WorldSocket>* WorldSocketMgr::CreateThreads() const

View File

@@ -41,7 +41,7 @@ public:
/// Stops all network threads, It will wait for all running threads .
void StopNetwork() override;
void OnSocketOpen(tcp::socket&& sock, uint32 threadIndex) override;
void OnSocketOpen(IoContextTcpSocket&& sock, uint32 threadIndex) override;
std::size_t GetApplicationSendBufferSize() const { return _socketApplicationSendBufferSize; }
@@ -50,9 +50,9 @@ protected:
NetworkThread<WorldSocket>* CreateThreads() const override;
static void OnSocketAccept(tcp::socket&& sock, uint32 threadIndex)
static void OnSocketAccept(IoContextTcpSocket&& sock, uint32 threadIndex)
{
Instance().OnSocketOpen(std::forward<tcp::socket>(sock), threadIndex);
Instance().OnSocketOpen(std::move(sock), threadIndex);
}
private:

View File

@@ -20,6 +20,7 @@
#include "IpAddress.h"
#include "Log.h"
#include "Socket.h"
#include "Systemd.h"
#include <atomic>
#include <boost/asio/ip/tcp.hpp>
@@ -32,7 +33,7 @@ constexpr auto ACORE_MAX_LISTEN_CONNECTIONS = boost::asio::socket_base::max_list
class AsyncAcceptor
{
public:
typedef void(*AcceptCallback)(tcp::socket&& newSocket, uint32 threadIndex);
typedef void(*AcceptCallback)(IoContextTcpSocket&& newSocket, uint32 threadIndex);
AsyncAcceptor(Acore::Asio::IoContext& ioContext, std::string const& bindIp, uint16 port, bool supportSocketActivation = false) :
_acceptor(ioContext), _endpoint(Acore::Net::make_address(bindIp), port),
@@ -56,7 +57,7 @@ public:
template<AcceptCallback acceptCallback>
void AsyncAcceptWithCallback()
{
tcp::socket* socket;
IoContextTcpSocket* socket;
uint32 threadIndex;
std::tie(socket, threadIndex) = _socketFactory();
_acceptor.async_accept(*socket, [this, socket, threadIndex](boost::system::error_code error)
@@ -129,16 +130,16 @@ public:
_acceptor.close(err);
}
void SetSocketFactory(std::function<std::pair<tcp::socket*, uint32>()> func) { _socketFactory = func; }
void SetSocketFactory(std::function<std::pair<IoContextTcpSocket*, uint32>()> func) { _socketFactory = std::move(func); }
private:
std::pair<tcp::socket*, uint32> DefaultSocketFactory() { return std::make_pair(&_socket, 0); }
std::pair<IoContextTcpSocket*, uint32> DefaultSocketFactory() { return std::make_pair(&_socket, 0); }
tcp::acceptor _acceptor;
tcp::endpoint _endpoint;
tcp::socket _socket;
boost::asio::basic_socket_acceptor<boost::asio::ip::tcp, IoContextTcpSocket::executor_type> _acceptor;
boost::asio::ip::tcp::endpoint _endpoint;
IoContextTcpSocket _socket;
std::atomic<bool> _closed;
std::function<std::pair<tcp::socket*, uint32>()> _socketFactory;
std::function<std::pair<IoContextTcpSocket*, uint32>()> _socketFactory;
bool _supportSocketActivation;
};

View File

@@ -91,13 +91,13 @@ public:
SocketAdded(sock);
}
tcp::socket* GetSocketForAccept() { return &_acceptSocket; }
IoContextTcpSocket* GetSocketForAccept() { return &_acceptSocket; }
void EnableProxyProtocol() { _proxyHeaderReadingEnabled = true; }
protected:
virtual void SocketAdded(std::shared_ptr<SocketType> /*sock*/) { }
virtual void SocketRemoved(std::shared_ptr<SocketType> /*sock*/) { }
virtual void SocketAdded(std::shared_ptr<SocketType> const& /*sock*/) { }
virtual void SocketRemoved(std::shared_ptr<SocketType> const& /*sock*/) { }
void AddNewSockets()
{
@@ -229,7 +229,7 @@ private:
SocketContainer _newSockets;
Acore::Asio::IoContext _ioContext;
tcp::socket _acceptSocket;
IoContextTcpSocket _acceptSocket;
boost::asio::steady_timer _updateTimer;
bool _proxyHeaderReadingEnabled;

View File

@@ -21,9 +21,8 @@
#include "Log.h"
#include "MessageBuffer.h"
#include <atomic>
#include <boost/asio.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <functional>
#include <memory>
#include <queue>
#include <type_traits>
@@ -35,6 +34,23 @@ using boost::asio::ip::tcp;
#define AC_SOCKET_USE_IOCP
#endif
// Specialize boost socket for io_context executor instead of type-erased any_io_executor
// This avoids the type-erasure overhead of any_io_executor
using IoContextTcpSocket = boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::io_context::executor_type>;
enum class SocketReadCallbackResult
{
KeepReading,
Stop
};
enum class SocketState : uint8
{
Open = 0,
Closing = 1,
Closed = 2
};
enum ProxyHeaderReadingState {
PROXY_HEADER_READING_STATE_NOT_STARTED,
PROXY_HEADER_READING_STATE_STARTED,
@@ -51,8 +67,8 @@ template<class T>
class Socket : public std::enable_shared_from_this<T>
{
public:
explicit Socket(tcp::socket&& socket) : _socket(std::move(socket)), _remoteAddress(_socket.remote_endpoint().address()),
_remotePort(_socket.remote_endpoint().port()), _readBuffer(), _closed(false), _closing(false), _isWritingAsync(false),
explicit Socket(IoContextTcpSocket&& socket) : _socket(std::move(socket)), _remoteAddress(_socket.remote_endpoint().address()),
_remotePort(_socket.remote_endpoint().port()), _readBuffer(), _state(SocketState::Open), _isWritingAsync(false),
_proxyHeaderReadingState(PROXY_HEADER_READING_STATE_NOT_STARTED)
{
_readBuffer.Resize(READ_BLOCK_SIZE);
@@ -60,7 +76,7 @@ public:
virtual ~Socket()
{
_closed = true;
_state = SocketState::Closed;
boost::system::error_code error;
_socket.close(error);
}
@@ -69,13 +85,14 @@ public:
virtual bool Update()
{
if (_closed)
SocketState state = _state.load();
if (state == SocketState::Closed)
{
return false;
}
#ifndef AC_SOCKET_USE_IOCP
if (_isWritingAsync || (_writeQueue.empty() && !_closing))
if (_isWritingAsync || (_writeQueue.empty() && state != SocketState::Closing))
{
return true;
}
@@ -150,12 +167,18 @@ public:
[[nodiscard]] ProxyHeaderReadingState GetProxyHeaderReadingState() const { return _proxyHeaderReadingState; }
[[nodiscard]] bool IsOpen() const { return !_closed && !_closing; }
[[nodiscard]] bool IsOpen() const { return _state.load() == SocketState::Open; }
void CloseSocket()
{
if (_closed.exchange(true))
return;
SocketState expected = SocketState::Open;
if (!_state.compare_exchange_strong(expected, SocketState::Closed))
{
// If it was Closing, try to transition to Closed
expected = SocketState::Closing;
if (!_state.compare_exchange_strong(expected, SocketState::Closed))
return; // Already closed
}
boost::system::error_code shutdownError;
_socket.shutdown(boost::asio::socket_base::shutdown_send, shutdownError);
@@ -168,13 +191,17 @@ public:
}
/// Marks the socket for closing after write buffer becomes empty
void DelayedCloseSocket() { _closing = true; }
void DelayedCloseSocket()
{
SocketState expected = SocketState::Open;
_state.compare_exchange_strong(expected, SocketState::Closing);
}
MessageBuffer& GetReadBuffer() { return _readBuffer; }
protected:
virtual void OnClose() { }
virtual void ReadHandler() = 0;
virtual SocketReadCallbackResult ReadHandler() = 0;
bool AsyncProcessQueue()
{
@@ -188,7 +215,7 @@ protected:
_socket.async_write_some(boost::asio::buffer(buffer.GetReadPointer(), buffer.GetActiveSize()), std::bind(&Socket<T>::WriteHandler,
this->shared_from_this(), std::placeholders::_1, std::placeholders::_2));
#else
_socket.async_wait(tcp::socket::wait_write, [self = this->shared_from_this()](boost::system::error_code error)
_socket.async_wait(boost::asio::socket_base::wait_write, [self = this->shared_from_this()](boost::system::error_code error)
{
self->WriteHandlerWrapper(error, 0);
});
@@ -216,7 +243,8 @@ private:
}
_readBuffer.WriteCompleted(transferredBytes);
ReadHandler();
if (ReadHandler() == SocketReadCallbackResult::KeepReading)
AsyncRead();
}
// ProxyReadHeaderHandler reads Proxy Protocol v2 header (v1 is not supported).
@@ -344,7 +372,7 @@ private:
if (!_writeQueue.empty())
AsyncProcessQueue();
else if (_closing)
else if (_state.load() == SocketState::Closing)
CloseSocket();
}
else
@@ -380,7 +408,7 @@ private:
_writeQueue.pop();
if (_closing && _writeQueue.empty())
if (_state.load() == SocketState::Closing && _writeQueue.empty())
{
CloseSocket();
}
@@ -391,7 +419,7 @@ private:
{
_writeQueue.pop();
if (_closing && _writeQueue.empty())
if (_state.load() == SocketState::Closing && _writeQueue.empty())
{
CloseSocket();
}
@@ -406,7 +434,7 @@ private:
_writeQueue.pop();
if (_closing && _writeQueue.empty())
if (_state.load() == SocketState::Closing && _writeQueue.empty())
{
CloseSocket();
}
@@ -415,7 +443,7 @@ private:
}
#endif
tcp::socket _socket;
IoContextTcpSocket _socket;
boost::asio::ip::address _remoteAddress;
uint16 _remotePort;
@@ -423,8 +451,7 @@ private:
MessageBuffer _readBuffer;
std::queue<MessageBuffer> _writeQueue;
std::atomic<bool> _closed;
std::atomic<bool> _closing;
std::atomic<SocketState> _state;
bool _isWritingAsync;

View File

@@ -91,7 +91,7 @@ public:
_threads[i].Wait();
}
virtual void OnSocketOpen(tcp::socket&& sock, uint32 threadIndex)
virtual void OnSocketOpen(IoContextTcpSocket&& sock, uint32 threadIndex)
{
try
{
@@ -117,7 +117,7 @@ public:
return min;
}
std::pair<tcp::socket*, uint32> GetSocketForAccept()
std::pair<IoContextTcpSocket*, uint32> GetSocketForAccept()
{
uint32 threadIndex = SelectThreadWithMinConnections();
return { _threads[threadIndex].GetSocketForAccept(), threadIndex };

View File

@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""
Socket Stress Test for AzerothCore
Tests authserver and worldserver connection handling under heavy load.
Usage:
python3 socket_stress_heavy.py [duration_seconds] [auth_threads] [world_threads]
Defaults:
duration: 300 seconds (5 minutes)
auth_threads: 100
world_threads: 150
"""
import socket
import time
import threading
import sys
AUTH_PORT = 3724
WORLD_PORT = 8085
HOST = '127.0.0.1'
stats = {'auth_ok': 0, 'auth_fail': 0, 'world_ok': 0, 'world_fail': 0}
running = True
def stress_auth():
"""Flood authserver with login challenge packets."""
global stats
while running:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
s.connect((HOST, AUTH_PORT))
# AUTH_LOGON_CHALLENGE packet
packet = bytes([
0x00, # cmd: AUTH_LOGON_CHALLENGE
0x00, # error
0x24, 0x00, # size (36)
0x57, 0x6F, 0x57, 0x00, # 'WoW\0'
0x03, 0x03, 0x05, # version 3.3.5
0x30, 0x30, # build 12340
0x78, 0x38, 0x36, 0x00, # 'x86\0'
0x6E, 0x69, 0x57, 0x00, # 'niW\0' (Win reversed)
0x53, 0x55, 0x6E, 0x65, # 'SUne' (enUS reversed)
0x3C, 0x00, 0x00, 0x00, # timezone
0x7F, 0x00, 0x00, 0x01, # IP 127.0.0.1
0x04, # account name length
0x54, 0x45, 0x53, 0x54 # 'TEST'
])
s.sendall(packet)
s.close()
stats['auth_ok'] += 1
except Exception:
stats['auth_fail'] += 1
def stress_world():
"""Flood worldserver with connection attempts."""
global stats
while running:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
s.connect((HOST, WORLD_PORT))
# Wait for SMSG_AUTH_CHALLENGE
s.recv(64)
s.close()
stats['world_ok'] += 1
except Exception:
stats['world_fail'] += 1
def main():
global running
# Parse arguments
duration = int(sys.argv[1]) if len(sys.argv) > 1 else 300
auth_threads = int(sys.argv[2]) if len(sys.argv) > 2 else 100
world_threads = int(sys.argv[3]) if len(sys.argv) > 3 else 150
print("=" * 60)
print("SOCKET STRESS TEST")
print("=" * 60)
print(f"Duration: {duration} seconds")
print(f"Auth threads: {auth_threads} -> {HOST}:{AUTH_PORT}")
print(f"World threads: {world_threads} -> {HOST}:{WORLD_PORT}")
print("-" * 60)
threads = []
for _ in range(auth_threads):
t = threading.Thread(target=stress_auth, daemon=True)
t.start()
threads.append(t)
for _ in range(world_threads):
t = threading.Thread(target=stress_world, daemon=True)
t.start()
threads.append(t)
print(f"Started {len(threads)} threads")
print("-" * 60)
start = time.time()
try:
while time.time() - start < duration:
elapsed = int(time.time() - start)
total = stats['auth_ok'] + stats['world_ok']
rate = total / max(elapsed, 1)
print(f"\r[{elapsed:3d}s] Auth: {stats['auth_ok']:7d} ok {stats['auth_fail']:5d} fail | "
f"World: {stats['world_ok']:7d} ok {stats['world_fail']:5d} fail | "
f"Rate: {rate:,.0f}/s ", end='', flush=True)
time.sleep(1)
except KeyboardInterrupt:
print("\n\nInterrupted by user")
running = False
time.sleep(0.5)
total_ok = stats['auth_ok'] + stats['world_ok']
total_fail = stats['auth_fail'] + stats['world_fail']
elapsed = time.time() - start
print("\n" + "=" * 60)
print("RESULTS:")
print(f" Duration: {elapsed:.1f} seconds")
print(f" Auth: {stats['auth_ok']:,} ok, {stats['auth_fail']:,} failed")
print(f" World: {stats['world_ok']:,} ok, {stats['world_fail']:,} failed")
print(f" Total: {total_ok:,} ok, {total_fail:,} failed")
print(f" Rate: {total_ok / elapsed:,.0f} connections/sec average")
if total_fail > 0:
print(f" Failure: {total_fail / (total_ok + total_fail) * 100:.2f}%")
print("=" * 60)
if __name__ == '__main__':
main()