Show HN: BinaryRPC – Lightweight WebSocket-based RPC framework in modern C++

Jul 12, 2025 - 18:00
 0  0
Show HN: BinaryRPC – Lightweight WebSocket-based RPC framework in modern C++

BinaryRPC

🧭 Motivation

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.


✨ Highlights

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.

1. Prerequisites

  • CMake: Version 3.16 or higher.
  • C++ Compiler: A compiler with C++20 support (e.g., MSVC, GCC, Clang).
  • vcpkg: A C++ package manager from Microsoft, used for dependencies.
  • Git: For cloning the repository.

2. Building and Installing the Library

Note: Starting from version 0.1.0, BinaryRPC expects you to set the VCPKG_ROOT environment variable to your vcpkg installation path on Windows. This makes the build process portable and avoids hardcoding paths. If you do not set this variable on Windows, CMake will stop with an error.

Linux/macOS users: You don't need to set VCPKG_ROOT as the project uses system package managers on these platforms.

How to set VCPKG_ROOT (Windows only):

  • Windows (PowerShell):
    $env:VCPKG_ROOT = "C:/path/to/vcpkg"
  • Windows (CMD):
    set VCPKG_ROOT=C:/path/to/vcpkg

Replace /path/to/vcpkg with the actual path where you cloned vcpkg.

Step 2.1: Install Dependencies with vcpkg

First, ensure you have vcpkg installed and bootstrapped. Then, install all required dependencies:

Install all dependencies for binaryrpc

Core Dependencies

These are required for building and running the core framework:

Windows (vcpkg)

./vcpkg install unofficial-uwebsockets zlib boost-thread folly glog gflags fmt double-conversion --triplet x64-windows

Linux (Arch/pacman)

sudo pacman -S uwebsockets zlib boost folly glog gflags fmt double-conversion openssl usockets

Linux (Ubuntu/Debian)

sudo apt update
sudo apt install libuwebsockets-dev zlib1g-dev libboost-thread-dev libfolly-dev libgoogle-glog-dev libgflags-dev libfmt-dev libdouble-conversion-dev libssl-dev libusockets-dev

Note on folly and glog:

  • On some distributions, the package names may differ or the packages may not be available in the default repositories. In that case, you may need to build folly and glog from source. Please refer to their official documentation for build instructions.
  • Folly and glog can sometimes be incompatible on certain Linux systems. If you encounter build errors related to glog, you can try disabling glog or ensure you are using compatible versions. On Linux, BinaryRPC disables glog logging by default if there is a known incompatibility.
  • If you see an error like folly library not found!, make sure folly is installed and the library path is visible to the linker (e.g., /usr/lib or /usr/local/lib).
  • If you see an error like glog library not found!, make sure glog is installed and the library path is visible to the linker.

Optional Dependencies

Install these only if you need the corresponding features:

  • JWT-based authentication middleware:
    • Windows: ./vcpkg install jwt-cpp --triplet x64-windows
    • Linux: sudo pacman -S jwt-cpp
  • JSON payload support (e.g., for nlohmann-json):
    • Windows: ./vcpkg install nlohmann-json --triplet x64-windows
    • Linux: sudo pacman -S nlohmann-json

You only need to install these optional dependencies if you plan to use JWT authentication or JSON-based payloads in your application or middleware.

Step 2.2: Configure, Build, and Install BinaryRPC

This process will compile the library and install its headers and binaries into a standard system location, making it available for other projects.

# 1. Clone the repository
git clone https://github.com/efecan0/binaryrpc-framework.git
cd binaryrpc

# 2. Create a build directory
cmake -E make_directory build
cd build

# 3. Configure the project with the vcpkg toolchain
#    Use the VCPKG_ROOT environment variable for portability
cmake .. -DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake

# 4. Build the library
cmake --build . --config Release

# 5. Install the library
#    On Linux/macOS, you may need to run this with sudo
cmake --install . --config Release

For Linux/macOS users:

# 1. Clone the repository
git clone https://github.com/efecan0/binaryrpc-framework.git
cd binaryrpc

# 2. Create a build directory
cmake -E make_directory build
cd build

# 3. Configure the project (no toolchain file needed on Linux/macOS)
cmake ..

# 4. Build the library
cmake --build . --config Release

# 5. Install the library
#    You may need to run this with sudo
cmake --install . --config Release

After this step, BinaryRPC is installed on your system and can be found by any other CMake project using find_package(binaryrpc).

3. Building and Running the Examples (Optional)

The examples directory contains a separate project that demonstrates how to use the installed library.

# From the root of the binaryrpc repository, navigate to the examples
cd examples

# Create a build directory
cmake -E make_directory build
cd build

# Configure the example project. CMake will find the installed library.
# No toolchain file needed on Linux/macOS.
cmake ..

# Build the examples
cmake --build . --config Release

# Run the basic server example
./basic_server

4. Using BinaryRPC in Your Own Project

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)

🚪 Custom handshake & authentication (IHandshakeInspector)

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 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::arrayuint8_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());

Connection Parameters

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.

Alternative: JWT-based Handshake

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 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());

Reusing an active session

If the inspector returns a ClientIdentity whose sessionToken matches a live session (i.e. inside sessionTtlMs) with the same clientId + deviceId, BinaryRPC will:

  1. Attach the new socket to that session.

  2. Replay all QoS‑1/2 frames still waiting in its outbox, so the client sees every offline message.

  3. 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.


🚦 Reliability & QoS

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. DATAclient
QoSLevel::AtLeastOnce At‑least‑once. Server retries until client ACKs. DATAACK
QoSLevel::ExactlyOnce Exactly‑once. Two‑phase commit eliminates duplicates. PREPAREPREPARE_ACKCOMMITCOMPLETE

Why sessionTtlMs matters 🤔

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.

ReliableOptions

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 backoffStrategy; // pluggable curve

};

Custom back‑off policy

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;isize_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()

});

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.


🗄️ Session management via FrameworkAPI

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() };

Lifecycle notifications (planned)

SessionManager currently does not expose built‑in onCreate/onDestroy callbacks. If you need presence or audit logging today, implement it via:

  1. Middleware – add a global middleware; on first RPC from a session record "online", and call fw.disconnect() after timeout to record "offline".

  2. 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 flag – when to flip it

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.

Example: Persist state and target users

// 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}}));

}

🗺️ Architecture Overview

+-----------------------------+
| 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.


🛠️ Customisation Cheat‑Sheet

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; Attach via App::use / useFor.
Plugins (lifecycle) Implement IPlugin::initialize(); register with App::usePlugin.
Session fields fw.setField(sid, key, value, indexed) / fw.getField(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).

🔄 Middleware Management

BinaryRPC provides three ways to attach middleware to your application:

1. Global Middleware (use)

What's Your Reaction?

Like Like 0
Dislike Dislike 0
Love Love 0
Funny Funny 0
Angry Angry 0
Sad Sad 0
Wow Wow 0