Updated in August 2026: this article reflects
sioxxv0.2.0 and the latest additions onmain, including binary attachments with the default JSON parser.
The established socket.io-client-cpp remains a useful C++ Socket.IO client. Its architecture, however, is built around websocketpp, standalone Asio, a custom sio::message hierarchy, and RapidJSON for serialization. Its dependencies are provided as Git submodules by default, although current versions can also use installed packages and provide modern CMake targets.
The networking dependency is also showing its age. websocketpp last received a commit on 19 April 2025, roughly 16 months ago at the time of this update. Its two commits in 2025 were a single maintenance change to support CMake 4; before that, the previous commit was in April 2020. In practice, functional development has been dormant for more than six years.
I needed a client that fitted applications already using Boost.Asio, Boost.Beast, and nlohmann::json, without adding a second networking and JSON stack. That led to sioxx: a C++17 Socket.IO client with a familiar event-driven API, but a different implementation.
- 🔗 GitHub: jfayot/sioxx
- 🔗 Documentation: Read the docs
How sioxx differs
socket.io-client-cpp |
sioxx |
|
|---|---|---|
| Payload model | custom sio::message types; RapidJSON serialization |
nlohmann::json aliases |
| Networking | standalone Asio + websocketpp | Boost.Asio + Boost.Beast |
| Socket.IO parsers | default JSON protocol | JSON, MessagePack, or an application-defined parser |
| Engine.IO transports | WebSocket | WebSocket with HTTP long-polling fallback, or forced polling |
| Reconnection | capped exponential backoff | capped exponential backoff with configurable symmetric jitter |
| Build and packaging | submodules by default or installed dependencies; CMake install/export |
FetchContent by default or system Boost/JSON; CMake install/export and Conan 2 recipe |
| Tests | Catch2 packet tests | GoogleTest unit tests plus opt-in Node.js-backed end-to-end tests |
The API deliberately retains the shape expected from a Socket.IO client — client, socket("/namespace"), on(), emit(), and acknowledgements — but it is not intended to be a drop-in replacement. The documentation includes a practical migration guide.
Use nlohmann/json directly
The original client defines a hierarchy of message types. sioxx instead uses nlohmann::json as its public value type:
namespace sioxx {
using json = nlohmann::json;
using message = json;
using message_list = json; // an array of event arguments
}
Event arguments therefore use ordinary JSON values:
auto socket = client.socket("/chat");
socket->emit("hello", sioxx::json{"world"});
socket->emit("ping_ack", sioxx::make_args(1, 2, 3),
[](sioxx::message reply) {
std::cout << "ack: " << reply.dump() << "\n";
});
sioxx::binary_message() creates a json::binary_t value from a byte buffer, so binary data uses the same message API as strings, numbers, arrays, and objects with either built-in parser.
Built-in JSON and MessagePack parsers
Socket.IO uses a parser above the Engine.IO transport. sioxx ships with two parser strategies:
-
json_parserimplements the standard Socket.IO text protocol. -
msgpack_parserimplementssocket.io-msgpack-parserusingnlohmann::json::to_msgpack()andfrom_msgpack(), with no additional MessagePack dependency.
The parser is selected per client and must match the server configuration:
sioxx::client_options options;
options.parser = sioxx::parser_kind::msgpack; // JSON is the default
sioxx::client client(options);
Applications can also provide a parser_factory. The repository demonstrates this with a CBOR parser and a matching Node.js server. Parser negotiation is intentionally not automatic, just as it is not automatic in the JavaScript clients.
Binary attachments with either parser
Both built-in parsers support binary values, including multiple or nested json::binary_t values in events and acknowledgements.
The default JSON parser implements Socket.IO's BINARY_EVENT and BINARY_ACK placeholder protocol for both encoding and decoding. It sends binary values as attachment frames and reconstructs the original JSON structure once all attachments arrive. Polling writes are serialized so the header and its attachments remain in wire order.
The same value works with either built-in parser:
auto bytes = sioxx::binary_message(
std::vector<std::uint8_t>{0x00, 0x7f, 0xff});
socket->emit("upload", sioxx::make_args(bytes));
MessagePack carries the value in one binary frame, while the JSON parser uses the standard placeholder plus attachment frames.
WebSocket and HTTP long-polling
WebSocket is the preferred transport. If the initial WebSocket connection fails, sioxx can fall back to Engine.IO v4 HTTP long-polling. Polling may also be selected from the start:
sioxx::client_options options;
options.force_http_polling = true;
sioxx::client client(options);
The polling implementation performs the Engine.IO GET/POST cycle over HTTP or HTTPS, supports extra headers and TLS verification settings, and uses Engine.IO base64 framing when a polling packet contains binary data. Each poll or write currently opens a fresh HTTP connection, and an established polling session is not upgraded back to WebSocket.
Layers and threading
The implementation follows the Socket.IO/Engine.IO split:
sioxx::client
└─ client_impl Socket.IO packets and namespace registry
└─ engineio_client Engine.IO v4 handshake and heartbeat
├─ websocket_transport Boost.Beast WebSocket (ws/wss)
└─ http_polling_transport HTTP(S) long-polling
engineio_client reads pingInterval and pingTimeout from the Engine.IO handshake, responds to server pings with pongs, and detects heartbeat timeouts. The selected parser decodes Socket.IO packets, while client_impl routes events and acknowledgements to the correct namespace socket.
Networking runs on background threads. Event handlers, acknowledgement callbacks, and lifecycle/error listeners therefore also run there. A GUI or any other thread-affine application must dispatch work back to its own thread. The optional Qt Widgets chat example shows that pattern.
Namespaces, authentication, buffering, and acknowledgements
The v0.2.0 API supports namespace authentication, custom Engine.IO paths and query parameters, catch-all listeners, and acknowledgements in both directions:
sioxx::client_options options;
options.engineio_path = "/io/";
options.query = {{"client", "desktop"}};
options.extra_headers = {{"Authorization", "Bearer example-token"}};
options.reconnect_attempts = 5;
sioxx::client client(options);
auto socket = client.socket("/chat", {{"token", "namespace-token"}});
socket->on(
"question",
[](const std::string&, sioxx::message data,
sioxx::socket::ack_callback acknowledge) {
acknowledge(sioxx::make_args("received"));
});
socket->on_any([](const std::string& event, sioxx::message data) {
std::cout << event << ": " << data.dump() << "\n";
});
client.connect("wss://example.com");
Events emitted before a namespace finishes connecting are buffered per namespace and flushed in order, including events that request acknowledgements.
Reconnection
Reconnects use capped exponential backoff. sioxx doubles the delay for each attempt and can apply symmetric jitter to reduce synchronized reconnect storms:
sioxx::client_options options;
options.reconnect_attempts = 5; // zero disables reconnection
options.reconnect_delay = std::chrono::milliseconds(1000);
options.reconnect_delay_max = std::chrono::milliseconds(30000);
options.reconnect_randomization_factor = 0.5;
CMake and packaging
sioxx currently requires CMake 3.28, a C++17 compiler, Boost 1.74 or newer, nlohmann-json 3.8 or newer, and OpenSSL. By default, CMake fetches tested Boost and JSON versions with FetchContent; applications can instead request installed packages with SIOXX_USE_SYSTEM_BOOST and SIOXX_USE_SYSTEM_JSON.
It installs an exported target, so a consumer can use:
find_package(sioxx CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE sioxx::sioxx)
Both static and shared builds are supported. A Conan 2 recipe and consumer tests are included in the repository. CMake presets exercise installation, find_package() consumption, and add_subdirectory() integration.
Tests and CI
The GoogleTest unit suite covers parsers, namespaces and socket bookkeeping, acknowledgements, Engine.IO framing and heartbeat behavior, URL parsing, polling framing, and reconnection policy without opening real sockets.
An opt-in end-to-end suite starts dedicated Node.js Socket.IO servers and verifies WebSocket and polling connections, automatic fallback, JSON and MessagePack interoperability, events and acknowledgements, binary payloads with both built-in parsers, multiple namespaces, and reconnection after an unexpected server shutdown.
GitHub Actions builds and tests on Linux with GCC and Clang, macOS with Clang, and Windows x64 and ARM64 with MSVC. Separate workflows cover CMake integration, Conan packaging, documentation, coverage, and Coverity analysis. A v* tag creates a release only after the build, integration, and packaging jobs succeed.
Try it
sudo apt install cmake ccache libssl-dev
git clone https://github.com/jfayot/sioxx
cmake -S sioxx -B sioxx/build -DCMAKE_BUILD_TYPE=Release
cmake --build sioxx/build -j
./sioxx/build/sioxx_basic_client
The matching Node.js server lives under examples/basic_client/server:
cd sioxx/examples/basic_client/server
pnpm install
pnpm start # JSON
# or: pnpm start:msgpack
# or: pnpm start:cbor
# or: pnpm start:polling
The optional Qt chat example provides a more complete integration example.
Current limitations
sioxx is still a focused client library rather than a complete port of the JavaScript Socket.IO ecosystem. In particular, it does not provide a Socket.IO server, and HTTP long-polling sessions are not upgraded to WebSocket after connection.
Issues and pull requests are welcome: github.com/jfayot/sioxx
Top comments (4)
I particularly appreciate how
sioxxsimplifies the JSON value handling by leveragingnlohmann::jsondirectly, eliminating the need for a customsio::messageclass hierarchy. The use ofjsonas a type alias fornlohmann::jsonandmessagealso improves code readability. The support for both JSON and MessagePack wire protocols through a strategy interface is also a great design choice, allowing for flexibility without adding unnecessary complexity. Have you considered adding support for other serialization formats, such as CBOR or BSON, to further enhance the library's versatility?Thanks! CBOR and BSON would indeed be relatively straightforward to add, especially since "nlohmann::json" already provides built-in conversion functions for both formats.
The main consideration is interoperability: Socket.IO officially supports JSON, while MessagePack works through the existing "socket.io-msgpack-parser" ecosystem. Supporting another format would also require a compatible parser on the server side.
That said, the strategy-based parser interface was designed precisely to make such extensions possible. One option I’m considering is exposing a parser setter so users can plug in their own parser implementation. This would allow CBOR, BSON, or other serialization formats to be supported without adding each one directly to the core library.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.