The oatpp-socketio project is an implementation of the Socket.IO / Engine.IO, v4 protocol in C++, specifically for OAT++.
Status engine.io v4 and the socket.io v4 text protocol are implemented and covered by a test suite: long-polling and websocket transports, the engine.io ping/pong keepalive, namespaces, publish/subscribe, and event/ack round-trips. Still open: binary packets (5/6), socket.io disconnect/ack packets on the inbound path, and maxPayload, which is advertised in the engine.io OPEN packet but not yet enforced on what comes back. See Limitations.
The layering follows the two tiers of the Socket.IO protocol:
include/oatpp_sio/eio/ engine.io v4: packet framing, the connection store,
long-polling and websocket transports
include/oatpp_sio/sio/ socket.io v4: the wire codec, namespaces (Space),
the per-connection engine.io <-> socket.io bridge
(SioAdapter), the namespace registry (SioServer),
the auth hook (AuthPlugin)
include/oatpp_sio/webapi/ oatpp glue: controllers, DTOs, app components
src/ implementations
app/ two runnable servers
test/ unit + integration suites (see Testing below)
This uses minimal prerequisites, esp.:
- CMake (build project)
- Oatpp (used for HTTP handling)
- oatpp-websockets (websockets implementation)
- oatpp-Swagger (for API documentation)
As this has been implemented on Linux, build the oatpp prerequisites with the shared library option turned on.
mkdir build
cd build
cmake ..cmake --build build -j$(nproc)./build/app/engineio_server # engine.io only
./build/app/sio_server # engine.io + socket.ioThen stop it with Ctrl-C (or SIGTERM); both servers shut the web api down before exiting.
| environment variable | default | meaning |
|---|---|---|
OATPP_SIO_HOST |
0.0.0.0 |
listen address |
OATPP_SIO_PORT |
8000 |
listen port |
WEBAPI |
1 |
0 turns the swagger UI off |
The Swagger API documentation is then at http://localhost:8000/swagger/ui#/
and the OpenAPI description at /api-docs/oas-3.0.0.json.
A client does not get to decide what namespaces exist. "/" is always
there; anything else has to be declared, the same way the reference server
requires io.of("/chat"):
#include "oatpp_sio/sio/sioServer.hpp"
oatpp_sio::sio::SioServer::serverInstance().newSpace("/chat");A CONNECT for a name that was not declared is answered with
connect_error {"message": "Invalid namespace"} and the namespace is not
created. The engine.io connection stays up, because one transport carries
all the namespaces this client is on - being refused for one is not an error
on the others.
If your application really wants clients to invent namespaces, say so:
SioServer::serverInstance().setAutoCreateSpaces(true);Look a namespace up with findSpace() (returns null, never throws, creates
nothing) rather than getSpace() (throws; creates if auto-create is on).
dropSpace() retires one again and refuses "/" and any namespace that
still has members.
AuthPlugin is asked before a client joins a namespace, and again before an
event is published into one:
class ChatAuth : public oatpp_sio::sio::AuthPlugin {
public:
bool mayConnect(const std::string& spaceName,
const SpaceListener::Ptr& listener,
std::string& reason) override {
if (!tokenChecksOut(listener)) {
reason = "chat needs a ticket"; // -> connect_error {"message": reason}
return false;
}
return true;
}
};
SioServer::serverInstance().setAuthPlugin(std::make_shared<ChatAuth>());There is always a plugin in force - AllowAllAuth until you install another
one, so forgetting to configure one fails open rather than locking everyone
out, and setAuthPlugin(nullptr) is ignored with a warning. mayPublish()
defaults to allow and can only narrow what a connection may do: the check
that a connection may only publish into namespaces it actually joined is not
configurable and is not a plugin's to switch off. Swapping a plugin while
traffic is running is safe — each connect and publish works from its own copy
of the pointer — but installing it before webApiStart() is still the tidy
thing to do.
cmake -DBUILD_TESTS=ON .. && cmake --build build -j$(nproc)
ctest --test-dir build --output-on-failureTwo binaries, both driven through CTest:
unit.*- protocol building blocks, no sockets: the engine.io framing, the socket.io wire codec (table-driven against the reference decoder, plus a truncation sweep over hostile inputs), namespace registry and pub/sub bookkeeping, session ids, the auth hook. Each unit test is its own CTest process, so a hard crash in one cannot take the others with it, and each one checks the global oatpp object count for leaks.integration.all- starts the web api in-process on a local port and talks to it with hand-written protocol clients (raw HTTP over BSD sockets, a minimal RFC 6455 client, and an engine.io long-poll client). Deliberately not oatpp's client and not a third-party socket.io client: the point is to be able to send what a hostile or merely nonconforming client sends.
Run one test, or list them:
./build/test/sio-unit-tests WireTest
./build/test/sio-integration-tests SocketIoTest
./build/test/sio-integration-tests --list
ctest --test-dir build -R unit.SpaceTest -V # -V is ctest's own flagTo check that a test really tests the behaviour it claims, revert the behaviour and see whether anything notices:
./tools_mutation_check.py --list
./tools_mutation_check.py- Inbound socket.io packets:
connectandeventare handled;disconnect,ack,connect_errorand both binary packet types are logged and ignored. A client that leaves a namespace the polite way therefore leaves its subscription behind until the transport goes away. - A refused publish is silent: if the client asked for an ack, its ack callback is never called.
- Long-polling ends an idle poll with an empty
200; the reference writes apingonto the outstanding poll instead. - The wire codec is more lenient than the reference decoder in two places
(
2/chat["x"]and2/["x"]); seesio/wire.hpp. Space::size()returnsintbuilt from asize_t.
The namespace registry and the per-namespace subscription maps are
synchronised, so declaring, retiring and joining namespaces under traffic is
supported; dropSpace() refuses a namespace that has members, and the check
and the removal are atomic against a join.
Parts of this project have been developed with funding from the European Union’s Horizon Europe research and innovation programme under Grant Agreement No 101070599.