While working at my company, I had previously developed a WebSocket server prototype in Java. However, over time, we started to experience performance issues. This led me to turn to C++, a language that offers more speed and low-level system control. Through my research, I discovered that uWebSockets was one of the best options in terms of performance, so I began developing with this library.
However, since uWebSockets is a very "core" library, I had to handle many details myself. Even adding a simple feature required a lot of infrastructure management. During this process, I explored other libraries in the C++ ecosystem, but most were either too heavy or did not prioritize developer experience.
Having developed many projects with Node.js and Express.js in the past, I had this idea: Why not have a modern RPC framework in C++ that supports middleware and session logic, just like Express.js?
With this idea in mind, I started designing a system that includes session management, a middleware structure, and an RPC handler architecture. At first, it was just a dream, but over time I built the building blocks of that dream one by one. I laid the foundations of this framework before graduating, and after graduation, I decided to make it open source as both a gift to myself and a contribution to the developer community.
Although I sometimes got lost in the vast control that C++ offers, I progressed step by step to bring it to its current state. I tried to include the essential things a developer might need. I hope you enjoy using it and encounter no issues, because this project is, in fact, a heartfelt contribution that a newly graduated engineer wanted to offer to the world.
In the future, I aim to evolve this architecture into an actor-mailbox model. However, I believe the current structure needs to be further solidified first. If you have any suggestions or contributions regarding this process, please feel free to contact me.
BinaryRPC is a high‑throughput RPC framework built on top of uWebSockets.
It is designed for latency-sensitive applications such as multiplayer games, financial tick streams, and IoT dashboards, delivering ultra-low latency and minimal overhead. With a modular architecture and efficient networking, BinaryRPC remains lightweight both in resource usage and developer experience.
Capability | Description |
---|---|
⚡ WebSocket transport | Blazing‑fast networking powered by uWebSockets and epoll /kqueue . |
🔄 Configurable reliability (QoS) | None , AtLeastOnce , ExactlyOnce with retries, ACKs & two‑phase commit, plus pluggable back‑off strategies & per‑session TTL. |
🧩 Pluggable layers | Drop‑in protocols (SimpleText , MsgPack, …), transports, middleware & plugins. |
🧑🤝🧑 Stateful sessions | Reconnect‑friendly Session objects with automatic expiry & indexed fields. |
🛡️ Middleware chain | JWT auth, token bucket rate‑limiter and any custom middleware you write. |
🔌 Header‑only core | Almost all of BinaryRPC lives in headers – just add include path. |
This project is a modern C++ RPC framework with several external dependencies. To use it, you should first install the required dependencies (see below), then build the project, and finally link it in your own project. The example_server directory demonstrates typical usage.
- CMake: Version 3.16 or higher.
- C++ Compiler: A compiler with C++20 support (e.g., MSVC, GCC, Clang).
- Git: For cloning the repository.
This process will compile the library.
# 1. Clone the repository
git clone https://github.com/efecan0/binaryrpc-framework.git --recurse-submodules
cd binaryrpc-framework
# 2. Configure the build
cmake --preset release
# 3. Build the library
cmake --build build --config Release
# 4. Install the library
# You can specify a prefix directory by appending --prefix=INSERT_YOUR_DIRECTORY
cmake --install build --config Release
To use BinaryRPC in your own CMake project, simply add the following to your CMakeLists.txt
:
# Find the installed binaryrpc package
find_package(binaryrpc 0.1.0 REQUIRED)
# ...
# Link your executable or library to binaryrpc
# The binaryrpc::core target will automatically bring in all necessary
# dependencies and include directories.
target_link_libraries(your_target_name PRIVATE binaryrpc::core)
The first thing every WebSocket upgrade hits is an IHandshakeInspector
. The default one simply accepts the socket and builds a trivial ClientIdentity
from the URL query‑string. In production you almost always want to sub‑class it.
Below is a condensed version of the CustomHandshakeInspector
actually running in the reference chat server:
class CustomHandshakeInspector : public IHandshakeInspector {
public:
std::optional<ClientIdentity> extract(uWS::HttpRequest& req) override {
// --- 1. Parse query‑string ------------------------------
// Expected → ws://host:9010/?clientId=123&deviceId=456&sessionToken=ABCD…
std::string q{req.getQuery()};
auto [clientId, deviceIdStr, tokenHex] = parseQuery(q);
if (clientId.empty() || deviceIdStr.empty())
return std::nullopt; // missing mandatory IDs
// --- 2. Validate deviceId is numeric --------------------
int deviceId;
try {
deviceId = std::stoi(deviceIdStr);
} catch (...) {
return std::nullopt;
}
// --- 3. Check persistent session file ------------------
if (!sessionFileContains(clientId, deviceIdStr))
return std::nullopt; // kick unknown combos
// --- 4. Build / generate sessionToken ------------------
std::array<std::uint8_t, 16> token{};
if (tokenHex.size() == 32)
hexStringToByteArray(tokenHex, token); // reuse supplied token
else
token = sha256_16(clientId + ':' + deviceIdStr + ':' + epochMs());
// --- 5. Emit identity ----------------------------------
ClientIdentity id;
id.clientId = clientId;
id.deviceId = deviceId;
id.sessionToken = token;
return id; // non‑nullopt ⇒ upgrade success
}
};
// Inspector'ı WebSocket sunucusuna ata
ws->setHandshakeInspector(std::make_shared<CustomHandshakeInspector>());
The sample inspector above expects the client to provide three query parameters on the WebSocket URL:
clientId
: An opaque user string (e.g., email or database ID).deviceId
: An integer identifying the physical device.sessionToken
(optional): A 32-character hex string to resume a previous session.
If extract()
returns std::nullopt
, the upgrade is rejected with a 400 Bad Request. If the sessionToken
is absent or invalid, a secure inspector should generate a new one and provide it to the client after a successful connection.
If you prefer to use standards like JSON Web Tokens (JWT) for authentication, you can write an inspector that checks for an HTTP header instead of query parameters.
// Forward declarations for your business logic
ClientIdentity makeClientFromJwt(const std::string& token);
bool verifyJwt(const std::string& token);
class JwtInspector : public IHandshakeInspector {
public:
std::optional<ClientIdentity> extract(uWS::HttpRequest& req) override {
// This is illustrative. You would use req.getHeader("x-access-token").
std::string_view token_sv = req.getHeader("x-access-token");
if (token_sv.empty()) {
return std::nullopt;
}
std::string token{token_sv};
if (verifyJwt(token)) {
return makeClientFromJwt(token);
}
return std::nullopt;
}
};
// In your main setup:
// ws->setHandshakeInspector(std::make_shared<JwtInspector>());
If the inspector returns a ClientIdentity
whose sessionToken
matches a live session (i.e. inside sessionTtlMs
) with the same clientId
+ deviceId
, BinaryRPC will:
-
Attach the new socket to that session.
-
Replay all QoS‑1/2 frames still waiting in its outbox, so the client sees every offline message.
-
Retain all custom fields you stored via
FrameworkAPI
(e.g. inventories, lobby, XP) – the client continues as if the connection had never dropped.
No extra code on your side – just be sure your inspector passes back the exact 16‑byte token previously issued.
BinaryRPC offers three delivery tiers inspired by MQTT but adapted for WebSockets:
Level | Guarantees | Frame flow |
---|---|---|
QoSLevel::None |
At‑most‑once. Fire‑and‑forget – lowest latency. | DATA → client |
QoSLevel::AtLeastOnce |
At‑least‑once. Server retries until client ACKs. | DATA ↔ ACK |
QoSLevel::ExactlyOnce |
Exactly‑once. Two‑phase commit eliminates duplicates. | PREPARE ↔ PREPARE_ACK ↔ COMMIT ↔ COMPLETE |
sessionTtlMs
defines how long BinaryRPC keeps a disconnected client's session alive in memory and on‑disk outbox:
-
Seamless reconnects – mobile/Wi‑Fi users frequently drop for a few seconds. As long as they re‑join within the TTL, the framework stitches the new socket onto the old
Session
so no one re‑logs or re‑syncs. -
Reliable offline push – any QoS‑1/2 frames queued meanwhile are flushed the instant the user is back online, preserving order.
-
Duplicate shielding – ExactlyOnce's commit ledger is also retained, so retries from the old socket are recognised and ignored.
Set it small (e.g. 5 s) for kiosk/lan apps to free RAM aggressively; crank it up (minutes) for flaky mobile networks or game lobbies that reconnect on map load.
With ExactlyOnce BinaryRPC persists outbound frames to the session outbox. If the socket drops but the session survives (sessionTtlMs
), any queued frames are auto‑replayed on reconnect – offline messaging for free.
Configure granular behaviour via WebSocketTransport::setReliable(options)
:
struct ReliableOptions {
QoSLevel level = QoSLevel::None; // delivery tier
std::uint32_t baseRetryMs = 100; // initial retry delay
std::uint32_t maxBackoffMs = 2'000; // cap for delay
std::uint16_t maxRetry = 5; // fail after N attempts (0 = infinite)
std::uint32_t sessionTtlMs = 30'000; // resilience window for reconnect & offline push
std::shared_ptr<IBackoffStrategy> backoffStrategy; // pluggable curve
};
Supply your own IBackoffStrategy
implementation:
class FibonacciBackoff : public IBackoffStrategy {
std::chrono::milliseconds nextDelay(std::size_t n) const override {
if(n < 2) return {100};
std::size_t a=0,b=1;
for(std::size_t i=0;i<n;++i){
std::size_t t=a+b; a=b; b=t;
}
return std::chrono::milliseconds(std::min(b*100UL, 5'000UL));
}
};
ws->setReliable({
.level = QoSLevel::AtLeastOnce,
.baseRetryMs = 100,
.backoffStrategy = std::make_shared<FibonacciBackoff>()
});
Your strategy receives the attempt index (0‑based) and returns the delay before the next retry. It may be stateless or use RNG for jitter.
FrameworkAPI
glues the transport and the lock‑free SessionManager
.
Use it to store, search and push data to any connected (or recently disconnected!) client.
using namespace binaryrpc;
// helper bound to current runtime
auto& app = App::getInstance();
FrameworkAPI fw{ &app.getSessionManager(), app.getTransport() };
SessionManager
currently does not expose built‑in onCreate/onDestroy callbacks. If you need presence or audit logging today, implement it via:
-
Middleware – add a global middleware; on first RPC from a session record "online", and call
fw.disconnect()
after timeout to record "offline". -
Custom plugin – poll
app.getSessionManager().allSessions()
every few seconds and diff lists, then emit events.
Native hooks are on the roadmap; once merged you'll be able to register lambdas before app.run()
. Follow issue #42 in the repo for progress.
indexed |
Behaviour | Complexity |
---|---|---|
false (default) |
Value kept only in session blob. | Look‑up ⇒ O(N) scan. |
true |
Value also put into a global hash‑map. | fw.findBy() ⇒ O(1). |
Use indexed=true
for identifiers you filter on (e.g. userId
, roomId
) and keep transient or high‑cardinality data unindexed.
Session
objects live until you explicitly disconnect()
them or the transport's sessionTtlMs
expires. All QoS delivery guarantees survive reconnects within that TTL.
// 1️⃣ Persist state (optionally indexed)
fw.setField(ctx.sessionId(), "userId", userId, /*indexed=*/true);
fw.setField(ctx.sessionId(), "username", username); // not indexed
fw.setField(ctx.sessionId(), "xp", 0);
// 2️⃣ Target users later
for (auto& s : fw.findBy("userId", std::to_string(userId))) {
fw.sendToSession(s, app.getProtocol()->serialize("levelUp", {{"lvl", newLvl}}));
}
+-----------------------------+
| raw bytes --> IProtocol |
| (parse) |
+--------------+--------------+
|
v
+--------------+--------------+
| ParsedRequest |
+--------------+--------------+
|
v
+--------------+--------------+
| MiddlewareChain (*) |
+--------------+--------------+
|
v
+--------------+--------------+
| RpcContext & SessionManager |
+--------------+--------------+
|
v
+--------------+--------------+
| RPCManager |
+--------------+--------------+
|
v
+--------------+--------------+
| ITransport (send) |
| <--> client |
+-----------------------------+
All rectangles are replaceable: implement the interface and plug your own.
What you want to change | How |
---|---|
QoS level / retries | WebSocketTransport::setReliable(ReliableOptions) . Fields: level , baseRetryMs , maxRetry , maxBackoffMs , sessionTtlMs , backoffStrategy . |
Back‑off curve | Implement IBackoffStrategy::nextDelay() and pass via ReliableOptions.backoffStrategy . |
Serialisation | Implement IProtocol – just parse/serialise and throw ErrorObj on bad input. |
Transport | Implement ITransport (start/stop/send callbacks). Ready‑made: WebSocketTransport . |
Middleware | using Middleware = std::function<void(RpcContext&, Next)>; Attach via App::use / useFor . |
Plugins (lifecycle) | Implement IPlugin::initialize() ; register with App::usePlugin . |
Session fields | fw.setField(sid, key, value, indexed) / fw.getField<T>(sid, key) – choose indexed=true for fast look‑ups |
Duplicate RPC guard | Already built‑in: qos::DuplicateFilter – just call accept(payload, ttl) ; no extra wiring needed. |
Logging sink | Logger::inst().setSink([](LogLevel l, const std::string& m){ … }); + setLevel(LogLevel::Debug) . |
BinaryRPC provides three ways to attach middleware to your application:
Attaches middleware to all RPC calls:
app.use([](Session& session, const std::string& method, std::vector<uint8_t>& payload, NextFunc next) {
// Runs before every RPC
LOG_DEBUG("Incoming request: " + method);
next();
// Runs after every RPC
});
Attaches middleware to a specific RPC method:
app.useFor("login", [](Session& session, const std::string& method, std::vector<uint8_t>& payload, NextFunc next) {
// Runs only before "login" RPC
auto req = parseMsgPackPayload(payload);
if (!validateCredentials(req)) {
throw ErrorObj("Invalid credentials");
}
next();
// Runs after "login" RPC
});
Attaches middleware to multiple RPC methods:
app.useForMulti({"send_message", "join_room"}, [](Session& session, const std::string& method, std::vector<uint8_t>& payload, NextFunc next) {
// Runs before "send_message" and "join_room" RPCs
if (!session.isAuthenticated()) {
throw ErrorObj("Authentication required");
}
next();
// Runs after these RPCs
});
- Middleware executes in the order they are registered
- Each middleware must call
next()
to continue the chain - If a middleware throws an exception, the chain stops and the error is sent to the client
- Middleware can modify the
Session
andpayload
before and after the RPC execution
- Authentication/Authorization
- Rate limiting
- Request logging
- Input validation
- Session management
- Error handling
Example of a complete middleware setup:
// Global logging
app.use([](Session& session, const std::string& method, std::vector<uint8_t>& payload, NextFunc next) {
LOG_INFO("Request: " + method);
next();
LOG_INFO("Response sent");
});
// Auth check for sensitive methods
app.useForMulti({"send_message", "join_room"}, [](Session& session, const std::string& method, std::vector<uint8_t>& payload, NextFunc next) {
if (!session.isAuthenticated()) {
throw ErrorObj("Please login first");
}
next();
});
// Rate limiting for specific method
app.useFor("login", [](Session& session, const std::string& method, std::vector<uint8_t>& payload, NextFunc next) {
if (isRateLimited(session.getClientId())) {
throw ErrorObj("Too many attempts");
}
next();
});
In addition to writing your own logic, BinaryRPC provides pre-built modules for common scenarios.
Instead of implementing the logic manually as shown in the generic example, you can use the dedicated RateLimiter
module which uses the Token Bucket algorithm.
Example: Limit login attempts to 1 per 5 seconds.
#include <binaryrpc/middlewares/rate_limiter.hpp>
#include <binaryrpc/core/app.hpp>
// Configure the RateLimiter: 1 request every 5 seconds (0.2 req/sec).
// The bucket capacity is 1, meaning no bursts are allowed.
auto loginLimiter = binaryrpc::middlewares::RateLimiter(0.2, 1);
// Attach the middleware to the "login" RPC
app.useFor("login", loginLimiter);
If a user exceeds the limit, the middleware throws an ErrorObj
, and the client receives a "Too many requests" message.
This middleware validates incoming RPCs using a JSON Web Token (JWT).
Example: Secure the "get_profile" RPC by expecting a token in the payload.
#include <binaryrpc/middlewares/jwt_auth.hpp>
#include <binaryrpc/core/app.hpp>
#include <nlohmann/json.hpp>
const std::string jwt_secret = "your-very-secret-key";
// Configure the middleware to extract the token from a "token" field in the payload.
auto authMiddleware = binaryrpc::middlewares::JwtAuth(
jwt_secret,
[](const binaryrpc::Payload& payload) -> std::string {
try {
auto data = nlohmann::json::from_msgpack(payload);
return data.value("token", "");
} catch (...) { return ""; }
}
);
// Protect methods that require authentication
app.useForMulti({"get_profile", "update_settings"}, authMiddleware);
The core of your application is the set of RPC handlers that process client requests. You register them on the App
instance.
This method binds a handler to a specific method name. The handler is a function that receives the raw payload and an RpcContext
object. The handler is responsible for parsing the incoming payload and serializing the outgoing payload before replying.
The IProtocol
interface (MsgPackProtocol
, SimpleTextProtocol
, etc.) is responsible for framing the data, not for serializing your application-specific data structures. It wraps your payload (std::vector<uint8_t>
) with a method name.
Here is a corrected example using nlohmann/json
to handle the application-level data serialization.
// main.cpp
#include "binaryrpc/core/app.hpp"
#include "binaryrpc/core/protocol/msgpack_protocol.hpp"
#include <nlohmann/json.hpp> // You'll need a JSON library
#include <iostream>
int main() {
auto& app = binaryrpc::App::getInstance();
// Use the MsgPack protocol for framing
auto protocol = std::make_shared<binaryrpc::MsgPackProtocol>();
app.setProtocol(protocol);
// Create a FrameworkAPI instance to interact with sessions
FrameworkAPI api(&app.getSessionManager(), app.getTransport());
// Register a "login" handler
app.registerRPC("login", [&](const std::vector<uint8_t>& payload, binaryrpc::RpcContext& ctx) {
nlohmann::json response_data;
try {
// 1. Parse the incoming payload
auto request = nlohmann::json::parse(payload);
std::string username = request.value("username", "");
std::string password = request.value("password", "");
// 2. Authenticate the user
if (username == "admin" && password == "password") {
std::cout << "Login successful for: " << username << std::endl;
// 3. Store state using FrameworkAPI
api.setField(ctx.session().id(), "isAuthenticated", true, false);
api.setField(ctx.session().id(), "username", username, true); // indexed
// 4. Prepare the response payload
response_data["status"] = "success";
response_data["ts"] = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
} else {
response_data["status"] = "error";
response_data["reason"] = "Invalid credentials";
}
} catch (const std::exception& e) {
std::cerr << "Error in login handler: " << e.what() << std::endl;
response_data["status"] = "error";
response_data["reason"] = "Internal server error";
}
// 5. Serialize the response data and reply
std::string response_str = response_data.dump();
std::vector<uint8_t> response_bytes(response_str.begin(), response_str.end());
// Use the protocol to frame the response
ctx.reply(protocol->serialize("login_response", response_bytes));
});
// Add an authentication middleware for "get_profile"
app.useFor("get_profile", [&](Session& session, const std::string& method, std::vector<uint8_t>& payload, NextFunc next) {
auto isAuthenticated = api.getField<bool>(session.id(), "isAuthenticated");
if (!isAuthenticated.value_or(false)) {
// User is not authenticated. Send an error response directly and stop the chain.
nlohmann::json err_data = {
{"status", "error"},
{"reason", "Authentication required"}
};
std::string err_str = err_data.dump();
std::vector<uint8_t> err_bytes(err_str.begin(), err_str.end());
// Frame the error response
auto framed_error = protocol->serialize("auth_error", err_bytes);
// Send it directly to the session and DO NOT call next()
// Note: This creates a shared_ptr copy of the session to pass to the API.
api.sendToSession(std::make_shared<Session>(session), framed_error);
return;
}
// User is authenticated, proceed to the RPC handler
next();
});
// Register another handler - now protected by middleware
app.registerRPC("get_profile", [&](const auto& payload, auto& ctx) {
// Auth check is now in the middleware, we can proceed directly.
auto username = api.getField<std::string>(ctx.session().id(), "username");
nlohmann::json profile_data;
profile_data["username"] = username.value_or("N/A");
// ... other profile data
std::string profile_str = profile_data.dump();
std::vector<uint8_t> profile_bytes(profile_str.begin(), profile_str.end());
ctx.reply(protocol->serialize("profile_data", profile_bytes));
});
app.run(9010);
return 0;
}
The RpcContext
is your gateway to interacting with the client and their session:
ctx.reply(payload)
: Sends a message back to the originating client.ctx.broadcast(payload)
: Sends a message to all connected clients.ctx.session()
: Returns a reference to theSession
object for the current client. You can use this to store and retrieve connection-specific state usingsetField
andgetField
.ctx.disconnect()
: Closes the connection.
Delivery of replies respects the QoS settings of the transport, just like sendToSession()
does (including ACKs, retries, and the offline outbox).
The sample inspector above demands three query parameters:
clientId ⇒ opaque user string (e.g. email)
deviceId ⇒ integer; each physical device gets a stable ID
sessionToken (optional) ⇒ 32‑hex chars to resume previous session
If the token is absent or invalid the server autogenerates a fresh one and returns it via a dedicated get_token
RPC.
The repository ships with a minimal reference client in index.html
. It demonstrates:
import "./binaryrpc.js";
const rpc = new BinaryRPC("ws://localhost:9010", {
onConnect() { rpc.call("join"); },
onError: console.error
});
rpc.connect("user‑42", "web", /*session*/ null);
// later
rpc.call("broadcast", { message: "hello" });
Internally the helper wraps the QoS‑2 handshake (DATA → ACK
etc.) and keeps an outbox so messages typed while offline flush once the socket is back (mirroring server‑side sessionTtlMs
). You can plug your own MessagePack/JSON codec by swapping MessagePack.encode/decode
.
If you need headers, JWT, IP checks … attach an IHandshakeInspector
:
// Forward declarations for your business logic
ClientIdentity makeClientFromJwt(const std::string& token);
bool verifyJwt(const std::string& token);
class JwtInspector : public IHandshakeInspector {
public:
std::optional<ClientIdentity> extract(uWS::HttpRequest& req) override {
// Note: uWS::HttpRequest does not have getHeader in this form.
// This is illustrative. You would use req.getHeader("x-access-token").
std::string_view token_sv = req.getHeader("x-access-token");
if (token_sv.empty()) {
return std::nullopt;
}
std::string token{token_sv};
if (verifyJwt(token)) {
return makeClientFromJwt(token);
}
return std::nullopt;
}
};
// In your main setup:
// ws->setHandshakeInspector(std::make_shared<JwtInspector>());
If extract()
returns std::nullopt
the upgrade is rejected with 400 Bad Request.
While the cheat-sheet provides quick answers, this section explains how to use the ready-to-use components that ship with BinaryRPC.
Plugins add complex, stateful capabilities to the framework.
The room_plugin.hpp
provides core functionality for chat rooms, game lobbies, or any group-based scenario.
Example: Joining a room and broadcasting a message.
#include <binaryrpc/plugins/room_plugin.hpp>
#include <binaryrpc/core/app.hpp>
#include <binaryrpc/core/framework_api.hpp>
// ... setup app and api ...
// Create and register the RoomPlugin
auto roomPlugin = std::make_unique<binaryrpc::RoomPlugin>(app.getSessionManager(), app.getTransport());
binaryrpc::RoomPlugin* room_ptr = roomPlugin.get();
app.usePlugin(std::move(roomPlugin));
// RPC for a client to join a room
app.registerRPC("join_room", [&](const auto& req, auto& ctx) {
std::string room_name(req.begin(), req.end()); // e.g., "/gaming"
room_ptr->join(room_name, ctx.session().id());
// Announce that the user has joined
std::string join_msg = "User " + ctx.session().identity().clientId + " joined.";
std::vector<uint8_t> payload(join_msg.begin(), join_msg.end());
room_ptr->broadcast(room_name, app.getProtocol()->serialize("room_event", payload));
});
The transport layer is responsible for sending and receiving raw data over the network.
The WebSocketTransport
allows you to register a callback that is triggered when a client's connection is terminated or the session expires. This is powerful for tracking user presence or cleaning up resources.
Example: Log disconnections and update user status.
#include <binaryrpc/transports/websocket/websocket_transport.hpp>
// ... other includes ...
// Create the transport
auto ws = std::make_unique<binaryrpc::WebSocketTransport>(sm, /*idleTimeoutSec=*/60);
// Set the disconnect callback
ws->setDisconnectCallback([&api](std::shared_ptr<binaryrpc::Session> session) {
if (!session) return;
const auto& identity = session->identity();
LOG_INFO("Client disconnected: " + identity.clientId);
// Update the user's status to "offline"
api.setField(session->id(), "onlineStatus", std::string("offline"), /*indexed=*/true);
});
// Set the transport on the app
app.setTransport(std::move(ws));
The framework provides several utilities in the binaryrpc/core/util/
directory.
You can redirect the framework's logs to a file or a logging service.
Example: Log to both console and a file.
#include <binaryrpc/core/util/logger.hpp>
#include <fstream>
std::ofstream logFile("server.log", std::ios::app);
// Set a custom sink
binaryrpc::Logger::inst().setSink([&logFile](binaryrpc::LogLevel lvl, const std::string& msg) {
static const char* names[]{"TRACE", "DEBUG", "INFO", "WARN", "ERROR"};
std::string log_line = "[" + std::string(names[(int)lvl]) + "] " + msg;
std::cout << log_line << std::endl;
logFile << log_line << std::endl;
});
binaryrpc::Logger::inst().setLevel(binaryrpc::LogLevel::Debug);
Throw an ErrorObj
in your RPC handlers to send a structured error to the client.
Example:
#include <binaryrpc/core/util/error_types.hpp>
app.registerRPC("place_bet", [&](const auto& req, auto& ctx) {
if (/* user has insufficient balance */) {
throw binaryrpc::ErrorObj("Insufficient balance", 1001); // (message, custom_code)
}
});
For a production‑ready browser/Node client, check binaryrpc‑client‑js → https://github.com/efecan0/binaryrpc-client-js.
import "./binaryrpc.js";
const rpc = new BinaryRPC("ws://localhost:9010", {
onConnect() { rpc.call("join"); },
onError: console.error
});
// The client expects the server's IHandshakeInspector to understand these params.
rpc.connect("user‑42", "web", /*sessionToken*/ null);
// later
rpc.call("broadcast", { message: "hello" });
The helper wraps the QoS handshake and queues outbound messages while offline. If you prefer another codec, simply replace MessagePack.encode/decode.
BinaryRPC's C++ unit tests are written using the Catch2 framework. You need to install Catch2 before you can build and run the tests.
- Windows (vcpkg):
./vcpkg install catch2 --triplet x64-windows
- Linux (Arch/pacman):
sudo pacman -S catch2
- Linux (Ubuntu/Debian):
sudo apt install catch2
Note: On some systems, Catch2's CMake config files (
Catch2Config.cmake
) may be missing. In that case, you can build and install Catch2 from source by following the instructions on the Catch2 GitHub page.
By default, the BINARYRPC_BUILD_TESTS
option in the main CMakeLists.txt is ON, so tests are built automatically.
- To build the tests:
cmake .. -DBINARYRPC_BUILD_TESTS=ON cmake --build . --config Release
- To run all tests:
ctest --output-on-failure
- Or you can run the test binaries directly:
./tests/binaryrpc_unit_tests ./tests/binaryrpc_qos_tests
Some integration and memory leak tests are written in Python. To run them:
- Make sure you have Python 3 and the required packages installed. You can install dependencies with:
pip install -r tests/integration_tests_py/requirements.txt
- To run the tests, simply execute the scripts directly:
# Example: Run a memory leak test python tests/Memory_leak_stress_test_py/memory_leak_test.py # Example: Run an integration test python tests/integration_tests_py/session/app.py