A production-grade C++ WebSocket client proof-of-concept for TallyIX with a focus on thread safety, synchronization, and extensibility.
This project demonstrates a robust WebSocket client built on top of IXWebSocket library, providing a higher-level abstraction suitable for evolving into production-level TallyIX integration.
- ✅ Thread-Safe Logging: Mutex-protected stdout access with configurable log levels
- ✅ State Machine: Proper connection lifecycle management (Disconnected → Connecting → Connected → Closing)
- ✅ Synchronization Primitives: Condition variables for blocking waits with timeout and error detection
- ✅ Protocol Abstraction: Typed messages decouple application from JSON wire format
- ✅ Message Routing: Observer pattern separates transport from business logic
- ✅ Exception-Safe:
std::unique_ptrfor automatic resource cleanup - ✅ Automatic DLL Deployment: Post-build events copy OpenSSL DLLs to output directory
- ✅ Helper Functions:
MessageTypeToString(),IsValidMessage(),GetStateString()for debugging - ✅ Configurable Logging:
SetMinLevel()to filter log output for production/debug builds - ✅ Connection Retry Logic: Exponential backoff for resilient connections (NEW)
- ✅ Binary Transfer Security: Overflow protection and size validation (NEW)
- ✅ Thread-Safe Shutdown: Condition variable-based deterministic cleanup (NEW)
- ✅ Protocol Error Propagation: Application notification for protocol violations (NEW)
- ✅ Configuration Validation:
Config::IsValid()for bounds checking (NEW)
Logger (Thread-safe singleton)
├── Mutex-protected console access
├── Timestamp and severity levels
└── Used by all components
Protocol (Message abstraction)
├── Typed message enums
├── JSON serialization/parsing
├── Configuration struct
└── No external dependencies
MessageHandler (Observer pattern)
├── IMessageHandler interface
├── MessageRouter for dispatch
└── Separates transport from app logic
WsClient (Production WebSocket client)
├── State machine with mutex + CV
├── IXWebSocket wrapper
├── Thread-safe send methods
├── Binary reassembly tracking
└── Pimpl pattern for encapsulation
main.cpp (Example application)
├── TallyIXMessageHandler implementation
├── Demonstrates proper usage flow
├── Error handling at each step
└── 3-second wait for echo response
IXWebSocket thread: Main thread:
OnMessage() ──────────> WaitForConnection()
↓ ↓
Protocol::Parse() SendText()
↓ SendBinary()
MessageRouter ↓
↓ IXWebSocket::send()
IMessageHandler::On*()
- Visual Studio 2022 (v143, C++17)
- IXWebSocket: Compiled at
D:\ALL_BINS\TallyIXWebSocketPOC\IXWebSocket\build\{Debug|Release}\ixwebsocket.lib - OpenSSL:
C:\Program Files\OpenSSL-Win64(for secure WebSocket support)
- Open
VS_Websocket.slnin Visual Studio - Select configuration (Debug or Release) and platform (x64)
- Build → Rebuild Solution
- Executable:
x64\{Debug|Release}\VS_Websocket.exe- OpenSSL DLLs are automatically copied to output directory via post-build events
msbuild VS_Websocket.sln /p:Configuration=Debug /p:Platform=x64pip install websockets
python src/server_mock/echo_server.py
# WebSocket echo server running on ws://127.0.0.1:9001x64\Debug\VS_Websocket.exe16:18:37.668 [INF][Main] ====================================================
16:18:37.671 [INF][Main] TallyIX WebSocket POC - v2.0
16:18:37.672 [INF][Main] ====================================================
16:18:37.673 [INF][Main] Configuration: timeout=10000ms, maxBinarySize=100MB
16:18:37.677 [INF][Main] Network initialization attempt 1/3
16:18:37.679 [INF][WsClient] Network system initialized successfully
16:18:37.680 [INF][Main] Starting connection attempts (max: 5, exponential backoff)
16:18:37.681 [INF][Main] Connection attempt 1/5 to ws://127.0.0.1:9001
16:18:37.702 [INF][WsClient] Connected to server
16:18:37.703 [INF][Main] Connected to server successfully on attempt 1!
16:18:37.705 [INF][Main] Sending Hello message...
16:18:37.709 [INF][App] Received text message - Type: Hello, MsgID: msg_001
16:18:37.713 [INF][App] Binary transfer starting - Expected size: 1048576 bytes
16:18:38.041 [DBG][App] Received binary chunk: 1048576 bytes (Total: 1048576)
16:18:38.042 [INF][App] Binary transfer complete - 1048576 bytes received
16:18:41.034 [INF][Main] Closing connection...
16:18:41.039 [INF][Main] Final state: Disconnected
16:18:41.042 [INF][Main] TallyIX WebSocket POC - Complete
Protocol::Config config;
config.connectionTimeoutMs = 10000;
config.maxBinaryPayloadSize = 100 * 1024 * 1024;
// Validate configuration before use (NEW)
if (!config.IsValid()) {
// Handle invalid configuration
}
WsClient client(config);
MyMessageHandler handler;
client.SetMessageHandler(&handler);
// Configure logging level for production
Logger::Instance().SetMinLevel(Logger::Level::Info);
client.Open(); // Initialize
// Connection with retry logic (IMPROVED)
const int maxAttempts = 5;
int backoffMs = 500;
bool connected = false;
for (int attempt = 1; attempt <= maxAttempts && !connected; ++attempt) {
client.Connect("ws://host:port"); // Non-blocking
if (client.WaitForConnection(10000)) {
connected = true;
} else {
// Exponential backoff: 500ms, 1s, 2s, 4s, 8s
std::this_thread::sleep_for(std::chrono::milliseconds(backoffMs));
backoffMs *= 2;
}
}
// Check connection state
std::cout << "State: " << client.GetStateString() << std::endl;
client.SendText(json);
client.SendBinary(data, size);
client.Close(); // Graceful shutdown with proper synchronizationclass MyHandler : public IMessageHandler {
public:
void OnTextMessage(const Protocol::Message& msg) override {
// Use helper function for better logging
std::cout << "Type: " << Protocol::MessageTypeToString(msg.type) << std::endl;
// Validate message before processing
if (!Protocol::IsValidMessage(msg)) {
// Handle invalid message
return;
}
// Handle hello, ack, error
}
void OnBinaryStart(const Protocol::Message& msg) override {
// msg.binarySize tells you expected bytes
}
void OnBinaryChunk(const uint8_t* data, size_t size) override {
// Called multiple times as data arrives
}
void OnBinaryComplete() override {
// All binary data received
}
void OnProtocolError(const std::string& reason) override {
// Handle protocol-level errors
}
};All messages are JSON with the following structure:
{
"type": "hello|binary_start|binary_data|ack|error",
"msg_id": "unique-message-id",
"content": "optional-content",
"size": 1048576
}| Type | Direction | Purpose |
|---|---|---|
hello |
Bi | Initial handshake |
binary_start |
Bi | Signals incoming binary with size metadata |
binary_data |
Bi | Raw binary payload (not wrapped in JSON) |
ack |
Bi | Acknowledgment of received message |
error |
Bi | Error response |
| Component | Access Model | Synchronization |
|---|---|---|
| Logger | Singleton | Mutex (lock_guard) + Min Level Filter |
| WsClient state | Shared (main + IXWs) | Mutex + Condition Variable |
| WsClient binary state | Shared (callback thread) | Dedicated binaryMutex (NEW) |
| WsClient shutdown | Shared (main + callback) | shutdownCV + shutdownComplete flag (NEW) |
| MessageRouter | Called from IXWs | No mutex (callback-only writes) |
| Protocol | Stateless | None needed |
| IMessageHandler | Called from IXWs | App responsible for internal sync |
- Binary Transfer:
binaryBytesReceivedandbinaryExpectedSizeare protected bybinaryMutex - Shutdown Synchronization:
Close()waits onshutdownCVfor deterministic cleanup (up to 5s timeout) - State Validation:
WaitForConnection()validates state before waiting - TOCTOU Documented:
SendText()/SendBinary()intentionally release lock before I/O (documented)
// Convert MessageType to readable string
std::string typeStr = Protocol::MessageTypeToString(msg.type); // e.g., "Hello"
// Validate a message has required fields
bool valid = Protocol::IsValidMessage(msg); // type != Unknown && msgId not empty// Set minimum log level (Debug < Info < Warning < Error)
Logger::Instance().SetMinLevel(Logger::Level::Warning); // Only warnings and errors// Get state as enum
WsClient::ConnectionState state = client.GetState();
// Get state as string for logging
std::string stateStr = client.GetStateString(); // e.g., "Connected"VS_Websocket/
├── .github/
│ └── copilot-instructions.md # AI agent guidance
├── src/
│ ├── main.cpp # Example application
│ ├── WsClient.hpp/.cpp # WebSocket client
│ ├── Logger.hpp/.cpp # Thread-safe logging
│ ├── Protocol.hpp/.cpp # Message protocol
│ ├── MessageHandler.hpp/.cpp # Observer pattern
│ └── server_mock/
│ └── echo_server.py # Mock server for testing
├── VS_Websocket.sln # Visual Studio solution
├── VS_Websocket.vcxproj # Project configuration
├── .gitignore # Git exclusions
├── README.md # This file
└── TODO.md # Future work
- Add to
Protocol::MessageTypeenum - Update
Protocol::ParseJsonMessage()to recognize it - Update
Protocol::SerializeJsonMessage()to emit it - Add handler method to
IMessageHandler - Call it from
MessageRouter::RouteMessage()
- Add field to
Protocol::Configstruct - Pass config in
WsClientconstructor - Use in appropriate method
- Simple JSON parsing (no external library); breaks on special characters
- No automatic reconnection built-in (retry logic example provided in main.cpp)
- Binary fragmentation handled per-message only
- No compression support yet
- Mock server just echoes (no validation)
- Memory usage during binary transfers can be 2-3x payload size due to internal copies
- Integer Overflow Protection: Binary reassembly checks for overflow before accumulating
- Size Validation:
BinaryStartsize validated againstmaxBinaryPayloadSize - Zero Size Rejection:
BinaryStartwith size 0 is rejected as invalid - State Reset on Disconnect: Binary transfer state cleared on connection close
- Configuration Bounds:
Config::IsValid()enforces reasonable limits (max 1GB binary)
See TODO.md for planned enhancements.
- IXWebSocket: https://github.com/machinezone/IXWebSocket
- OpenSSL: https://www.openssl.org/
- WebSocket RFC: https://tools.ietf.org/html/rfc6455
This is a proof-of-concept for TallyIX. All rights reserved.