DEV Community

Jérôme
Jérôme

Posted on Edited on

sioxx — a modern C++ socket.io client (nlohmann/json + Boost.Beast, JSON or MessagePack)

Updated in August 2026: this article reflects sioxx v0.2.0 and the latest additions on main, 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.

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
}
Enter fullscreen mode Exit fullscreen mode

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";
             });
Enter fullscreen mode Exit fullscreen mode

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_parser implements the standard Socket.IO text protocol.
  • msgpack_parser implements socket.io-msgpack-parser using nlohmann::json::to_msgpack() and from_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);
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
topstar_ai profile image
Luis Cruz

I particularly appreciate how sioxx simplifies the JSON value handling by leveraging nlohmann::json directly, eliminating the need for a custom sio::message class hierarchy. The use of json as a type alias for nlohmann::json and message also 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?

Collapse
 
jfayot profile image
Jérôme

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.