├── tests ├── integration_tests_py │ ├── requirements.txt │ ├── qos_tests │ │ └── pytest.ini │ ├── middleware │ │ └── app.py │ ├── error_propagation │ │ └── app.py │ ├── Concurrency_and_multiple_clients │ │ └── app.py │ ├── session │ │ ├── app.py │ │ └── advanced_app.py │ ├── e2e │ │ └── ws_e2e_test.py │ └── general │ │ └── advanced_app.py ├── catch2_main.cpp ├── qos_mock_transport.hpp ├── mock_client.cpp ├── CMakeLists.txt ├── mock_client.hpp ├── rate_limiter.test.cpp ├── generic_index.test.cpp ├── qos.test.cpp └── session_manager.test.cpp ├── Doxyfile ├── include └── binaryrpc │ ├── plugins │ ├── reliable_plugins.xpp │ └── room_plugin.hpp │ ├── core │ ├── types.hpp │ ├── interfaces │ │ ├── iplugin.hpp │ │ ├── IBackoffStrategy.hpp │ │ ├── iprotocol.hpp │ │ ├── IHandshakeInspector.hpp │ │ └── itransport.hpp │ ├── util │ │ ├── error_types.hpp │ │ ├── qos.hpp │ │ ├── DefaultInspector.hpp │ │ ├── logger.hpp │ │ └── thread_pool.hpp │ ├── strategies │ │ ├── linear_backoff.hpp │ │ └── exponential_backoff.hpp │ ├── auth │ │ └── ClientIdentity.hpp │ ├── protocol │ │ ├── simple_text_protocol.hpp │ │ └── msgpack_protocol.hpp │ ├── middleware │ │ └── middleware_chain.hpp │ ├── rpc │ │ └── rpc_context.hpp │ ├── framework_api.hpp │ ├── app.hpp │ └── session │ │ └── session.hpp │ ├── binaryrpc.hpp │ ├── middlewares │ ├── jwt_auth.hpp │ └── rate_limiter.hpp │ └── transports │ └── websocket │ └── websocket_transport.hpp ├── vcpkg.json ├── .gitmodules ├── example_server ├── CMakeLists.txt └── utils │ └── parser.hpp ├── .clang-tidy ├── CMakeSettings.json ├── CMakePresets.json ├── src ├── internal │ └── core │ │ ├── util │ │ ├── byteorder.hpp │ │ ├── time.hpp │ │ ├── random.hpp │ │ ├── hex.hpp │ │ └── conn_state.hpp │ │ ├── session │ │ └── generic_index.hpp │ │ ├── rpc │ │ └── rpc_manager.hpp │ │ └── qos │ │ └── duplicate_filter.hpp ├── core │ ├── rpc │ │ ├── rpc_context.cpp │ │ └── rpc_manager.cpp │ ├── protocol │ │ ├── simple_text_protocol.cpp │ │ └── msgpack_protocol.cpp │ ├── middleware │ │ └── middleware_chain.cpp │ ├── session │ │ ├── session.cpp │ │ └── generic_index.cpp │ ├── util │ │ ├── DefaultInspector.cpp │ │ └── thread_pool.cpp │ └── framework_api.cpp └── plugins │ └── room_plugin.cpp ├── LICENSE ├── .gitignore ├── test_server ├── main.cpp ├── error_propagation_test.cpp ├── session_test.cpp ├── advanced_general_server.cpp ├── qos2_test.cpp ├── qos1_test.cpp └── middleware_test.cpp ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── RoadMap.md ├── CMakeLists.txt ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md /tests/integration_tests_py/requirements.txt: -------------------------------------------------------------------------------- 1 | websockets -------------------------------------------------------------------------------- /tests/catch2_main.cpp: -------------------------------------------------------------------------------- 1 | // tests/catch2_main.cpp 2 | #define CATCH_CONFIG_MAIN 3 | #include 4 | -------------------------------------------------------------------------------- /Doxyfile: -------------------------------------------------------------------------------- 1 | INPUT = include/ src/ 2 | RECURSIVE = YES 3 | EXTRACT_ALL = YES 4 | GENERATE_HTML = YES 5 | GENERATE_LATEX = NO 6 | PROJECT_NAME = "BinaryRpc" -------------------------------------------------------------------------------- /include/binaryrpc/plugins/reliable_plugins.xpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/efecan0/binaryrpc-framework/HEAD/include/binaryrpc/plugins/reliable_plugins.xpp -------------------------------------------------------------------------------- /tests/integration_tests_py/qos_tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -ra -q 3 | markers = 4 | asyncio: mark async tests for pytest-asyncio 5 | asyncio_mode = strict 6 | asyncio_default_fixture_loop_scope = function 7 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "binaryrpc-framework", 3 | "version": "0.1.0", 4 | "dependencies": [ 5 | "catch2", 6 | "nlohmann-json", 7 | "uwebsockets", 8 | "msgpack", 9 | "jwt-cpp" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vcpkg"] 2 | path = vcpkg 3 | url = https://github.com/microsoft/vcpkg.git 4 | [submodule "third_party/unordered_dense"] 5 | path = third_party/unordered_dense 6 | url = https://github.com/martinus/unordered_dense.git 7 | -------------------------------------------------------------------------------- /example_server/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.25) 2 | 3 | set(CMAKE_CXX_STANDARD 20) 4 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 5 | 6 | add_executable(basic_chat basic_chat.cpp) 7 | 8 | find_package(OpenSSL REQUIRED) 9 | 10 | 11 | target_link_libraries(basic_chat PRIVATE binaryrpc_core 12 | OpenSSL::Crypto 13 | ) -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | Checks: > 2 | clang-analyzer-*, 3 | bugprone-*, 4 | performance-*, 5 | modernize-use-nullptr, 6 | -modernize-use-trailing-return-type, 7 | -readability-identifier-length, 8 | -readability-braces-around-statements 9 | 10 | WarningsAsErrors: clang-analyzer-*;bugprone-* 11 | HeaderFilterRegex: '^(?!.*vcpkg).*' # 3rd-party dahil etme 12 | FormatStyle: file 13 | -------------------------------------------------------------------------------- /CMakeSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "environments": [ 3 | { 4 | "CMAKE_TOOLCHAIN_FILE": "C:/DEV/vcpkg/scripts/buildsystems/vcpkg.cmake" 5 | } 6 | ], 7 | "configurations": [ 8 | { 9 | "name": "x64-Debug", 10 | "generator": "Ninja", 11 | "configurationType": "Debug", 12 | "buildRoot": "${projectDir}\\out\\build\\${name}", 13 | "installRoot": "${projectDir}\\out\\install\\${name}", 14 | "cmakeCommandArgs": "", 15 | "buildCommandArgs": "", 16 | "ctestCommandArgs": "", 17 | "inheritEnvironments": [ "msvc_x64_x64" ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /include/binaryrpc/core/types.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace binaryrpc { 8 | 9 | class Session; // Forward-declaration 10 | class RpcContext; // Forward-declaration 11 | using NextFunc = std::function; 12 | using Middleware = std::function&, NextFunc)>; 13 | using RpcContextHandler = 14 | std::function&, RpcContext&)>; 15 | 16 | enum class SendMode { 17 | Client, 18 | Broadcast 19 | }; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 6, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 16 6 | }, 7 | "configurePresets": [ 8 | { 9 | "name": "base", 10 | "hidden": true, 11 | "generator": "Ninja", 12 | "binaryDir": "${sourceDir}/build", 13 | "cacheVariables": { 14 | "CMAKE_TOOLCHAIN_FILE": "./vcpkg/scripts/buildsystems/vcpkg.cmake" 15 | } 16 | }, 17 | { 18 | "name": "development", 19 | "inherits": "base", 20 | "cacheVariables": { 21 | "CMAKE_BUILD_TYPE": "RelWithDebInfo", 22 | "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" 23 | } 24 | }, 25 | { 26 | "name": "release", 27 | "inherits": "base", 28 | "cacheVariables": { 29 | "CMAKE_BUILD_TYPE": "Release" 30 | } 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/internal/core/util/byteorder.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #ifdef _WIN32 4 | #include 5 | #else 6 | #include 7 | #endif 8 | 9 | namespace binaryrpc { 10 | /** 11 | * @brief Converts a 64-bit integer from host to network byte order (big endian). 12 | * @param value The value to convert. 13 | * @return The value in network byte order. 14 | */ 15 | inline uint64_t hostToNetwork64(uint64_t value) { 16 | #ifdef _WIN32 17 | return _byteswap_uint64(value); 18 | #else 19 | return htobe64(value); 20 | #endif 21 | } 22 | 23 | /** 24 | * @brief Converts a 64-bit integer from network byte order (big endian) to host byte order. 25 | * @param value The value to convert. 26 | * @return The value in host byte order. 27 | */ 28 | inline uint64_t networkToHost64(uint64_t value) { 29 | #ifdef _WIN32 30 | return _byteswap_uint64(value); 31 | #else 32 | return be64toh(value); 33 | #endif 34 | } 35 | } -------------------------------------------------------------------------------- /include/binaryrpc/core/interfaces/iplugin.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file iplugin.hpp 3 | * @brief Interface for plugins in BinaryRPC. 4 | * 5 | * Defines the IPlugin interface for implementing custom plugins that can be loaded 6 | * into the BinaryRPC framework. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | 13 | namespace binaryrpc { 14 | 15 | /** 16 | * @class IPlugin 17 | * @brief Interface for custom plugins in BinaryRPC. 18 | * 19 | * Implement this interface to provide custom plugin functionality for the framework. 20 | */ 21 | class IPlugin { 22 | public: 23 | virtual ~IPlugin() = default; 24 | /** 25 | * @brief Initialize the plugin. Called when the plugin is loaded. 26 | */ 27 | virtual void initialize() = 0; 28 | /** 29 | * @brief Get the name of the plugin. 30 | * @return Name of the plugin as a C-string 31 | */ 32 | virtual const char* name() const = 0; 33 | }; 34 | 35 | } -------------------------------------------------------------------------------- /include/binaryrpc/binaryrpc.hpp: -------------------------------------------------------------------------------- 1 | // This is the single entry point for the BinaryRPC library. 2 | // Include this file to get access to the core public API. 3 | 4 | #pragma once 5 | 6 | // Core application and framework 7 | #include "binaryrpc/core/app.hpp" 8 | #include "binaryrpc/core/framework_api.hpp" 9 | 10 | // Core data types 11 | #include "binaryrpc/core/types.hpp" 12 | 13 | // RPC and Session context 14 | #include "binaryrpc/core/rpc/rpc_context.hpp" 15 | #include "binaryrpc/core/session/session.hpp" 16 | #include "binaryrpc/core/session/session_manager.hpp" 17 | 18 | // Client identity for authentication 19 | #include "binaryrpc/core/auth/ClientIdentity.hpp" 20 | 21 | // Public interfaces for extension 22 | #include "binaryrpc/core/interfaces/iplugin.hpp" 23 | #include "binaryrpc/core/interfaces/iprotocol.hpp" 24 | #include "binaryrpc/core/interfaces/itransport.hpp" 25 | #include "binaryrpc/core/interfaces/IHandshakeInspector.hpp" 26 | #include "binaryrpc/core/interfaces/IBackoffStrategy.hpp" 27 | 28 | // Common utilities 29 | #include "binaryrpc/core/util/logger.hpp" 30 | #include "binaryrpc/core/util/qos.hpp" -------------------------------------------------------------------------------- /include/binaryrpc/core/interfaces/IBackoffStrategy.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file IBackoffStrategy.hpp 3 | * @brief Interface for custom retry backoff strategies in BinaryRPC. 4 | * 5 | * Defines the IBackoffStrategy interface for implementing custom retry backoff algorithms 6 | * used in reliable message delivery (QoS) scenarios. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include 13 | 14 | namespace binaryrpc { 15 | 16 | /** 17 | * @class IBackoffStrategy 18 | * @brief Interface for custom retry backoff strategies. 19 | * 20 | * Implement this interface to provide custom backoff algorithms for retrying message delivery. 21 | */ 22 | class IBackoffStrategy { 23 | public: 24 | virtual ~IBackoffStrategy() = default; 25 | 26 | /** 27 | * @brief Calculate the next backoff delay. 28 | * @param attempt Current retry attempt (starting from 1) 29 | * @return Duration to wait before the next retry 30 | */ 31 | virtual std::chrono::milliseconds nextDelay(uint32_t attempt) const = 0; 32 | }; 33 | 34 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 BinaryRPC Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tests/integration_tests_py/middleware/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import websockets 3 | 4 | async def test_rpc(method, payload): 5 | uri = "ws://127.0.0.1:9000" 6 | headers = { 7 | "x-client-id": "1", 8 | "x-device-id": "dev_1" 9 | } 10 | async with websockets.connect(uri, additional_headers=headers) as websocket: 11 | message = f"{method}:{payload}" 12 | binary_message = message.encode('utf-8') 13 | 14 | print(f"[Client] Sending binary frame: {binary_message}") 15 | await websocket.send(binary_message) 16 | 17 | response = await websocket.recv() 18 | 19 | if isinstance(response, bytes): 20 | decoded_response = response.decode('utf-8') 21 | print(f"[Client ✅] Binary response received: {decoded_response}") 22 | else: 23 | print(f"[Client ⚠️] Text response received: {response}") 24 | 25 | # Normal successful call 26 | asyncio.run(test_rpc("login", "test payload")) 27 | asyncio.run(test_rpc("test.middleware", "another payload")) 28 | 29 | # Chain with missing next() 30 | asyncio.run(test_rpc("stuck.method", "should fail")) 31 | 32 | # Middleware that throws 33 | asyncio.run(test_rpc("throw.method", "should throw")) 34 | -------------------------------------------------------------------------------- /tests/qos_mock_transport.hpp: -------------------------------------------------------------------------------- 1 | TEST_CASE("AtLeastOnce delivers with possible duplicates", "[qos]") { 2 | // 1) App + WebSocketTransport 3 | App app; 4 | auto ws = std::make_unique(app.getSessionManager(), 60); 5 | ReliableOptions opt{ QoSLevel::AtLeastOnce, 100, 3, 1000, 6 | std::make_shared(100ms, 1000ms)}; 7 | ws->setReliable(opt); 8 | auto* wsPtr = ws.get(); 9 | app.setTransport(std::move(ws)); 10 | 11 | // 2) MockClient <-> WebSocketTransport pipe 12 | MockClient client(*wsPtr); 13 | 14 | // 3) echo handler, kaç kez tetiklendiÄŸini sayaçla 15 | int called = 0; 16 | app.registerRPC("echo", [&](auto const&, RpcContext& ctx) { 17 | ++called; 18 | ctx.reply({1}); // 1‑byte payload 19 | }); 20 | 21 | // 4) Client ACK’i ilk seferde DROPlasın -> retry 22 | client.setAckMode(DropFirst); // enum 23 | client.send("echo", {42}); 24 | 25 | // event‑loop / tick 26 | app.pollFor(500ms); 27 | 28 | REQUIRE(called >= 1); // en az bir kez handler çaÄŸrılmalı 29 | REQUIRE(called <= 2); // AtLeastOnce → duplicate 0 veya 1 30 | 31 | REQUIRE(client.receivedPayloads().front() == std::vector{1}); 32 | } 33 | -------------------------------------------------------------------------------- /src/core/rpc/rpc_context.cpp: -------------------------------------------------------------------------------- 1 | #include "binaryrpc/core/rpc/rpc_context.hpp" 2 | #include "binaryrpc/core/interfaces/itransport.hpp" 3 | #include "binaryrpc/core/session/session.hpp" 4 | #include 5 | 6 | namespace binaryrpc { 7 | 8 | RpcContext::RpcContext(std::shared_ptr sess, 9 | void* conn, 10 | ITransport* tr) 11 | : session_(std::move(sess)), connection_(conn), transport_(tr) {} 12 | 13 | Session& RpcContext::session() { return *session_; } 14 | const Session& RpcContext::session() const { return *session_; } 15 | std::shared_ptr RpcContext::sessionPtr() const { return session_; } 16 | 17 | 18 | void RpcContext::reply(const std::vector& data) const { 19 | if (transport_ && connection_) 20 | transport_->sendToClient(connection_, data); 21 | } 22 | 23 | void RpcContext::broadcast(const std::vector& data) const { 24 | if (transport_) transport_->send(data); 25 | } 26 | 27 | void RpcContext::disconnect() const { 28 | if (transport_ && connection_) transport_->disconnectClient(connection_); 29 | } 30 | 31 | bool RpcContext::hasRole(const std::string& expected) const { 32 | try { 33 | return session().get("role") == expected; 34 | } 35 | catch (...) { return false; } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/internal/core/util/time.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file time.hpp 3 | * @brief Time utility functions for BinaryRPC. 4 | * 5 | * Provides helper functions for retrieving the current time in milliseconds, 6 | * using both wall-clock and steady clock sources. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include 13 | #include 14 | 15 | namespace binaryrpc { 16 | 17 | /** 18 | * @brief Get the current wall-clock time in milliseconds since the Unix epoch. 19 | * 20 | * Uses std::chrono::system_clock to retrieve the current time. 21 | * 22 | * @return Current time in milliseconds since epoch (uint64_t) 23 | */ 24 | inline std::uint64_t epochMillis() 25 | { 26 | using namespace std::chrono; 27 | return duration_cast( 28 | system_clock::now().time_since_epoch()).count(); 29 | } 30 | 31 | /** 32 | * @brief Get the current steady clock time in milliseconds. 33 | * 34 | * Uses std::chrono::steady_clock for monotonic time measurement. 35 | * 36 | * @return Current steady clock time in milliseconds (uint64_t) 37 | */ 38 | static uint64_t clockMs() { 39 | using namespace std::chrono; 40 | return duration_cast( 41 | steady_clock::now().time_since_epoch() 42 | ).count(); 43 | } 44 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | /build/ 3 | /cmake-build-*/ 4 | *.o 5 | *.obj 6 | *.exe 7 | *.dll 8 | *.so 9 | *.a 10 | *.lib 11 | *.pdb 12 | *.pch 13 | *.ilk 14 | *.manifest 15 | *.rsp 16 | *.idb 17 | *.mod 18 | *.exp 19 | *.log 20 | *.tmp 21 | *.cache 22 | *.cmake 23 | CTestTestfile.cmake 24 | Testing/ 25 | 26 | # IDE files 27 | .idea/ 28 | .vscode/ 29 | .vs/ 30 | *.sln 31 | *.vcxproj 32 | *.vcxproj.* 33 | *.user 34 | *.opensdf 35 | *.sdf 36 | *.suo 37 | *.ncb 38 | *.db 39 | *.DS_Store 40 | 41 | # Python venv 42 | venv/ 43 | 44 | # OS files 45 | Thumbs.db 46 | .DS_Store 47 | 48 | # Other 49 | *.swp 50 | *.bak 51 | *.old 52 | *.orig 53 | 54 | .idea/ 55 | cmake-build-debug-visual-studio/ 56 | cmake-build-release-visual-studio/ 57 | CTestTestfile.cmake 58 | error.txt 59 | log_analysis.txt 60 | cpp.hint 61 | doc.odt 62 | 63 | 64 | tests/CMakeFiles/ 65 | tests/cmake_install.cmake 66 | tests/CTestTestfile.cmake 67 | tests/*.vcxproj* 68 | tests/RUN_TESTS.vcxproj* 69 | 70 | **/venv/ 71 | **/.venv/ 72 | **/__pycache__/ 73 | **/.pytest_cache/ 74 | 75 | **/Testing/ 76 | **/Testing/Temporary/ 77 | **/CMakeFiles/ 78 | **/cmake_install.cmake 79 | **/CTestTestfile.cmake 80 | 81 | examples/ 82 | 83 | tests/Memory_leak_stress_test_py/ 84 | 85 | # VCPKG 86 | vcpkg_installed 87 | vcpkg-configuration.json 88 | 89 | # Third-party libraries (submodules) 90 | # third_party/ # Commented out to allow submodules 91 | -------------------------------------------------------------------------------- /tests/mock_client.cpp: -------------------------------------------------------------------------------- 1 | #include "mock_client.hpp" 2 | #include "binaryrpc/transports/websocket/websocket_transport.hpp" 3 | #include 4 | 5 | using namespace binaryrpc; 6 | 7 | void MockClient::send(const std::string& meth, const std::vector& pl) { 8 | std::vector frame; 9 | frame.push_back(uint8_t(FrameType::FRAME_DATA)); 10 | uint64_t id = 1; // testte sabit id 11 | for (int i = 7; i >= 0; --i) // big‑endian 12 | frame.push_back(uint8_t(id >> (i*8))); 13 | frame.insert(frame.end(), meth.begin(), meth.end()); 14 | frame.push_back(':'); 15 | frame.insert(frame.end(), pl.begin(), pl.end()); 16 | sendRaw(frame); 17 | } 18 | 19 | void MockClient::onServerFrame(const std::vector& buf) { 20 | FrameType t = FrameType(buf[0]); 21 | if (t == FrameType::FRAME_ACK) { 22 | if (mode_ == AckMode::DropFirst && !firstAckDropped_) { 23 | firstAckDropped_ = true; // drop 24 | return; 25 | } 26 | // Normal: teslim et -> hiçbir ÅŸey yapmaya gerek yok 27 | } else if (t == FrameType::FRAME_DATA) { 28 | // id atla 29 | recv_.emplace_back(buf.begin()+1+8, buf.end()); 30 | // Otomatik ACK 31 | std::vector ack{ uint8_t(FrameType::FRAME_ACK) }; 32 | ack.insert(ack.end(), buf.begin()+1, buf.begin()+1+8); 33 | sendRaw(ack); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/core/protocol/simple_text_protocol.cpp: -------------------------------------------------------------------------------- 1 | #include "binaryrpc/core/protocol/simple_text_protocol.hpp" 2 | #include "binaryrpc/core/interfaces/iprotocol.hpp" 3 | #include "binaryrpc/core/util/logger.hpp" 4 | #include "binaryrpc/core/util/error_types.hpp" 5 | #include 6 | 7 | namespace binaryrpc { 8 | 9 | ParsedRequest SimpleTextProtocol::parse(const std::vector& data) { 10 | LOG_DEBUG("[SimpleTextProtocol::parse] Gelen veri: " + std::string(data.begin(), data.end())); 11 | std::string s(data.begin(), data.end()); 12 | auto pos = s.find(':'); 13 | if (pos == std::string::npos) return { "", {} }; 14 | 15 | ParsedRequest req; 16 | req.methodName = s.substr(0, pos); 17 | std::string param = s.substr(pos + 1); 18 | req.payload.assign(param.begin(), param.end()); 19 | return req; 20 | } 21 | 22 | std::vector SimpleTextProtocol::serialize(const std::string& method, 23 | const std::vector& payload) { 24 | std::string out = method + ":"; 25 | out.insert(out.end(), payload.begin(), payload.end()); 26 | return std::vector(out.begin(), out.end()); 27 | } 28 | 29 | std::vector SimpleTextProtocol::serializeError(const ErrorObj& e) { 30 | std::string s = "error:" + std::to_string(static_cast(e.code)) + ":" + e.msg; 31 | return { s.begin(), s.end() }; 32 | } 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /test_server/main.cpp: -------------------------------------------------------------------------------- 1 | // src/main.cpp 2 | #include "binaryrpc/core/rpc/rpc_context.hpp" 3 | #include "binaryrpc/core/protocol/simple_text_protocol.hpp" 4 | #include "binaryrpc/core/util/logger.hpp" 5 | #include 6 | #include "binaryrpc/core/app.hpp" // App sınıfı :contentReference[oaicite:0]{index=0}:contentReference[oaicite:1]{index=1} 7 | #include "binaryrpc/transports/websocket/websocket_transport.hpp" 8 | #include "binaryrpc/core/util/qos.hpp" 9 | 10 | using namespace binaryrpc; 11 | 12 | int main() { 13 | uint16_t port = 9011; 14 | 15 | Logger::inst().setLevel(LogLevel::Debug); 16 | // 1) App nesnesi oluÅŸtur ve WebSocketTransport’u ata 17 | App& app = App::getInstance(); 18 | auto ws = std::make_unique(app.getSessionManager(), /*idleTimeoutSec=*/60); 19 | 20 | ReliableOptions opts; 21 | opts.level = QoSLevel::None; 22 | 23 | ws->setReliable(opts); 24 | 25 | 26 | app.setTransport(std::move(ws)); 27 | 28 | // 2) Basit bir 'echo' RPC kaydı 29 | app.registerRPC("echo", [](auto const& req, RpcContext& ctx) { 30 | // Gelen payload'u birebir geri gönder 31 | ctx.reply(req); 32 | }); 33 | 34 | // 3) Server’ı ayaÄŸa kaldır 35 | std::cout << "[Server] Listening on port " << port << "\n"; 36 | app.run(port); 37 | 38 | // (CTRL+C ile durdurulunca stop() çaÄŸrılacak) 39 | std::cin.get(); 40 | return 0; 41 | } 42 | -------------------------------------------------------------------------------- /test_server/error_propagation_test.cpp: -------------------------------------------------------------------------------- 1 | #include "binaryrpc/core/rpc/rpc_context.hpp" 2 | #include "binaryrpc/core/protocol/simple_text_protocol.hpp" 3 | #include "binaryrpc/core/util/logger.hpp" 4 | #include 5 | #include "binaryrpc/core/app.hpp" 6 | #include "binaryrpc/core/framework_api.hpp" 7 | #include "binaryrpc/core/protocol/simple_text_protocol.hpp" 8 | #include "binaryrpc/transports/websocket/websocket_transport.hpp" 9 | 10 | using namespace binaryrpc; 11 | 12 | int main() { 13 | App& app = App::getInstance(); 14 | SessionManager& sm = app.getSessionManager(); 15 | auto ws = std::make_unique(sm, /*idleTimeoutSec=*/30); 16 | 17 | ReliableOptions opts; 18 | opts.level = QoSLevel::None; 19 | 20 | ws->setReliable(opts); 21 | 22 | app.setTransport(std::move(ws)); 23 | 24 | app.registerRPC("echo", [](const std::vector& req, RpcContext& ctx) { 25 | ctx.reply(req); 26 | }); 27 | 28 | app.registerRPC("throwing_handler", [](const std::vector& req, RpcContext& ctx) { 29 | try { 30 | throw std::runtime_error("intentional error!"); 31 | } catch (const std::exception& ex) { 32 | std::string err_msg = "error:Handler exception: "; 33 | err_msg += ex.what(); 34 | ctx.reply(std::vector(err_msg.begin(), err_msg.end())); 35 | } 36 | }); 37 | 38 | 39 | app.run(9002); 40 | 41 | 42 | std::cin.get(); 43 | return 0; 44 | } 45 | -------------------------------------------------------------------------------- /src/internal/core/util/random.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file random.hpp 3 | * @brief Random number utility functions for BinaryRPC. 4 | * 5 | * Provides helper functions for generating cryptographically suitable random tokens, 6 | * typically used for session IDs and other unique identifiers. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include 13 | #include 14 | #include 15 | 16 | namespace binaryrpc { 17 | 18 | /** 19 | * @brief Fill a 16-byte array with cryptographically suitable random data. 20 | * 21 | * Uses a thread-local Mersenne Twister engine seeded with std::random_device and high-resolution clock. 22 | * 23 | * @param tok Reference to a 16-byte array to fill with random bytes 24 | */ 25 | inline void randomFill(std::array& tok) 26 | { 27 | static thread_local std::mt19937_64 rng{ 28 | std::random_device{}() ^ (uint64_t)std::chrono::high_resolution_clock::now().time_since_epoch().count() 29 | }; 30 | std::uniform_int_distribution dist(0, 0xFFFFFFFF); 31 | 32 | for (size_t i = 0; i < 16; i += 4) { 33 | uint32_t rnd = dist(rng); 34 | tok[i] = static_cast(rnd & 0xFF); 35 | tok[i + 1] = static_cast((rnd >> 8) & 0xFF); 36 | tok[i + 2] = static_cast((rnd >> 16) & 0xFF); 37 | tok[i + 3] = static_cast((rnd >> 24) & 0xFF); 38 | } 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /include/binaryrpc/core/util/error_types.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file error_types.hpp 3 | * @brief Error type definitions for BinaryRPC. 4 | * 5 | * Provides enums and structures for representing error codes and error objects 6 | * used in RPC error handling and propagation. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include 13 | 14 | namespace binaryrpc { 15 | 16 | /** 17 | * @enum RpcErr 18 | * @brief Error codes for RPC operations. 19 | * 20 | * - Parse: Parsing error 21 | * - Middleware: Middleware denied the request 22 | * - NotFound: RPC method not found 23 | * - Auth: Authentication error 24 | * - RateLimited: Rate limiting triggered 25 | * - Internal: Internal server error 26 | */ 27 | enum class RpcErr : int { 28 | Parse = 1, ///< Parsing error 29 | Middleware, ///< Middleware denied the request 30 | NotFound, ///< RPC method not found 31 | Auth, ///< Authentication error 32 | RateLimited, ///< Rate limiting triggered 33 | Internal = 99 ///< Internal server error 34 | }; 35 | 36 | /** 37 | * @struct ErrorObj 38 | * @brief Structure representing an error object for RPC error propagation. 39 | */ 40 | struct ErrorObj { 41 | RpcErr code; ///< Error code 42 | std::string msg; ///< Error message 43 | std::vector data; ///< Optional error data 44 | }; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /tests/integration_tests_py/error_propagation/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import websockets 3 | 4 | async def error_propagation_test(): 5 | uri = "ws://localhost:9002" 6 | headers = { 7 | "x-client-id": "1", 8 | "x-device-id": "dev_1" 9 | } 10 | 11 | async with websockets.connect(uri, additional_headers=headers) as websocket: 12 | # Test 1: invalid method 13 | print("[TEST] Sending invalid method...") 14 | await websocket.send(b"invalid_method:xyz") 15 | resp1 = await websocket.recv() 16 | resp1_str = resp1.decode() 17 | print(f"Response (invalid method): {resp1_str}") 18 | assert "error" in resp1_str.lower(), "Expected error for invalid method" 19 | 20 | # Test 2: throwing handler 21 | print("[TEST] Calling throwing_handler...") 22 | await websocket.send(b"throwing_handler:test") 23 | resp2 = await websocket.recv() 24 | resp2_str = resp2.decode() 25 | print(f"Response (throwing handler): {resp2_str}") 26 | assert "error" in resp2_str.lower(), "Expected error for throwing handler" 27 | 28 | # Test 3: protocol parse error (invalid format) 29 | print("[TEST] Sending invalid protocol message...") 30 | await websocket.send(b"gibberish_that_doesnt_match:protocol") 31 | resp3 = await websocket.recv() 32 | resp3_str = resp3.decode() 33 | print(f"Response (parse error): {resp3_str}") 34 | assert "error" in resp3_str.lower(), "Expected error for protocol parse error" 35 | 36 | print("[TEST] SUCCESS: Error propagation test passed!") 37 | 38 | asyncio.run(error_propagation_test()) 39 | -------------------------------------------------------------------------------- /include/binaryrpc/core/strategies/linear_backoff.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file linear_backoff.hpp 3 | * @brief Linear backoff strategy implementation for BinaryRPC. 4 | * 5 | * Provides an implementation of IBackoffStrategy that increases the delay linearly 6 | * with each retry attempt, up to a maximum delay. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include "../interfaces/IBackoffStrategy.hpp" 13 | #include 14 | 15 | namespace binaryrpc { 16 | 17 | /** 18 | * @class LinearBackoff 19 | * @brief Linear backoff strategy for retrying message delivery. 20 | * 21 | * Increases the delay linearly with each retry attempt, up to a maximum delay. 22 | */ 23 | class LinearBackoff : public IBackoffStrategy { 24 | public: 25 | /** 26 | * @brief Construct a LinearBackoff strategy. 27 | * @param base Initial delay for the first retry 28 | * @param max Maximum delay for any retry 29 | */ 30 | LinearBackoff(std::chrono::milliseconds base, std::chrono::milliseconds max) 31 | : base_(base), max_(max) {} 32 | 33 | /** 34 | * @brief Calculate the next backoff delay based on the attempt number. 35 | * @param attempt Current retry attempt (starting from 1) 36 | * @return Duration to wait before the next retry 37 | */ 38 | std::chrono::milliseconds nextDelay(uint32_t attempt) const override { 39 | auto d = base_ * attempt; 40 | return d < max_ ? d : max_; 41 | } 42 | private: 43 | std::chrono::milliseconds base_; 44 | std::chrono::milliseconds max_; 45 | }; 46 | 47 | } // namespace binaryrpc 48 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(Catch2 3 REQUIRED CONFIG) 2 | 3 | add_executable(binaryrpc_unit_tests 4 | session_manager.test.cpp 5 | generic_index.test.cpp 6 | middleware_chain.test.cpp 7 | #rpc_manager.test.cpp 8 | protocol.test.cpp 9 | rate_limiter.test.cpp 10 | ) 11 | 12 | target_link_libraries(binaryrpc_unit_tests 13 | PRIVATE 14 | binaryrpc_core 15 | Catch2::Catch2WithMain 16 | #glog::glog 17 | ) 18 | 19 | target_include_directories(binaryrpc_unit_tests 20 | PRIVATE 21 | ${CMAKE_CURRENT_SOURCE_DIR}/../include 22 | ${CMAKE_CURRENT_SOURCE_DIR}/../src 23 | ) 24 | 25 | add_test(NAME SessionManagerTests COMMAND binaryrpc_unit_tests) 26 | 27 | 28 | add_executable(binaryrpc_qos_tests 29 | mock_client.cpp 30 | qos.test.cpp 31 | ) 32 | 33 | target_link_libraries(binaryrpc_qos_tests 34 | PRIVATE 35 | binaryrpc_core 36 | Catch2::Catch2WithMain 37 | #glog::glog 38 | ) 39 | 40 | target_include_directories(binaryrpc_qos_tests 41 | PRIVATE 42 | ${CMAKE_CURRENT_SOURCE_DIR}/../include 43 | ${CMAKE_CURRENT_SOURCE_DIR}/../src 44 | ) 45 | 46 | target_compile_definitions(binaryrpc_qos_tests PUBLIC BINARYRPC_TEST) 47 | 48 | if(MSVC) 49 | target_compile_options(binaryrpc_qos_tests PRIVATE 50 | $<$:/MDd /D_ITERATOR_DEBUG_LEVEL=2> 51 | $<$:/MD /D_ITERATOR_DEBUG_LEVEL=0> 52 | ) 53 | 54 | target_compile_options(binaryrpc_unit_tests PRIVATE 55 | $<$:/MDd /D_ITERATOR_DEBUG_LEVEL=2> 56 | $<$:/MD /D_ITERATOR_DEBUG_LEVEL=0> 57 | ) 58 | endif() 59 | 60 | add_test(NAME QoSTests COMMAND binaryrpc_qos_tests) 61 | -------------------------------------------------------------------------------- /src/internal/core/util/hex.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file hex.hpp 3 | * @brief Hexadecimal encoding and decoding utilities for BinaryRPC. 4 | * 5 | * Provides helper functions for converting between 16-byte binary buffers and 32-character hexadecimal strings. 6 | * 7 | * @author Efecan 8 | * @date 2025 9 | */ 10 | #pragma once 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace binaryrpc { 20 | 21 | /** 22 | * @brief Convert a 16-byte array to a 32-character lowercase hexadecimal string. 23 | * 24 | * @param buf 16-byte array to convert 25 | * @return 32-character lowercase hexadecimal string 26 | */ 27 | inline std::string toHex(const std::array& buf) 28 | { 29 | std::ostringstream oss; 30 | oss << std::hex << std::setfill('0'); 31 | for (auto b : buf) 32 | oss << std::setw(2) << static_cast(b); 33 | return oss.str(); 34 | } 35 | 36 | /** 37 | * @brief Convert a 32-character hexadecimal string to a 16-byte array. 38 | * 39 | * @param hex 32-character hexadecimal string 40 | * @param out Reference to a 16-byte array to fill 41 | * @throws std::invalid_argument if the input string is not 32 characters or contains invalid hex 42 | */ 43 | inline void fromHex(std::string_view hex, std::array& out) 44 | { 45 | if (hex.size() != 32) 46 | throw std::invalid_argument("fromHex: length != 32"); 47 | for (size_t i = 0; i < 16; ++i) 48 | { 49 | auto byte = std::stoul(std::string{ hex.substr(i * 2,2) }, nullptr, 16); 50 | out[i] = static_cast(byte & 0xFF); 51 | } 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /include/binaryrpc/core/auth/ClientIdentity.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace binaryrpc { 8 | 9 | 10 | /** 11 | * @brief Transport-agnostic client identity. 12 | * 13 | * ┌──────────┬───────────┬──────────────────────────────┐ 14 | * │ clientId │ deviceId │ sessionToken (128-bit rand.) │ 15 | * └──────────┴───────────┴──────────────────────────────┘ 16 | * 17 | * • IP address is **not** used in equality or hashing (may change on mobile networks). 18 | * • sessionToken is only used for reconnect/authentication purposes. 19 | */ 20 | struct ClientIdentity { 21 | std::string clientId; 22 | std::uint64_t deviceId{}; 23 | std::array sessionToken{}; ///< Random UUID compliant with RFC 4122 24 | 25 | /* Identity equality — ignores IP and token */ 26 | [[nodiscard]] bool operator==(const ClientIdentity& o) const noexcept { 27 | return clientId == o.clientId && deviceId == o.deviceId; 28 | } 29 | }; 30 | 31 | /* ---------- utility ---------- */ 32 | inline void hashCombine(std::size_t& seed, std::size_t v) noexcept { 33 | seed ^= v + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2); 34 | } 35 | 36 | } 37 | 38 | /* ---------- std::hash specialization ---------- */ 39 | template<> 40 | struct std::hash { 41 | std::size_t operator()(const binaryrpc::ClientIdentity& id) const noexcept { 42 | std::size_t h = std::hash{}(id.clientId); 43 | binaryrpc::hashCombine(h, std::hash{}(id.deviceId)); 44 | return h; // sessionToken is not included in hash → stable across reconnects 45 | } 46 | }; -------------------------------------------------------------------------------- /include/binaryrpc/core/strategies/exponential_backoff.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file exponential_backoff.hpp 3 | * @brief Exponential backoff strategy implementation for BinaryRPC. 4 | * 5 | * Provides an implementation of IBackoffStrategy that increases the delay exponentially 6 | * with each retry attempt, up to a maximum delay. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include "../interfaces/IBackoffStrategy.hpp" 13 | #include 14 | 15 | namespace binaryrpc { 16 | 17 | /** 18 | * @class ExponentialBackoff 19 | * @brief Exponential backoff strategy for retrying message delivery. 20 | * 21 | * Increases the delay exponentially with each retry attempt, up to a maximum delay. 22 | */ 23 | class ExponentialBackoff : public IBackoffStrategy { 24 | public: 25 | /** 26 | * @brief Construct an ExponentialBackoff strategy. 27 | * @param base Initial delay for the first retry 28 | * @param max Maximum delay for any retry 29 | */ 30 | ExponentialBackoff(std::chrono::milliseconds base, std::chrono::milliseconds max) 31 | : base_(base), max_(max) {} 32 | 33 | /** 34 | * @brief Calculate the next backoff delay based on the attempt number. 35 | * @param attempt Current retry attempt (starting from 1) 36 | * @return Duration to wait before the next retry 37 | */ 38 | std::chrono::milliseconds nextDelay(uint32_t attempt) const override { 39 | long long delay = (long long)base_.count() * (1LL << (attempt - 1)); 40 | delay = std::min(delay, (long long)max_.count()); 41 | return std::chrono::milliseconds(delay); 42 | } 43 | 44 | private: 45 | std::chrono::milliseconds base_; 46 | std::chrono::milliseconds max_; 47 | }; 48 | 49 | } -------------------------------------------------------------------------------- /include/binaryrpc/middlewares/jwt_auth.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include "binaryrpc/core/middleware/middleware_chain.hpp" 5 | #include "binaryrpc/core/session/session.hpp" 6 | 7 | #include 8 | 9 | namespace binaryrpc { 10 | 11 | /** 12 | * JWT tabanlı kimlik doğrulama middleware’i. 13 | * 14 | * @param secret : HS256 imza anahtarı 15 | * @param requiredRole : boş değilse payload içindeki "role" claim’inin bu string olmasını şart koşar 16 | */ 17 | inline auto jwtAuth(const std::string& secret, 18 | const std::string& requiredRole = "") { 19 | return [secret, requiredRole](Session& s, NextFunc next) { 20 | try { 21 | // İstemci Login sırasında Session’a koymuş olmalı 22 | auto token = s.get("jwt"); 23 | if (token.empty()) return; // token yoksa reddet 24 | 25 | // Decode & imza doğrulama 26 | auto decoded = jwt::decode(token); 27 | jwt::verify() 28 | .allow_algorithm(jwt::algorithm::hs256{ secret }) 29 | .verify(decoded); 30 | 31 | // Rol kontrolü 32 | auto roleClaim = decoded.get_payload_claim("role"); 33 | std::string role = roleClaim.as_string(); 34 | if (!requiredRole.empty() && role != requiredRole) { 35 | return; // rol yetkisi yok 36 | } 37 | 38 | // Başarılı → role bilgisini Session’a set edelim 39 | s.set("role", role); 40 | next(); 41 | } 42 | catch (...) { 43 | // herhangi bir hata (parse, verify, claim eksikliği) → istek reddedilsin 44 | } 45 | }; 46 | } 47 | 48 | } // namespace binaryrpc 49 | -------------------------------------------------------------------------------- /src/core/middleware/middleware_chain.cpp: -------------------------------------------------------------------------------- 1 | #include "binaryrpc/core/middleware/middleware_chain.hpp" 2 | #include "binaryrpc/core/util/logger.hpp" 3 | 4 | namespace binaryrpc { 5 | 6 | void MiddlewareChain::add(Middleware mw) { global_.push_back(std::move(mw)); } 7 | 8 | void MiddlewareChain::addFor(const std::string& m, Middleware mw) { 9 | scoped_[m].push_back(std::move(mw)); 10 | } 11 | 12 | bool MiddlewareChain::execute(Session& s, const std::string& m, std::vector& payload) { 13 | std::vector chain = global_; 14 | if (auto it = scoped_.find(m); it != scoped_.end()) 15 | chain.insert(chain.end(), it->second.begin(), it->second.end()); 16 | 17 | size_t idx = 0; 18 | 19 | std::function next = [&]() { 20 | if (idx >= chain.size()) return; // end of the chain 21 | size_t current = idx; // current middleware 22 | bool nextCalled = false; 23 | 24 | ++idx; // move to next index 25 | 26 | try { 27 | chain[current](s, m, payload, [&] { 28 | nextCalled = true; 29 | next(); // proceed to the next middleware 30 | }); 31 | } catch (const std::exception& ex) { 32 | LOG_ERROR("[MiddlewareChain] Exception caught: " + std::string(ex.what())); 33 | idx = chain.size() + 1; // stop the chain 34 | } catch (...) { 35 | LOG_ERROR("[MiddlewareChain] Unknown exception caught!"); 36 | idx = chain.size() + 1; // stop the chain 37 | } 38 | 39 | if (!nextCalled) { 40 | LOG_WARN("[MiddlewareChain] next() çağrılmadı → zincir durdu."); 41 | idx = chain.size() + 1; // stop the chain 42 | } 43 | }; 44 | 45 | if (!chain.empty()) 46 | next(); 47 | 48 | return idx == chain.size(); //return true if the whole chain was executed 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /include/binaryrpc/middlewares/rate_limiter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include // std::min 4 | #include "binaryrpc/core/types.hpp" 5 | #include "binaryrpc/core/session/session.hpp" 6 | 7 | namespace binaryrpc { 8 | 9 | // Basit Token Bucket 10 | struct RateBucket { 11 | int tokens; 12 | std::chrono::steady_clock::time_point last; 13 | }; 14 | 15 | /** 16 | * @param qps : saniyede yenilenen token sayısı 17 | * @param burst : en fazla birikebilecek token miktarı 18 | */ 19 | inline auto rateLimiter(int qps, int burst = 2) { 20 | return [qps, burst](Session& s, NextFunc next) { 21 | // Session’da saklı Bucket* alalım 22 | auto bucketPtr = s.get("_bucket"); 23 | if (!bucketPtr) { 24 | bucketPtr = new RateBucket{ burst, 25 | std::chrono::steady_clock::now() }; 26 | s.set("_bucket", bucketPtr); 27 | } 28 | RateBucket* b = bucketPtr; 29 | 30 | // Zaman farkı kadar token yenile 31 | auto now = std::chrono::steady_clock::now(); 32 | auto elapsed = std::chrono::duration_cast( 33 | now - b->last) 34 | .count(); 35 | if (elapsed > 0) { 36 | // burst ve int uyumu için cast ediyoruz 37 | int newTokens = static_cast(b->tokens + elapsed * qps); 38 | b->tokens = std::min(burst, newTokens); 39 | b->last = now; 40 | } 41 | 42 | if (b->tokens > 0) { 43 | --b->tokens; 44 | next(); 45 | } 46 | // token yoksa next() çağrılmaz, istek drop edilir 47 | }; 48 | } 49 | 50 | } // namespace binaryrpc 51 | -------------------------------------------------------------------------------- /tests/integration_tests_py/Concurrency_and_multiple_clients/app.py: -------------------------------------------------------------------------------- 1 | #advanced_general_server.cpp 2 | import asyncio 3 | import websockets 4 | 5 | async def client_task(client_id, port=9000): 6 | uri = f"ws://localhost:{port}" 7 | username = f"user_{client_id}" 8 | 9 | # Prepare headers 10 | headers = { 11 | "x-client-id": str(client_id), 12 | "x-device-id": f"dev_{client_id}" 13 | # "x-session-token": "...", # Add if necessary 14 | } 15 | 16 | async with websockets.connect(uri, additional_headers=headers) as websocket: 17 | print(f"[Client {client_id}] Connecting...") 18 | 19 | # STEP 1: set_data_indexed_true 20 | msg1 = f"set_data_indexed_true:{username}".encode() 21 | await websocket.send(msg1) 22 | resp1 = await websocket.recv() 23 | resp1_str = resp1.decode() 24 | print(f"[Client {client_id}] set_data_indexed_true response: {resp1_str}") 25 | assert resp1_str == "OK", f"Client {client_id} set_data_indexed_true failed" 26 | 27 | # STEP 2: get_data 28 | await websocket.send(b"get_data:") 29 | resp2 = await websocket.recv() 30 | resp2_str = resp2.decode() 31 | print(f"[Client {client_id}] get_data response: {resp2_str}") 32 | assert resp2_str == username, f"Client {client_id} get_data mismatch" 33 | 34 | # STEP 3: find_by 35 | msg3 = f"find_by:{username}".encode() 36 | await websocket.send(msg3) 37 | resp3 = await websocket.recv() 38 | resp3_str = resp3.decode() 39 | print(f"[Client {client_id}] find_by response: {resp3_str}") 40 | assert resp3_str.startswith("S"), f"Client {client_id} find_by failed" 41 | 42 | print(f"[Client {client_id}] SUCCESS") 43 | 44 | async def concurrency_test(num_clients=10): 45 | tasks = [client_task(i) for i in range(num_clients)] 46 | await asyncio.gather(*tasks) 47 | 48 | asyncio.run(concurrency_test(10)) 49 | -------------------------------------------------------------------------------- /src/core/rpc/rpc_manager.cpp: -------------------------------------------------------------------------------- 1 | #include "internal/core/rpc/rpc_manager.hpp" 2 | #include "binaryrpc/core/rpc/rpc_context.hpp" 3 | #include "binaryrpc/core/util/logger.hpp" // Corrected path 4 | #include 5 | 6 | namespace binaryrpc { 7 | 8 | /* Context-based registration */ 9 | void RPCManager::registerRPC(const std::string& method, 10 | RpcContextHandler handler, 11 | ITransport* transport) 12 | { 13 | std::lock_guard lock(mutex_); 14 | handlers_[method] = [handler, transport]( 15 | const std::vector& req, 16 | std::vector& /*resp*/, 17 | Session& session) 18 | { 19 | RpcContext ctx(std::shared_ptr(&session, [](Session*) {}), 20 | session.liveWs(), 21 | transport); 22 | handler(req, ctx); 23 | }; 24 | } 25 | 26 | /* Optional legacy-style registration */ 27 | void RPCManager::registerRPC(const std::string& method, 28 | InternalHandler handler) 29 | { 30 | std::lock_guard lock(mutex_); 31 | handlers_[method] = std::move(handler); 32 | } 33 | 34 | /* Invocation */ 35 | bool RPCManager::call(const std::string& method, 36 | const std::vector& request, 37 | std::vector& response, 38 | Session& session) 39 | { 40 | std::lock_guard lock(mutex_); 41 | auto it = handlers_.find(method); 42 | if (it != handlers_.end()) { 43 | try { 44 | it->second(request, response, session); 45 | } catch (const std::exception& ex) { 46 | LOG_WARN("[RPCManager] Handler exception: " + std::string(ex.what())); 47 | } 48 | return true; 49 | } 50 | LOG_WARN("[RPCManager] Method not found: " + method ); 51 | return false; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /tests/integration_tests_py/session/app.py: -------------------------------------------------------------------------------- 1 | import asyncio, websockets, json 2 | import random 3 | 4 | URI = "ws://127.0.0.1:9000" 5 | 6 | def pack(method, payload=""): 7 | """SimpleTextProtocol framework: method:payload – returns as bytes.""" 8 | return f"{method}:{payload}".encode() 9 | 10 | def make_headers(): 11 | cid = f"cli-{random.randint(1000, 9999)}" 12 | did = f"dev-{random.randint(1000, 9999)}" 13 | return [ 14 | ("x-client-id", cid), 15 | ("x-device-id", did) 16 | ] 17 | 18 | async def run(): 19 | # ---- 1. main connection ---- 20 | async with websockets.connect(URI, additional_headers=make_headers()) as ws: 21 | await ws.send(pack("set.nonidx", "Jane")) 22 | print("set.nonidx ➜", (await ws.recv()).decode()) 23 | 24 | await ws.send(pack("get.nonidx")) 25 | print("get.nonidx ➜", (await ws.recv()).decode()) 26 | 27 | await ws.send(pack("find.city", "Paris")) 28 | print("find.city(Paris, empty) ➜", (await ws.recv()).decode()) 29 | 30 | await ws.send(pack("set.idx", "Paris")) 31 | print("set.idx ➜", (await ws.recv()).decode()) 32 | 33 | await ws.send(pack("find.city", "Paris")) 34 | print("find.city(Paris, 1) ➜", (await ws.recv()).decode()) 35 | 36 | await ws.send(pack("list.sessions")) 37 | first_count = (await ws.recv()).decode() 38 | print("list.sessions (should be 1) ➜", first_count) 39 | 40 | # disconnect, session should be deleted 41 | await ws.send(pack("bye")) 42 | await asyncio.sleep(2) 43 | # ---- 2. new connection (no old session) ---- 44 | async with websockets.connect(URI, additional_headers=make_headers()) as ws2: 45 | await ws2.send(pack("find.city", "Paris")) 46 | print("find.city(Paris, 0) ➜", (await ws2.recv()).decode()) 47 | 48 | await ws2.send(pack("list.sessions")) 49 | print("list.sessions (should be 1) ➜", (await ws2.recv()).decode()) 50 | 51 | asyncio.run(run()) 52 | -------------------------------------------------------------------------------- /tests/integration_tests_py/e2e/ws_e2e_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | #main.cpp 3 | import asyncio 4 | import websockets 5 | 6 | SERVER_HOST = "localhost" 7 | SERVER_PORT = 9011 8 | SERVER_URI = f"ws://{SERVER_HOST}:{SERVER_PORT}" 9 | 10 | HEADERS = { 11 | "x-client-id": "1", 12 | "x-device-id": "dev_1" 13 | } 14 | 15 | def print_result(name, ok): 16 | print(f"[{'OK' if ok else 'FAIL'}] {name}") 17 | 18 | async def test_echo_simple_text(): 19 | try: 20 | async with websockets.connect(SERVER_URI, additional_headers=HEADERS) as ws: 21 | await ws.send(b"echo:hello world") 22 | resp = await ws.recv() 23 | if not isinstance(resp, (bytes, bytearray)): 24 | raise Exception("echo: response is not bytes") 25 | if resp != b"hello world": 26 | raise Exception(f"echo: unexpected response: {resp}") 27 | print_result("echo_simple_text", True) 28 | except Exception as e: 29 | print_result("echo_simple_text", False) 30 | raise 31 | 32 | async def test_error_simple_text(): 33 | try: 34 | async with websockets.connect(SERVER_URI, additional_headers=HEADERS) as ws: 35 | await ws.send(b"unknown:payload") 36 | resp = await ws.recv() 37 | if not isinstance(resp, (bytes, bytearray)): 38 | raise Exception("error: response is not bytes") 39 | if not resp.startswith(b"error:"): 40 | raise Exception(f"error: does not start with 'error:': {resp}") 41 | if not resp.startswith(b"error:3:"): 42 | raise Exception(f"error: does not start with 'error:3:': {resp}") 43 | print_result("error_simple_text", True) 44 | except Exception as e: 45 | print_result("error_simple_text", False) 46 | raise 47 | 48 | async def main(): 49 | await test_echo_simple_text() 50 | await test_error_simple_text() 51 | print("E2E tests passed") 52 | 53 | if __name__ == "__main__": 54 | asyncio.run(main()) 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve BinaryRPC 4 | title: '[BUG] ' 5 | labels: ['bug', 'needs-triage'] 6 | assignees: '' 7 | --- 8 | 9 | ## 🐛 Bug Description 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | ## 🔄 Steps to Reproduce 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## ✅ Expected Behavior 21 | 22 | A clear and concise description of what you expected to happen. 23 | 24 | ## ❌ Actual Behavior 25 | 26 | A clear and concise description of what actually happened. 27 | 28 | ## 📸 Screenshots 29 | 30 | If applicable, add screenshots to help explain your problem. 31 | 32 | ## 🖥️ Environment 33 | 34 | **OS:** [e.g., Windows 10, Ubuntu 20.04, macOS 12.0] 35 | **Compiler:** [e.g., GCC 11.2, Clang 13.0, MSVC 2019] 36 | **BinaryRPC Version:** [e.g., commit hash, tag, or version] 37 | **Dependencies:** [e.g., uWebSockets version, Folly version] 38 | 39 | ## 📋 Additional Context 40 | 41 | Add any other context about the problem here. 42 | 43 | ## 🔍 Minimal Reproduction 44 | 45 | ```cpp 46 | // Please provide a minimal code example that reproduces the issue 47 | #include "binaryrpc/core/app.hpp" 48 | 49 | int main() { 50 | // Your minimal reproduction code here 51 | return 0; 52 | } 53 | ``` 54 | 55 | ## 📊 Error Messages 56 | 57 | ``` 58 | // Paste any error messages or stack traces here 59 | ``` 60 | 61 | ## 🔧 Possible Solutions 62 | 63 | If you have suggestions on a fix for the bug, please describe them here. 64 | 65 | ## 📝 Checklist 66 | 67 | - [ ] I have searched existing issues for duplicates 68 | - [ ] I have tested with the latest master branch 69 | - [ ] I have provided a minimal reproduction example 70 | - [ ] I have included all relevant environment information 71 | - [ ] I have checked the documentation for similar issues 72 | 73 | --- 74 | 75 | **Note:** Please be as detailed as possible. The more information you provide, the easier it will be to diagnose and fix the issue. -------------------------------------------------------------------------------- /include/binaryrpc/core/protocol/simple_text_protocol.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file simple_text_protocol.hpp 3 | * @brief SimpleTextProtocol for text-based RPC serialization in BinaryRPC. 4 | * 5 | * Implements the IProtocol interface using a simple colon-separated text format 6 | * for easy debugging and human readability. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include "binaryrpc/core/interfaces/iprotocol.hpp" 13 | #include "binaryrpc/core/util/logger.hpp" 14 | 15 | namespace binaryrpc { 16 | 17 | /** 18 | * @class SimpleTextProtocol 19 | * @brief Simple text-based protocol implementation for BinaryRPC. 20 | * 21 | * Serializes and parses RPC requests, responses, and errors using a colon-separated text format. 22 | */ 23 | class SimpleTextProtocol : public IProtocol { 24 | public: 25 | /** 26 | * @brief Parse a colon-separated text RPC request. 27 | * 28 | * Extracts the method name and payload from the input string. 29 | * @param data Input data as a byte vector 30 | * @return ParsedRequest containing the method name and payload 31 | */ 32 | ParsedRequest parse(const std::vector& data) override; 33 | 34 | /** 35 | * @brief Serialize an RPC method and payload into colon-separated text format. 36 | * 37 | * Concatenates the method name and payload with a colon separator. 38 | * @param method Method name 39 | * @param payload Payload data 40 | * @return Serialized text data as a byte vector 41 | */ 42 | std::vector serialize(const std::string& method, const std::vector& payload) override; 43 | 44 | /** 45 | * @brief Serialize an error object into colon-separated text format. 46 | * 47 | * Formats the error code and message as a colon-separated string. 48 | * @param e Error object 49 | * @return Serialized error data as a byte vector 50 | */ 51 | std::vector serializeError(const ErrorObj& e) override; 52 | }; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /include/binaryrpc/core/interfaces/iprotocol.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file iprotocol.hpp 3 | * @brief Interface for serialization protocols in BinaryRPC. 4 | * 5 | * Defines the IProtocol interface and ParsedRequest struct for implementing custom 6 | * serialization and deserialization protocols for RPC communication. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include 13 | #include 14 | #include "binaryrpc/core/util/error_types.hpp" 15 | 16 | namespace binaryrpc { 17 | 18 | /** 19 | * @struct ParsedRequest 20 | * @brief Structure representing a parsed RPC request. 21 | */ 22 | struct ParsedRequest { 23 | std::string methodName; ///< Name of the RPC method 24 | std::vector payload; ///< Payload data 25 | }; 26 | 27 | /** 28 | * @class IProtocol 29 | * @brief Interface for custom serialization protocols in BinaryRPC. 30 | * 31 | * Implement this interface to provide custom serialization and deserialization 32 | * for RPC requests, responses, and errors. 33 | */ 34 | class IProtocol { 35 | public: 36 | virtual ~IProtocol() = default; 37 | /** 38 | * @brief Parse input data into a ParsedRequest structure. 39 | * @param data Input data as a byte vector 40 | * @return ParsedRequest containing the method name and payload 41 | */ 42 | virtual ParsedRequest parse(const std::vector& data) = 0; 43 | /** 44 | * @brief Serialize a method and payload into a byte vector. 45 | * @param method Method name 46 | * @param payload Payload data 47 | * @return Serialized data as a byte vector 48 | */ 49 | virtual std::vector serialize(const std::string& method, const std::vector& payload) = 0; 50 | /** 51 | * @brief Serialize an error object into a byte vector. 52 | * @param e Error object 53 | * @return Serialized error data as a byte vector 54 | */ 55 | virtual std::vector serializeError(const ErrorObj&) = 0; 56 | }; 57 | 58 | } 59 | -------------------------------------------------------------------------------- /tests/integration_tests_py/general/advanced_app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import websockets 3 | 4 | async def session_persistence_test(): 5 | uri = "ws://localhost:9000" 6 | headers = { 7 | "x-client-id": "1", 8 | "x-device-id": "dev_1" 9 | } 10 | 11 | async with websockets.connect(uri, additional_headers=headers) as websocket: 12 | print("[TEST] Sending set_data_indexed_false...") 13 | await websocket.send(b"set_data_indexed_false:brocan") 14 | resp1 = await websocket.recv() 15 | resp1_str = resp1.decode() 16 | print(f"Response: {resp1_str}") 17 | assert resp1_str == "OK", "set_data_indexed_false failed" 18 | 19 | print("[TEST] Sending get_data...") 20 | await websocket.send(b"get_data:") 21 | resp2 = await websocket.recv() 22 | resp2_str = resp2.decode() 23 | print(f"Response: {resp2_str}") 24 | assert resp2_str == "brocan", "get_data after indexed_false failed" 25 | 26 | print("[TEST] Sending find_by...") 27 | await websocket.send(b"find_by:brocan") 28 | resp3 = await websocket.recv() 29 | resp3_str = resp3.decode() 30 | print(f"Response: '{resp3_str}'") 31 | assert resp3_str.strip() == "", "find_by should return empty before indexed_true" 32 | 33 | async with websockets.connect(uri, additional_headers=headers) as websocket: 34 | print("[TEST] Sending set_data_indexed_true...") 35 | await websocket.send(b"set_data_indexed_true:brocan") 36 | resp4 = await websocket.recv() 37 | resp4_str = resp4.decode() 38 | print(f"Response: {resp4_str}") 39 | assert resp4_str == "OK", "set_data_indexed_true failed" 40 | 41 | print("[TEST] Sending find_by again...") 42 | await websocket.send(b"find_by:brocan") 43 | resp5 = await websocket.recv() 44 | resp5_str = resp5.decode() 45 | print(f"Response: '{resp5_str}'") 46 | assert resp5_str.startswith("S"), "find_by should return session ID after indexed_true" 47 | 48 | print("[TEST] SUCCESS: Session persistency test passed!") 49 | 50 | asyncio.run(session_persistence_test()) 51 | -------------------------------------------------------------------------------- /src/core/session/session.cpp: -------------------------------------------------------------------------------- 1 | #include "binaryrpc/core/session/session.hpp" 2 | #include "internal/core/qos/duplicate_filter.hpp" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace binaryrpc { 10 | 11 | // Define the implementation struct 12 | struct Session::Impl { 13 | ClientIdentity ident_; 14 | std::string legacyId_; 15 | std::unordered_map data_; 16 | WS* liveWs_ = nullptr; 17 | void* legacyConn_ = nullptr; 18 | qos::DuplicateFilter dupFilter_; 19 | 20 | Impl(ClientIdentity ident, std::string legacyId) 21 | : ident_(std::move(ident)), legacyId_(std::move(legacyId)) {} 22 | }; 23 | 24 | // Session methods delegating to the implementation 25 | Session::Session(ClientIdentity ident, std::string legacyId) 26 | : pImpl_(std::make_unique(std::move(ident), std::move(legacyId))) {} 27 | 28 | Session::~Session() = default; 29 | 30 | const std::string& Session::id() const { 31 | return pImpl_->legacyId_; 32 | } 33 | 34 | const ClientIdentity& Session::identity() const { 35 | return pImpl_->ident_; 36 | } 37 | 38 | void Session::rebind(WS* ws) { 39 | pImpl_->liveWs_ = ws; 40 | pImpl_->dupFilter_ = qos::DuplicateFilter(); 41 | } 42 | 43 | Session::WS* Session::liveWs() const { 44 | return pImpl_->liveWs_; 45 | } 46 | 47 | void Session::setConnection(void* conn) { 48 | pImpl_->legacyConn_ = conn; 49 | } 50 | 51 | void* Session::getConnection() const { 52 | return pImpl_->legacyConn_; 53 | } 54 | 55 | bool Session::acceptDuplicate(const std::vector& rpcPayload, std::chrono::milliseconds ttl) { 56 | return pImpl_->dupFilter_.accept(rpcPayload, ttl); 57 | } 58 | 59 | void Session::set_any(const std::string& key, std::any value) { 60 | pImpl_->data_[key] = std::move(value); 61 | } 62 | 63 | std::any Session::get_any(const std::string& key) const { 64 | auto it = pImpl_->data_.find(key); 65 | if (it != pImpl_->data_.end()) { 66 | return it->second; 67 | } 68 | return {}; 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /include/binaryrpc/core/util/qos.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file qos.hpp 3 | * @brief Quality of Service (QoS) level definitions and reliability options for BinaryRPC. 4 | * 5 | * Provides enums and configuration structures for controlling message delivery guarantees, 6 | * retry strategies, and reliability options in the BinaryRPC framework. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include "../interfaces/IBackoffStrategy.hpp" 13 | 14 | namespace binaryrpc { 15 | 16 | /** 17 | * @enum QoSLevel 18 | * @brief Defines the quality of service (QoS) levels for message delivery. 19 | * 20 | * - None: At most once delivery (no ACK expected) 21 | * - AtLeastOnce: At least once delivery (ACK and retry) 22 | * - ExactlyOnce: Exactly once delivery (two-phase commit) 23 | */ 24 | enum class QoSLevel { 25 | None = 0, ///< QoS 0 – "at most once" (no ACK expected) 26 | AtLeastOnce = 1, ///< QoS 1 – ACK and retry 27 | ExactlyOnce = 2 ///< QoS 2 – exactly once delivery 28 | }; 29 | 30 | /** 31 | * @struct ReliableOptions 32 | * @brief Configuration options for reliable message delivery. 33 | * 34 | * Used with setReliable() to control retry behavior, backoff strategy, session TTL, and more. 35 | */ 36 | struct ReliableOptions { 37 | QoSLevel level{ QoSLevel::None }; ///< Desired QoS level 38 | uint32_t baseRetryMs{ 100 }; ///< Initial retry delay in milliseconds 39 | uint32_t maxRetry{ 3 }; ///< Maximum number of retries 40 | uint32_t maxBackoffMs{ 1000 }; ///< Maximum backoff delay in milliseconds 41 | uint64_t sessionTtlMs { 15 * 60 * 1000 };///< Session time-to-live in milliseconds 42 | uint32_t duplicateTtlMs{ 5000 }; ///< TTL for duplicate detection (ms) 43 | std::shared_ptr backoffStrategy; ///< Custom backoff strategy 44 | bool enableCompression{false}; ///< Enable message compression 45 | size_t compressionThresholdBytes{1024}; ///< Minimum size for compression 46 | size_t maxSendQueueSize{1000}; ///< Maximum send queue size per session 47 | }; 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /include/binaryrpc/core/middleware/middleware_chain.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file middleware_chain.hpp 3 | * @brief MiddlewareChain for composing and executing middleware in BinaryRPC. 4 | * 5 | * Provides a chain-of-responsibility pattern for middleware, allowing global and method-specific 6 | * middleware to be registered and executed in order for each RPC call. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include 13 | #include 14 | #include "binaryrpc/core/session/session.hpp" 15 | #include "binaryrpc/core/util/logger.hpp" 16 | #include "binaryrpc/core/types.hpp" 17 | 18 | namespace binaryrpc { 19 | 20 | /** 21 | * @class MiddlewareChain 22 | * @brief Composes and executes middleware for RPC calls in BinaryRPC. 23 | * 24 | * Supports both global and method-specific middleware. Executes middleware in order, 25 | * allowing each to call next() to proceed or halt the chain. 26 | */ 27 | class MiddlewareChain { 28 | public: 29 | /** 30 | * @brief Add a global middleware to the chain. 31 | * @param mw Middleware function to add 32 | */ 33 | void add(Middleware mw); 34 | 35 | /** 36 | * @brief Add a middleware for a specific method. 37 | * @param method Method name 38 | * @param mw Middleware function to add 39 | */ 40 | void addFor(const std::string& method, Middleware mw); 41 | 42 | /** 43 | * @brief Execute the middleware chain for a session, method, and payload. 44 | * 45 | * Calls each middleware in order. If all call next(), returns true. If any middleware 46 | * does not call next(), the chain is halted and returns false. 47 | * 48 | * @param s Reference to the session 49 | * @param method Method name 50 | * @param payload Payload data (may be modified by middleware) 51 | * @return True if the entire chain was executed, false if halted 52 | */ 53 | bool execute(Session& s, const std::string& method, std::vector& payload); 54 | 55 | private: 56 | std::vector global_; ///< Global middleware chain 57 | std::unordered_map> scoped_; ///< Method-specific middleware 58 | }; 59 | 60 | } -------------------------------------------------------------------------------- /tests/integration_tests_py/session/advanced_app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import websockets 3 | import random 4 | 5 | async def session_persistence_test(): 6 | uri = "ws://localhost:9000" 7 | cid = f"cli-{random.randint(1000, 9999)}" 8 | did = f"dev-{random.randint(1000, 9999)}" 9 | headers = [ 10 | ("x-client-id", cid), 11 | ("x-device-id", did) 12 | ] 13 | 14 | def pack(method, payload=""): 15 | return f"{method}:{payload}".encode() 16 | 17 | async with websockets.connect(uri, additional_headers=headers) as websocket: 18 | print("[TEST] Sending set_data_indexed_false...") 19 | await websocket.send(pack("set_data_indexed_false", "brocan")) 20 | resp1 = await websocket.recv() 21 | resp1_str = resp1.decode() 22 | print(f"Response: {resp1_str}") 23 | assert resp1_str == "OK", "set_data_indexed_false failed" 24 | 25 | print("[TEST] Sending get_data...") 26 | await websocket.send(pack("get_data")) 27 | resp2 = await websocket.recv() 28 | resp2_str = resp2.decode() 29 | print(f"Response: {resp2_str}") 30 | assert resp2_str == "brocan", "get_data after indexed_false failed" 31 | 32 | print("[TEST] Sending find_by...") 33 | await websocket.send(pack("find_by", "brocan")) 34 | resp3 = await websocket.recv() 35 | resp3_str = resp3.decode() 36 | print(f"Response: '{resp3_str}'") 37 | assert resp3_str.strip() == "", "find_by should return empty before indexed_true" 38 | 39 | print("[TEST] Sending set_data_indexed_true...") 40 | await websocket.send(pack("set_data_indexed_true", "brocan")) 41 | resp4 = await websocket.recv() 42 | resp4_str = resp4.decode() 43 | print(f"Response: {resp4_str}") 44 | assert resp4_str == "OK", "set_data_indexed_true failed" 45 | 46 | print("[TEST] Sending find_by again...") 47 | await websocket.send(pack("find_by", "brocan")) 48 | resp5 = await websocket.recv() 49 | resp5_str = resp5.decode() 50 | print(f"Response: '{resp5_str}'") 51 | assert resp5_str.startswith("S"), "find_by should return session ID after indexed_true" 52 | 53 | print("[TEST] SUCCESS: Session persistency test passed!") 54 | 55 | asyncio.run(session_persistence_test()) 56 | -------------------------------------------------------------------------------- /tests/mock_client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "../include/binaryrpc/transports/websocket/websocket_transport.hpp" 9 | 10 | namespace binaryrpc { 11 | 12 | class WebSocketTransport; 13 | 14 | class MockClient { 15 | public: 16 | enum class AckMode { None, DropFirst, DropAll }; // ✅ yeni enum 17 | 18 | // ctor: transport'a kendimizi baÄŸlıyoruz 19 | MockClient(WebSocketTransport& transport) 20 | : transport_(transport), 21 | mode_(AckMode::None), 22 | firstAckDropped_(false) 23 | { 24 | // Sunucu → client yönlü “frame” çıktısını yakala 25 | transport_.setSendInterceptor([this](auto const& data) { 26 | if (mode_ == AckMode::DropFirst && !firstAckDropped_) { 27 | firstAckDropped_ = true; 28 | printf("[MOCK] dropped first ACK intentionally.\n"); 29 | return; 30 | } 31 | this->onServerFrame(data); 32 | }); 33 | 34 | } 35 | 36 | // ACK davranışını seç 37 | void setAckMode(AckMode m) { mode_ = m; } 38 | 39 | // RPC çaÄŸrısı çerçevesini hazırla ve gönder 40 | void send(const std::string& method, const std::vector& payload); 41 | 42 | 43 | // Sunucudan gelen DATA’ları oku 44 | const std::vector>& received() const { return recv_; } 45 | 46 | void sendDuplicateLast() { 47 | if (!sentMessages.empty()) { 48 | transport_.onRawFrameFromClient(sentMessages.back()); 49 | } 50 | } 51 | 52 | void resendLast() { 53 | sendDuplicateLast(); // alias 54 | } 55 | 56 | private: 57 | // gerçekte “kabuk” çerçeveyi alıp server’a besleyen fonksiyon 58 | void sendRaw(const std::vector& frame) { 59 | transport_.onRawFrameFromClient(frame); 60 | } 61 | 62 | // WebSocketTransport’dan gelen her frame’i ele al 63 | void onServerFrame(const std::vector& buf); 64 | 65 | 66 | WebSocketTransport& transport_; 67 | AckMode mode_; 68 | bool firstAckDropped_; 69 | std::vector> recv_; 70 | std::vector> sentMessages; // ✅ eksik field 71 | }; 72 | 73 | } // namespace binaryrpc 74 | -------------------------------------------------------------------------------- /include/binaryrpc/core/protocol/msgpack_protocol.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file msgpack_protocol.hpp 3 | * @brief MsgPackProtocol for MessagePack-based RPC serialization in BinaryRPC. 4 | * 5 | * Implements the IProtocol interface using MessagePack for efficient binary serialization 6 | * and deserialization of RPC requests, responses, and errors. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include "binaryrpc/core/interfaces/iprotocol.hpp" 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include // for std::overflow_error 18 | #include 19 | #include 20 | 21 | namespace binaryrpc { 22 | 23 | /** 24 | * @class MsgPackProtocol 25 | * @brief MessagePack-based protocol implementation for BinaryRPC. 26 | * 27 | * Serializes and parses RPC requests, responses, and errors using the MessagePack format. 28 | */ 29 | class MsgPackProtocol : public IProtocol { 30 | public: 31 | /** 32 | * @brief Parse a MessagePack-encoded RPC request. 33 | * 34 | * Extracts the method name and payload from the MessagePack map. 35 | * @param data MessagePack-encoded input data 36 | * @return ParsedRequest containing the method name and payload 37 | */ 38 | ParsedRequest parse(const std::vector& data) override; 39 | 40 | /** 41 | * @brief Serialize an RPC method and payload into MessagePack format. 42 | * 43 | * Packs the method name and payload as a MessagePack map. 44 | * @param method Method name 45 | * @param payload Payload data 46 | * @return Serialized MessagePack data 47 | * @throws std::overflow_error if payload size exceeds 4 GiB 48 | */ 49 | std::vector serialize(const std::string& method, const std::vector& payload) override; 50 | 51 | /** 52 | * @brief Serialize an error object into MessagePack format. 53 | * 54 | * Packs the error code, message, and optional data as a MessagePack map. 55 | * @param e Error object 56 | * @return Serialized MessagePack error data 57 | * @throws std::overflow_error if error data size exceeds 4 GiB 58 | */ 59 | std::vector serializeError(const ErrorObj& e) override; 60 | 61 | }; 62 | 63 | } -------------------------------------------------------------------------------- /src/internal/core/session/generic_index.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file generic_index.hpp 3 | * @brief Generic index for fast field-based session lookup in BinaryRPC. 4 | * 5 | * Provides a multi-level index for mapping field values to session IDs, enabling 6 | * efficient O(1) lookup of sessions by arbitrary fields. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace binaryrpc { 21 | /** 22 | * @class GenericIndex 23 | * @brief Multi-level index for fast field-based session lookup. 24 | * 25 | * Maps field names and values to sets of session IDs, enabling efficient lookup 26 | * and reverse mapping for cleanup. 27 | */ 28 | class GenericIndex { 29 | public: 30 | /** 31 | * @brief Add a field-value mapping for a session ID. 32 | * @param sid Session ID 33 | * @param field Field name 34 | * @param value Field value 35 | */ 36 | void add(const std::string& sid, 37 | const std::string& field, 38 | const std::string& value); 39 | 40 | /** 41 | * @brief Remove all index entries for a session ID. 42 | * @param sid Session ID 43 | */ 44 | void remove(const std::string& sid); 45 | 46 | /** 47 | * @brief Find all session IDs matching a field and value. 48 | * @param field Field name 49 | * @param value Field value 50 | * @return Set of matching session IDs 51 | */ 52 | std::shared_ptr> 53 | find(const std::string& field, 54 | const std::string& value) const; 55 | 56 | private: 57 | using SidSet = std::unordered_set; 58 | using SidSetPtr = std::shared_ptr; 59 | std::unordered_map> idx_; // value → {sid…} 61 | 62 | /** 63 | * @brief Reverse mapping for cleanup: session ID to list of field-value pairs. 64 | */ 65 | std::unordered_map>> back_; 67 | 68 | mutable std::shared_mutex mx_; 69 | }; 70 | 71 | } -------------------------------------------------------------------------------- /include/binaryrpc/core/util/DefaultInspector.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file DefaultInspector.hpp 3 | * @brief Default handshake inspector for BinaryRPC WebSocket connections. 4 | * 5 | * Provides a basic implementation of IHandshakeInspector that extracts client identity 6 | * from HTTP headers and always authorizes the connection by default. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | 13 | #include "binaryrpc/core/interfaces/IHandshakeInspector.hpp" 14 | #include "binaryrpc/core/auth/ClientIdentity.hpp" 15 | #include 16 | #include 17 | #include 18 | 19 | // Forward declaration to avoid including the uWebSockets header 20 | namespace uWS { struct HttpRequest; } 21 | 22 | namespace binaryrpc { 23 | 24 | /** 25 | * @class DefaultInspector 26 | * @brief Default implementation of IHandshakeInspector for extracting client identity. 27 | * 28 | * Extracts clientId, deviceId, and sessionToken from HTTP headers: 29 | * - clientId: "x-client-id" (required) 30 | * - deviceId: "x-device-id" (optional, parsed as uint64_t) 31 | * - sessionToken: "x-session-token" (optional, 32-character hex) 32 | * 33 | * Always authorizes the connection if clientId is present and deviceId is valid. 34 | */ 35 | class DefaultInspector : public IHandshakeInspector { 36 | public: 37 | DefaultInspector(); 38 | ~DefaultInspector() override; 39 | 40 | DefaultInspector(const DefaultInspector&) = delete; 41 | DefaultInspector& operator=(const DefaultInspector&) = delete; 42 | DefaultInspector(DefaultInspector&&) noexcept; 43 | DefaultInspector& operator=(DefaultInspector&&) noexcept; 44 | 45 | /** 46 | * @brief Extract client identity from the HTTP upgrade request and remote IP. 47 | * @param req Reference to the uWS::HttpRequest 48 | * @return Optional ClientIdentity if extraction is successful, std::nullopt otherwise 49 | */ 50 | std::optional extract(uWS::HttpRequest& req) override; 51 | 52 | /** 53 | * @brief Reason for handshake rejection (not used in default implementation). 54 | * @return String describing the rejection reason 55 | */ 56 | std::string rejectReason() const override; 57 | 58 | private: 59 | // PIMPL idiom to hide implementation details 60 | struct Impl; 61 | std::unique_ptr pImpl_; 62 | }; 63 | 64 | } -------------------------------------------------------------------------------- /tests/rate_limiter.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "binaryrpc/middlewares/rate_limiter.hpp" 4 | #include "binaryrpc/core/session/session.hpp" 5 | 6 | using namespace binaryrpc; 7 | 8 | // Yardımcı fonksiyon: Test için Session oluşturur 9 | static Session makeSession(const std::string& id) { 10 | ClientIdentity ident; 11 | ident.clientId = id; 12 | ident.deviceId = 0; 13 | return Session(ident, id); 14 | } 15 | 16 | // Helper: invoke limiter and count how many times `next` is called. 17 | static int invokeLimiter(const std::function& mw, 18 | Session& s, 19 | int calls) 20 | { 21 | int cnt = 0; 22 | NextFunc nf = [&]{ ++cnt; }; 23 | for (int i = 0; i < calls; ++i) 24 | mw(s, nf); 25 | return cnt; 26 | } 27 | 28 | TEST_CASE("RateLimiter: burst capacity enforced", "[rate]") { 29 | // qps=1, burst=2 30 | auto mw = rateLimiter(1,2); 31 | Session s = makeSession("A"); 32 | 33 | // İlk iki çağrı izinli, üçüncü reddedilir 34 | int ok = invokeLimiter(mw, s, 3); 35 | REQUIRE(ok == 2); 36 | } 37 | 38 | TEST_CASE("RateLimiter: refill after 1 second", "[rate]") { 39 | auto mw = rateLimiter(1,2); 40 | Session s = makeSession("B"); 41 | 42 | // Tüketelim 43 | invokeLimiter(mw, s, 2); 44 | auto b = s.get("_bucket"); 45 | REQUIRE(b != nullptr); 46 | 47 | // 1 saniye geriye al: 1 token eklenmeli 48 | b->last -= std::chrono::seconds(1); 49 | int ok1 = invokeLimiter(mw, s, 2); 50 | REQUIRE(ok1 == 1); // yalnızca 1 token refill edildi 51 | 52 | // Tekrar 2 saniye geriye al: burst geri gelmeli 53 | b->last -= std::chrono::seconds(2); 54 | int ok2 = invokeLimiter(mw, s, 3); 55 | REQUIRE(ok2 == 2); // burst=2 token 56 | } 57 | 58 | TEST_CASE("RateLimiter: multiple sessions isolated", "[rate]") { 59 | auto mw = rateLimiter(1,3); 60 | Session s1 = makeSession("S1"), s2 = makeSession("S2"); 61 | 62 | int c1 = invokeLimiter(mw, s1, 5); 63 | int c2 = invokeLimiter(mw, s2, 5); 64 | REQUIRE(c1 == 3); 65 | REQUIRE(c2 == 3); 66 | } 67 | 68 | TEST_CASE("RateLimiter: default burst parameter", "[rate]") { 69 | // burst default = 2 70 | auto mw = rateLimiter(5); 71 | Session s = makeSession("D"); 72 | int ok = invokeLimiter(mw, s, 4); 73 | REQUIRE(ok == 2); 74 | } 75 | 76 | TEST_CASE("RateLimiter: zero requests do nothing", "[rate]") { 77 | auto mw = rateLimiter(10,5); 78 | Session s = makeSession("Z"); 79 | int ok = invokeLimiter(mw, s, 0); 80 | REQUIRE(ok == 0); 81 | } -------------------------------------------------------------------------------- /include/binaryrpc/core/interfaces/IHandshakeInspector.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file IHandshakeInspector.hpp 3 | * @brief Interface for custom WebSocket handshake inspection and authorization in BinaryRPC. 4 | * 5 | * Defines the IHandshakeInspector interface for extracting and authorizing client identity 6 | * during the WebSocket handshake process. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include "binaryrpc/core/auth/ClientIdentity.hpp" 17 | 18 | namespace uWS { 19 | class HttpRequest; 20 | } 21 | 22 | namespace binaryrpc { 23 | 24 | /** 25 | * @class IHandshakeInspector 26 | * @brief Interface for extracting and authorizing client identity during WebSocket handshake. 27 | * 28 | * Implement this interface to perform custom header/IP extraction and authorization logic 29 | * for incoming WebSocket upgrade requests. 30 | */ 31 | class IHandshakeInspector { 32 | public: 33 | virtual ~IHandshakeInspector() = default; 34 | 35 | /** 36 | * @brief Extract a ClientIdentity from the handshake request. 37 | * 38 | * Implementations should parse headers or other request data to build a ClientIdentity. 39 | * Returning std::nullopt will reject the handshake. 40 | * 41 | * @param req WebSocket HTTP upgrade request. 42 | * @return std::optional if extraction (and basic validation) succeeds; std::nullopt to reject. 43 | */ 44 | virtual std::optional 45 | extract(uWS::HttpRequest& req) = 0; 46 | 47 | /** 48 | * @brief Optionally perform authorization based on extracted identity and full request. 49 | * 50 | * Default implementation always returns true (accepts all connections). 51 | * Override to implement custom authorization logic. 52 | * 53 | * @param identity Extracted client identity 54 | * @param req WebSocket HTTP upgrade request 55 | * @return true to authorize, false to reject 56 | */ 57 | virtual bool authorize(const ClientIdentity& identity, const uWS::HttpRequest& req) { 58 | return true; 59 | } 60 | 61 | /** 62 | * @brief Reason text sent when rejecting a handshake. 63 | * 64 | * Default is "unauthorized". Override to provide a custom rejection reason. 65 | * 66 | * @return String describing the rejection reason 67 | */ 68 | virtual std::string rejectReason() const { 69 | return "unauthorized"; 70 | } 71 | }; 72 | 73 | } -------------------------------------------------------------------------------- /src/core/util/DefaultInspector.cpp: -------------------------------------------------------------------------------- 1 | #include "binaryrpc/core/util/DefaultInspector.hpp" 2 | #include "binaryrpc/core/util/logger.hpp" 3 | #include 4 | #include 5 | 6 | namespace binaryrpc { 7 | 8 | struct DefaultInspector::Impl { 9 | // Implementation details can be added here if needed in the future. 10 | }; 11 | 12 | DefaultInspector::DefaultInspector() : pImpl_(std::make_unique()) {} 13 | 14 | DefaultInspector::~DefaultInspector() = default; 15 | 16 | DefaultInspector::DefaultInspector(DefaultInspector&&) noexcept = default; 17 | 18 | DefaultInspector& DefaultInspector::operator=(DefaultInspector&&) noexcept = default; 19 | 20 | std::optional DefaultInspector::extract(uWS::HttpRequest& req) { 21 | using std::string; 22 | string cid{ req.getHeader("x-client-id") }; 23 | if (cid.empty()) { 24 | LOG_ERROR("Missing x-client-id header"); 25 | return std::nullopt; 26 | } 27 | 28 | /* deviceId --> uint64 */ 29 | std::uint64_t did = 0; 30 | string didTxt{ req.getHeader("x-device-id") }; 31 | if (!didTxt.empty()) { 32 | const auto firstDigit = didTxt.find_first_of("0123456789"); 33 | if (firstDigit != std::string::npos) { 34 | try { 35 | did = std::stoull(didTxt.substr(firstDigit)); 36 | } 37 | catch (const std::exception& ex) { 38 | LOG_ERROR("Invalid device id '" + didTxt + "' – " + ex.what()); 39 | return std::nullopt; 40 | } 41 | } 42 | else { 43 | LOG_ERROR("Device id '" + didTxt + "' contains no numeric part"); 44 | return std::nullopt; 45 | } 46 | } 47 | 48 | /* optional 128-bit token (hex) */ 49 | std::array tok{}; 50 | string tokTxt{ req.getHeader("x-session-token") }; 51 | if (!tokTxt.empty() && tokTxt.size() == 32) { 52 | try { 53 | for (size_t i = 0; i < 16; ++i) 54 | tok[i] = static_cast( 55 | std::stoi(tokTxt.substr(i * 2, 2), nullptr, 16)); 56 | } 57 | catch (const std::exception& ex) { 58 | LOG_ERROR("Invalid session token format: " + std::string(ex.what())); 59 | return std::nullopt; 60 | } 61 | } 62 | 63 | return ClientIdentity{ cid, did, tok }; 64 | } 65 | 66 | std::string DefaultInspector::rejectReason() const { 67 | return "Invalid handshake data"; 68 | } 69 | 70 | } // namespace binaryrpc -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for BinaryRPC 4 | title: '[FEATURE] ' 5 | labels: ['enhancement', 'needs-triage'] 6 | assignees: '' 7 | --- 8 | 9 | ## 🚀 Feature Description 10 | 11 | A clear and concise description of the feature you'd like to see implemented. 12 | 13 | ## 💡 Problem Statement 14 | 15 | A clear and concise description of what problem this feature would solve. For example: 16 | - "I'm always frustrated when [...]" 17 | - "It would be helpful if BinaryRPC could [...]" 18 | - "Currently, there's no way to [...]" 19 | 20 | ## 🎯 Proposed Solution 21 | 22 | A clear and concise description of what you want to happen. 23 | 24 | ## 🔄 Alternative Solutions 25 | 26 | A clear and concise description of any alternative solutions or features you've considered. 27 | 28 | ## 📋 Use Cases 29 | 30 | Describe specific use cases where this feature would be beneficial: 31 | 32 | 1. **Use Case 1:** [Description] 33 | 2. **Use Case 2:** [Description] 34 | 3. **Use Case 3:** [Description] 35 | 36 | ## 🔧 Implementation Ideas 37 | 38 | If you have ideas about how this feature could be implemented, please share them: 39 | 40 | ```cpp 41 | // Example implementation approach 42 | class NewFeature { 43 | public: 44 | void newMethod(); 45 | // ... other methods 46 | }; 47 | ``` 48 | 49 | ## 📊 Impact Assessment 50 | 51 | **Priority:** [High/Medium/Low] 52 | **Complexity:** [High/Medium/Low] 53 | **Breaking Changes:** [Yes/No] 54 | **Performance Impact:** [High/Medium/Low/None] 55 | 56 | ## 🧪 Testing Considerations 57 | 58 | How should this feature be tested? 59 | 60 | - [ ] Unit tests 61 | - [ ] Integration tests 62 | - [ ] Performance benchmarks 63 | - [ ] Documentation updates 64 | - [ ] Example code updates 65 | 66 | ## 📚 Documentation Requirements 67 | 68 | What documentation would be needed for this feature? 69 | 70 | - [ ] API documentation 71 | - [ ] User guide updates 72 | - [ ] Example code 73 | - [ ] Migration guide (if breaking change) 74 | 75 | ## 🔗 Related Issues 76 | 77 | Link any related issues or discussions: 78 | - Related issue: #[issue number] 79 | - Related discussion: #[discussion number] 80 | 81 | ## 📝 Additional Context 82 | 83 | Add any other context, screenshots, or examples about the feature request here. 84 | 85 | ## 📋 Checklist 86 | 87 | - [ ] I have searched existing issues for similar requests 88 | - [ ] I have provided clear use cases 89 | - [ ] I have considered the impact on existing functionality 90 | - [ ] I have thought about testing requirements 91 | - [ ] I have considered documentation needs 92 | 93 | --- 94 | 95 | **Note:** Feature requests help us understand what the community needs. Please be as detailed as possible to help us evaluate and implement your request effectively. -------------------------------------------------------------------------------- /tests/generic_index.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "internal/core/session/generic_index.hpp" 5 | 6 | using namespace binaryrpc; 7 | static std::string sid(int i) { return "s" + std::to_string(i); } 8 | 9 | TEST_CASE("add() tek oturum – lookup çalışır", "[index]") { 10 | GenericIndex gi; 11 | gi.add(sid(1), "room", "lobby"); 12 | 13 | auto set_ptr = gi.find("room", "lobby"); 14 | REQUIRE(set_ptr); 15 | REQUIRE(set_ptr->size() == 1); 16 | REQUIRE(set_ptr->count(sid(1)) == 1); 17 | } 18 | 19 | TEST_CASE("add() duplicate id/value idempotent", "[index]") { 20 | GenericIndex gi; 21 | gi.add(sid(1), "tenant", "5"); 22 | gi.add(sid(1), "tenant", "5"); 23 | auto set_ptr = gi.find("tenant", "5"); 24 | REQUIRE(set_ptr); 25 | REQUIRE(set_ptr->size() == 1); 26 | } 27 | 28 | TEST_CASE("overwrite removes old mapping", "[index]") { 29 | GenericIndex gi; 30 | gi.add(sid(1), "tier", "silver"); 31 | gi.add(sid(1), "tier", "gold"); 32 | 33 | REQUIRE_FALSE(gi.find("tier","silver")); 34 | auto gold_ptr = gi.find("tier","gold"); 35 | REQUIRE(gold_ptr); 36 | REQUIRE(gold_ptr->size() == 1); 37 | REQUIRE(gold_ptr->count(sid(1)) == 1); 38 | } 39 | 40 | TEST_CASE("multiple sessions same key", "[index]") { 41 | GenericIndex gi; 42 | for (int i=1;i<=5;++i) gi.add(sid(i),"group","A"); 43 | auto set_ptr = gi.find("group", "A"); 44 | REQUIRE(set_ptr); 45 | REQUIRE(set_ptr->size() == 5); 46 | } 47 | 48 | TEST_CASE("remove(id) cleans all mappings", "[index]") { 49 | GenericIndex gi; 50 | gi.add("x","a","1"); 51 | gi.add("x","b","2"); 52 | 53 | gi.remove("x"); // GenericIndex::remove(sid) 54 | REQUIRE_FALSE(gi.find("a","1")); 55 | REQUIRE_FALSE(gi.find("b","2")); 56 | } 57 | 58 | 59 | TEST_CASE("large volume remains consistent", "[index][stress]") { 60 | GenericIndex gi; 61 | constexpr int N = 10000; 62 | for (int i=0;isize() == N/10); 67 | } 68 | TEST_CASE("thread‑safe add/remove in two phases", "[index][concurrency]") { 69 | GenericIndex gi; 70 | constexpr int THREADS = 16, OPS = 500; 71 | 72 | { 73 | std::vector writers; 74 | for (int t=0;tsize() == THREADS*OPS/2); 88 | } 89 | -------------------------------------------------------------------------------- /include/binaryrpc/core/rpc/rpc_context.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file rpc_context.hpp 3 | * @brief RpcContext class for handling RPC method calls in BinaryRPC. 4 | * 5 | * Provides access to the session, transport, and connection for an RPC call, 6 | * and helper methods for replying, broadcasting, and disconnecting. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include "binaryrpc/core/session/session.hpp" 13 | #include "binaryrpc/core/interfaces/itransport.hpp" 14 | 15 | namespace binaryrpc { 16 | /** 17 | * @class RpcContext 18 | * @brief Context for an RPC method call, providing access to session, transport, and helpers. 19 | * 20 | * Used by RPC handlers to reply to the client, broadcast messages, disconnect, and access session data. 21 | */ 22 | class RpcContext { 23 | public: 24 | /** 25 | * @brief Construct an RpcContext for a session, connection, and transport. 26 | * @param session Shared pointer to the session 27 | * @param connection Pointer to the client connection 28 | * @param transport Pointer to the transport 29 | */ 30 | RpcContext(std::shared_ptr session, void* connection, ITransport* transport); 31 | 32 | /** 33 | * @brief Send a reply to the client for this RPC call. 34 | * @param data Data to send as the reply 35 | */ 36 | void reply(const std::vector& data) const; 37 | 38 | /** 39 | * @brief Broadcast a message to all clients. 40 | * @param data Data to broadcast 41 | */ 42 | void broadcast(const std::vector& data) const; 43 | 44 | /** 45 | * @brief Disconnect the client associated with this RPC call. 46 | */ 47 | void disconnect() const; 48 | 49 | /** 50 | * @brief Get a reference to the session. 51 | * @return Reference to the Session 52 | */ 53 | Session& session(); 54 | /** 55 | * @brief Get a const reference to the session. 56 | * @return Const reference to the Session 57 | */ 58 | const Session& session() const; 59 | /** 60 | * @brief Get a shared pointer to the session. 61 | * @return Shared pointer to the Session 62 | */ 63 | std::shared_ptr sessionPtr() const; 64 | 65 | /** 66 | * @brief Check if the session has the specified role. 67 | * @param expected Expected role string 68 | * @return True if the session's "role" field matches, false otherwise 69 | */ 70 | bool hasRole(const std::string& expected) const; 71 | 72 | private: 73 | std::shared_ptr session_; ///< Session associated with the RPC call 74 | void* connection_; ///< Pointer to the client connection 75 | ITransport* transport_; ///< Pointer to the transport 76 | }; 77 | } -------------------------------------------------------------------------------- /tests/qos.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../include/binaryrpc/transports/websocket/websocket_transport.hpp" 3 | #include "binaryrpc/core/strategies/linear_backoff.hpp" 4 | #include "binaryrpc/core/strategies/exponential_backoff.hpp" 5 | #include "internal/core/util/conn_state.hpp" 6 | #include 7 | #include 8 | 9 | using namespace binaryrpc; 10 | 11 | TEST_CASE("makeFrame constructs correct binary layout", "[unit]") { 12 | uint64_t id = 12345; 13 | std::vector payload = {1, 2, 3, 4}; 14 | auto frame = WebSocketTransport::test_makeFrame(FrameType::FRAME_DATA, id, payload); 15 | 16 | REQUIRE(frame.size() == 1 + sizeof(id) + payload.size()); 17 | REQUIRE(static_cast(frame[0]) == FrameType::FRAME_DATA); 18 | uint64_t extractedId; 19 | std::memcpy(&extractedId, frame.data() + 1, sizeof(id)); 20 | extractedId = networkToHost64(extractedId); 21 | REQUIRE(extractedId == id); 22 | 23 | std::vector extractedPayload(frame.begin() + 1 + sizeof(id), frame.end()); 24 | REQUIRE(extractedPayload == payload); 25 | } 26 | 27 | TEST_CASE("registerSeen prevents duplicate within TTL", "[unit]") { 28 | ConnState state; 29 | uint64_t id = 42; 30 | uint32_t ttlMs = 1000; 31 | 32 | bool firstInsert = WebSocketTransport::test_registerSeen(&state, id, ttlMs); 33 | REQUIRE(firstInsert == true); 34 | 35 | bool secondInsert = WebSocketTransport::test_registerSeen(&state, id, ttlMs); 36 | REQUIRE(secondInsert == false); 37 | 38 | // Simulate TTL expiry 39 | std::this_thread::sleep_for(std::chrono::milliseconds(ttlMs + 10)); 40 | 41 | bool thirdInsert = WebSocketTransport::test_registerSeen(&state, id, ttlMs); 42 | REQUIRE(thirdInsert == true); 43 | } 44 | 45 | TEST_CASE("LinearBackoff increases linearly up to max", "[unit]") { 46 | auto backoff = LinearBackoff(std::chrono::milliseconds(10), std::chrono::milliseconds(50)); 47 | 48 | REQUIRE(backoff.nextDelay(1) == std::chrono::milliseconds(10)); 49 | REQUIRE(backoff.nextDelay(2) == std::chrono::milliseconds(20)); 50 | REQUIRE(backoff.nextDelay(3) == std::chrono::milliseconds(30)); 51 | REQUIRE(backoff.nextDelay(4) == std::chrono::milliseconds(40)); 52 | REQUIRE(backoff.nextDelay(5) == std::chrono::milliseconds(50)); 53 | REQUIRE(backoff.nextDelay(6) == std::chrono::milliseconds(50)); 54 | 55 | } 56 | 57 | TEST_CASE("ExponentialBackoff doubles until max", "[unit]") { 58 | auto backoff = ExponentialBackoff(std::chrono::milliseconds(10), std::chrono::milliseconds(80)); 59 | REQUIRE(backoff.nextDelay(1) == std::chrono::milliseconds(10)); 60 | REQUIRE(backoff.nextDelay(2) == std::chrono::milliseconds(20)); 61 | REQUIRE(backoff.nextDelay(3) == std::chrono::milliseconds(40)); 62 | REQUIRE(backoff.nextDelay(4) == std::chrono::milliseconds(80)); 63 | REQUIRE(backoff.nextDelay(5) == std::chrono::milliseconds(80)); 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/plugins/room_plugin.cpp: -------------------------------------------------------------------------------- 1 | #include "binaryrpc/plugins/room_plugin.hpp" 2 | #include "binaryrpc/core/session/session_manager.hpp" 3 | #include "binaryrpc/core/interfaces/itransport.hpp" 4 | #include "binaryrpc/core/util/logger.hpp" 5 | #include 6 | #include 7 | #include 8 | 9 | namespace binaryrpc { 10 | 11 | struct RoomPlugin::Impl { 12 | SessionManager& sessionManager_; 13 | ITransport* transport_; 14 | std::mutex mtx_; 15 | std::unordered_map> rooms_; 16 | 17 | Impl(SessionManager& sm, ITransport* t) : sessionManager_(sm), transport_(t) {} 18 | }; 19 | 20 | RoomPlugin::RoomPlugin(SessionManager& sessionManager, ITransport* transport) 21 | : pImpl_(std::make_unique(sessionManager, transport)) {} 22 | 23 | RoomPlugin::~RoomPlugin() = default; 24 | 25 | void RoomPlugin::initialize() {} 26 | 27 | void RoomPlugin::join(const std::string& room, const std::string& sid) { 28 | std::scoped_lock lk(pImpl_->mtx_); 29 | pImpl_->rooms_[room].insert(sid); 30 | } 31 | 32 | void RoomPlugin::leave(const std::string& room, const std::string& sid) { 33 | std::scoped_lock lk(pImpl_->mtx_); 34 | auto it = pImpl_->rooms_.find(room); 35 | if (it != pImpl_->rooms_.end()) { 36 | it->second.erase(sid); 37 | if (it->second.empty()) { 38 | pImpl_->rooms_.erase(it); 39 | } 40 | } 41 | } 42 | 43 | void RoomPlugin::leaveAll(const std::string& sid) { 44 | std::scoped_lock lk(pImpl_->mtx_); 45 | for (auto it = pImpl_->rooms_.begin(); it != pImpl_->rooms_.end(); ) { 46 | it->second.erase(sid); 47 | if (it->second.empty()) { 48 | it = pImpl_->rooms_.erase(it); 49 | } else { 50 | ++it; 51 | } 52 | } 53 | } 54 | 55 | void RoomPlugin::broadcast(const std::string& room, const std::vector& data) { 56 | std::vector members; 57 | { 58 | std::scoped_lock lk(pImpl_->mtx_); 59 | auto it = pImpl_->rooms_.find(room); 60 | if (it == pImpl_->rooms_.end()) return; 61 | members.assign(it->second.begin(), it->second.end()); 62 | } 63 | 64 | for (const auto& sid : members) { 65 | if (auto session = pImpl_->sessionManager_.getSession(sid)) { 66 | if(auto* ws = session->liveWs()) { 67 | pImpl_->transport_->sendToClient(ws, data); 68 | } 69 | } 70 | } 71 | } 72 | 73 | std::vector RoomPlugin::getRoomMembers(const std::string& room) const { 74 | std::scoped_lock lk(pImpl_->mtx_); 75 | auto it = pImpl_->rooms_.find(room); 76 | if (it == pImpl_->rooms_.end()) { 77 | return {}; 78 | } 79 | return {it->second.begin(), it->second.end()}; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /include/binaryrpc/plugins/room_plugin.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file room_plugin.hpp 3 | * @brief RoomPlugin for group communication and room management in BinaryRPC. 4 | * 5 | * Provides a plugin for managing rooms (groups of sessions), allowing users to join, leave, 6 | * broadcast messages, and query room membership. Thread-safe and integrated with the session manager. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | 13 | #include "binaryrpc/core/interfaces/iplugin.hpp" 14 | #include 15 | #include 16 | #include 17 | 18 | namespace binaryrpc { 19 | 20 | // Forward declarations 21 | class SessionManager; 22 | class ITransport; 23 | 24 | /** 25 | * @class RoomPlugin 26 | * @brief A plugin for managing chat rooms or game lobbies. 27 | * 28 | * Provides functionality for joining, leaving, and broadcasting messages to rooms. 29 | */ 30 | class RoomPlugin : public IPlugin { 31 | public: 32 | /** 33 | * @brief Construct a RoomPlugin. 34 | * @param sessionManager Reference to the SessionManager 35 | * @param transport Pointer to the ITransport instance 36 | */ 37 | RoomPlugin(SessionManager& sessionManager, ITransport* transport); 38 | 39 | /** 40 | * @brief Destructor for RoomPlugin. 41 | */ 42 | ~RoomPlugin() override; 43 | 44 | /** 45 | * @brief Initialize the plugin (called by App). 46 | */ 47 | void initialize() override; 48 | /** 49 | * @brief Get the name of the plugin. 50 | * @return Name of the plugin as a C-string 51 | */ 52 | const char* name() const override { return "RoomPlugin"; } 53 | 54 | /** 55 | * @brief Add a session to a room. 56 | * @param room Name of the room 57 | * @param sid Session ID to add 58 | */ 59 | void join(const std::string& room, const std::string& sid); 60 | /** 61 | * @brief Remove a session from a room. 62 | * @param room Name of the room 63 | * @param sid Session ID to remove 64 | */ 65 | void leave(const std::string& room, const std::string& sid); 66 | /** 67 | * @brief Remove a session from all rooms it belongs to. 68 | * @param sid Session ID to remove 69 | */ 70 | void leaveAll(const std::string& sid); 71 | /** 72 | * @brief Broadcast a message to all members of a room. 73 | * @param room Name of the room 74 | * @param data Data to broadcast 75 | */ 76 | void broadcast(const std::string& room, const std::vector& data); 77 | /** 78 | * @brief Get the list of session IDs in a room. 79 | * @param room Name of the room 80 | * @return Vector of session IDs in the room 81 | */ 82 | std::vector getRoomMembers(const std::string& room) const; 83 | 84 | private: 85 | // PIMPL idiom 86 | struct Impl; 87 | std::unique_ptr pImpl_; 88 | }; 89 | 90 | } -------------------------------------------------------------------------------- /src/core/session/generic_index.cpp: -------------------------------------------------------------------------------- 1 | #include "internal/core/session/generic_index.hpp" 2 | 3 | namespace binaryrpc { 4 | 5 | /*──────────── add / update ───────────*/ 6 | void GenericIndex::add(const std::string& sid, 7 | const std::string& field, 8 | const std::string& value) 9 | { 10 | std::unique_lock w(mx_); 11 | 12 | // 1. Önceki değeri (varsa) kaldır 13 | auto& hist = back_[sid]; 14 | for (auto& fv : hist) { 15 | if (fv.first == field) { 16 | if (fv.second == value) return; // Değer aynı, işlem yapma 17 | 18 | // Eski girdiyi idx_'den sil 19 | if (auto fit = idx_.find(field); fit != idx_.end()) { 20 | if (auto vit = fit->second.find(fv.second); vit != fit->second.end()) { 21 | vit->second->erase(sid); 22 | if (vit->second->empty()) { 23 | fit->second.erase(vit); 24 | } 25 | } 26 | } 27 | fv.second = value; // Geçmişi yeni değerle güncelle 28 | } 29 | } 30 | 31 | // 2. Yeni değeri ekle 32 | auto& valueMap = idx_[field]; 33 | auto it = valueMap.find(value); 34 | if (it == valueMap.end()) { 35 | it = valueMap.emplace(value, std::make_shared()).first; 36 | } 37 | it->second->insert(sid); 38 | 39 | // 3. Geçmişe ekle (eğer yeni bir alan ise) 40 | bool fieldExists = false; 41 | for (const auto& fv : hist) { 42 | if (fv.first == field) { 43 | fieldExists = true; 44 | break; 45 | } 46 | } 47 | if (!fieldExists) { 48 | hist.emplace_back(field, value); 49 | } 50 | } 51 | 52 | /*──────────── remove session ─────────*/ 53 | void GenericIndex::remove(const std::string& sid) { 54 | std::unique_lock w(mx_); 55 | auto it = back_.find(sid); 56 | if (it == back_.end()) return; 57 | 58 | for (auto& [f, v] : it->second) { 59 | auto fit = idx_.find(f); 60 | if (fit == idx_.end()) continue; 61 | 62 | auto& valueMap = fit->second; 63 | auto vit = valueMap.find(v); 64 | if (vit == valueMap.end()) continue; 65 | 66 | if(vit->second) { 67 | vit->second->erase(sid); 68 | if (vit->second->empty()) 69 | valueMap.erase(vit); 70 | } 71 | 72 | if (valueMap.empty()) 73 | idx_.erase(fit); 74 | } 75 | 76 | back_.erase(it); 77 | } 78 | 79 | 80 | /*──────────── O(1) lookup ────────────*/ 81 | std::shared_ptr> 82 | GenericIndex::find(const std::string& field, 83 | const std::string& value) const 84 | { 85 | std::shared_lock r(mx_); 86 | auto fit = idx_.find(field); 87 | if (fit == idx_.end()) return nullptr; 88 | auto vit = fit->second.find(value); 89 | if (vit == fit->second.end()) return nullptr; 90 | return vit->second; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /include/binaryrpc/core/util/logger.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file logger.hpp 3 | * @brief Logging utilities for BinaryRPC. 4 | * 5 | * Provides a singleton Logger class and logging macros for different log levels. 6 | * 7 | * @author Efecan 8 | * @date 2025 9 | */ 10 | #pragma once 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace binaryrpc { 18 | 19 | /** 20 | * @enum LogLevel 21 | * @brief Log levels for the logger. 22 | */ 23 | enum class LogLevel { Trace, Debug, Info, Warn, Error }; 24 | 25 | /** 26 | * @class Logger 27 | * @brief Singleton logger class for BinaryRPC. 28 | * 29 | * Provides thread-safe logging with customizable log sinks and log levels. 30 | */ 31 | class Logger { 32 | public: 33 | using Sink = std::function; 34 | 35 | /** 36 | * @brief Get the singleton Logger instance. 37 | * @return Reference to the Logger instance 38 | */ 39 | static Logger& inst() { 40 | static Logger L; return L; 41 | } 42 | 43 | /** 44 | * @brief Set the minimum log level. 45 | * @param lvl LogLevel to set 46 | */ 47 | void setLevel(LogLevel lvl) { level_ = lvl; } 48 | /** 49 | * @brief Set a custom log sink function. 50 | * @param s Sink function to use 51 | */ 52 | void setSink(Sink s) { sink_ = std::move(s); } 53 | 54 | /** 55 | * @brief Log a message at the specified log level. 56 | * @param lvl LogLevel for the message 57 | * @param msg Message to log 58 | */ 59 | void log(LogLevel lvl, const std::string& msg) { 60 | if (lvl < level_) return; 61 | std::scoped_lock lk(m_); 62 | sink_(lvl, msg); 63 | } 64 | 65 | private: 66 | Logger() { 67 | /* default sink → stdout */ 68 | sink_ = [](LogLevel l, const std::string& m) { 69 | static const char* names[]{ "TRACE","DEBUG","INFO","WARN","ERROR" }; 70 | std::cout << "[" << names[(int)l] << "] " << m << '\n'; 71 | }; 72 | } 73 | std::mutex m_; 74 | LogLevel level_{ LogLevel::Info }; 75 | Sink sink_; 76 | }; 77 | 78 | /** 79 | * @def LOG_TRACE 80 | * @brief Log a message at TRACE level. 81 | */ 82 | /** 83 | * @def LOG_DEBUG 84 | * @brief Log a message at DEBUG level. 85 | */ 86 | /** 87 | * @def LOG_INFO 88 | * @brief Log a message at INFO level. 89 | */ 90 | /** 91 | * @def LOG_WARN 92 | * @brief Log a message at WARN level. 93 | */ 94 | /** 95 | * @def LOG_ERROR 96 | * @brief Log a message at ERROR level. 97 | */ 98 | #define LOG_TRACE(msg) ::binaryrpc::Logger::inst().log(::binaryrpc::LogLevel::Trace, msg) 99 | #define LOG_DEBUG(msg) ::binaryrpc::Logger::inst().log(::binaryrpc::LogLevel::Debug, msg) 100 | #define LOG_INFO(msg) ::binaryrpc::Logger::inst().log(::binaryrpc::LogLevel::Info, msg) 101 | #define LOG_WARN(msg) ::binaryrpc::Logger::inst().log(::binaryrpc::LogLevel::Warn, msg) 102 | #define LOG_ERROR(msg) ::binaryrpc::Logger::inst().log(::binaryrpc::LogLevel::Error, msg) 103 | } -------------------------------------------------------------------------------- /src/internal/core/rpc/rpc_manager.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file rpc_manager.hpp 3 | * @brief RPCManager class for registering and dispatching RPC methods in BinaryRPC. 4 | * 5 | * Provides registration and invocation of RPC handlers, supporting both context-based 6 | * and legacy request/response handler styles. Thread-safe for concurrent use. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include "binaryrpc/core/session/session.hpp" 18 | #include "binaryrpc/core/rpc/rpc_context.hpp" 19 | #include "binaryrpc/core/interfaces/itransport.hpp" 20 | #include "binaryrpc/core/util/logger.hpp" 21 | #include "binaryrpc/core/types.hpp" 22 | 23 | namespace binaryrpc { 24 | 25 | /** 26 | * @typedef InternalHandler 27 | * @brief Internal handler type: used within the framework for request/response style. 28 | * 29 | * Receives request data, a response buffer, and a Session reference. 30 | */ 31 | using InternalHandler = 32 | std::function&, 33 | std::vector&, 34 | Session&)>; 35 | 36 | /** 37 | * @class RPCManager 38 | * @brief Manages registration and invocation of RPC methods in BinaryRPC. 39 | * 40 | * Supports both context-based and legacy request/response handler styles. 41 | * Thread-safe for concurrent registration and invocation. 42 | */ 43 | class RPCManager { 44 | public: 45 | /** 46 | * @brief Register an RPC handler using the context-based interface. 47 | * 48 | * The handler receives the request data and an RpcContext object. 49 | * @param method Name of the RPC method 50 | * @param handler Handler function to register 51 | * @param transport Pointer to the transport (used for replies) 52 | */ 53 | void registerRPC(const std::string& method, 54 | RpcContextHandler handler, 55 | ITransport* transport); 56 | 57 | /** 58 | * @brief Register an RPC handler using the legacy request/response style. 59 | * 60 | * The handler receives the request data, a response buffer, and a Session reference. 61 | * @param method Name of the RPC method 62 | * @param handler Handler function to register 63 | */ 64 | void registerRPC(const std::string& method, 65 | InternalHandler handler); 66 | 67 | /** 68 | * @brief Invoke a registered RPC handler by method name. 69 | * 70 | * Looks up the handler and calls it with the provided request, response, and session. 71 | * @param method Name of the RPC method 72 | * @param request Request data 73 | * @param response Buffer to store the response data 74 | * @param session Reference to the session 75 | * @return True if the handler was found and called, false otherwise 76 | */ 77 | bool call(const std::string& method, 78 | const std::vector& request, 79 | std::vector& response, 80 | Session& session); 81 | 82 | private: 83 | std::unordered_map handlers_; ///< Registered RPC handlers 84 | mutable std::mutex mutex_; ///< Protects access to the handlers map 85 | }; 86 | 87 | } -------------------------------------------------------------------------------- /test_server/session_test.cpp: -------------------------------------------------------------------------------- 1 | #include "binaryrpc/core/rpc/rpc_context.hpp" 2 | #include "binaryrpc/core/protocol/simple_text_protocol.hpp" 3 | #include "binaryrpc/core/util/logger.hpp" 4 | #include 5 | #include "binaryrpc/core/app.hpp" 6 | #include "binaryrpc/transports/websocket/websocket_transport.hpp" 7 | #include "binaryrpc/core/util/qos.hpp" 8 | #include "binaryrpc/core/framework_api.hpp" 9 | #include 10 | 11 | using namespace binaryrpc; 12 | 13 | int main() { 14 | Logger::inst().setLevel(LogLevel::Debug); 15 | 16 | App& app = App::getInstance(); 17 | 18 | auto ws = std::make_unique(app.getSessionManager(), 60); 19 | 20 | ReliableOptions opts; 21 | opts.level = QoSLevel::None; 22 | opts.sessionTtlMs = 1; 23 | 24 | ws->setReliable(opts); 25 | 26 | 27 | app.setTransport(std::move(ws)); 28 | 29 | // ---- Framework‑level API ---- 30 | auto& api = app.getFrameworkApi(); 31 | 32 | /* non‑indexed set/get */ 33 | app.registerRPC("set.nonidx", [&](const std::vector& p, RpcContext& ctx){ 34 | std::string sid = ctx.session().id(); 35 | std::string val(p.begin(), p.end()); 36 | api.setField(sid, "nonidx", val, /*indexed=*/false); 37 | ctx.reply(app.getProtocol()->serialize("set.nonidx", 38 | {'o','k'})); 39 | }); 40 | 41 | app.registerRPC("get.nonidx", [&](const std::vector&, RpcContext& ctx){ 42 | auto val = api.getField(ctx.session().id(), "nonidx") 43 | .value_or("missing"); 44 | ctx.reply(app.getProtocol()->serialize("get.nonidx", 45 | {val.begin(), val.end()})); 46 | }); 47 | 48 | /* indexed set / find */ 49 | app.registerRPC("set.idx", [&](const std::vector& p, RpcContext& ctx){ 50 | std::string sid = ctx.session().id(); 51 | std::string city(p.begin(), p.end()); 52 | api.setField(sid, "city", city, /*indexed=*/true); 53 | ctx.reply(app.getProtocol()->serialize("set.idx", 54 | {'o','k'})); 55 | }); 56 | 57 | app.registerRPC("find.city", [&](const std::vector& p, RpcContext& ctx){ 58 | std::string city(p.begin(), p.end()); 59 | auto v_ptr = api.findBy("city", city); 60 | std::string resp = "0"; 61 | if (v_ptr.size() > 0) { 62 | resp = std::to_string(v_ptr.size()); 63 | } 64 | ctx.reply(app.getProtocol()->serialize("find.city", 65 | {resp.begin(), resp.end()})); 66 | }); 67 | 68 | /* session enumeration / disconnect */ 69 | app.registerRPC("list.sessions", [&](const std::vector&, RpcContext& ctx){ 70 | auto n = api.listSessionIds().size(); 71 | std::string resp = std::to_string(n); 72 | ctx.reply(app.getProtocol()->serialize("list.sessions", 73 | {resp.begin(), resp.end()})); 74 | }); 75 | 76 | app.registerRPC("bye", [&](const std::vector&, RpcContext& ctx){ 77 | api.disconnect(ctx.session().id()); // istemciyi kopar 78 | }); 79 | 80 | app.run(9000); 81 | std::cout << "[server] listening on :9000\n"; 82 | std::cin.get(); 83 | return 1; 84 | } 85 | -------------------------------------------------------------------------------- /src/internal/core/util/conn_state.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file conn_state.hpp 3 | * @brief Connection state and QoS buffer structures for BinaryRPC sessions. 4 | * 5 | * Defines data structures for managing per-connection state, including QoS-1 and QoS-2 message buffers, 6 | * retry logic, duplicate detection, and statistics for BinaryRPC sessions. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | namespace binaryrpc { 24 | 25 | /** 26 | * @struct FrameInfo 27 | * @brief Stores information about a single QoS-1 frame, including retry state. 28 | */ 29 | struct FrameInfo { 30 | std::vector frame; ///< The message frame data 31 | std::chrono::steady_clock::time_point nextRetry{}; ///< Next retry time point 32 | uint32_t retryCount{ 0 }; ///< Number of retries attempted 33 | }; 34 | 35 | /** 36 | * @struct Q2Meta 37 | * @brief Metadata for QoS-2 (ExactlyOnce) message state and retry logic. 38 | */ 39 | struct Q2Meta { 40 | /** 41 | * @enum Stage 42 | * @brief Stages of the QoS-2 two-phase commit protocol. 43 | */ 44 | enum class Stage : uint8_t { PREPARE, COMMIT }; 45 | Stage stage{Stage::PREPARE}; ///< Current stage 46 | std::vector frame; ///< Frame to be resent 47 | uint32_t retryCount{0}; ///< Number of retries attempted 48 | std::chrono::steady_clock::time_point nextRetry{}; ///< Next retry time point 49 | std::chrono::steady_clock::time_point lastTouched{}; ///< Last activity time point 50 | }; 51 | 52 | /** 53 | * @struct ConnState 54 | * @brief Per-connection state for QoS message management and duplicate detection. 55 | * 56 | * Holds buffers and state for QoS-1 and QoS-2 message delivery, duplicate detection, 57 | * and statistics for a single client connection. 58 | */ 59 | struct ConnState { 60 | /*──────── QoS-1 ────────*/ 61 | std::atomic_uint64_t nextId{ 1 }; ///< Next message ID for QoS-1 62 | ankerl::unordered_dense::map pending1; ///< Pending QoS-1 frames 63 | std::shared_mutex pendMx; ///< Mutex for QoS-1 pending frames 64 | 65 | /* "gördüm" kümesi */ 66 | ankerl::unordered_dense::set seenSet; ///< Set of seen message IDs (duplicate detection) 67 | std::deque> seenQ; ///< Queue of seen IDs and timestamps 68 | 69 | /*──────── QoS-2 ────────*/ 70 | ankerl::unordered_dense::map> pubPrepare; ///< PREPARE state frames 71 | ankerl::unordered_dense::map> pendingResp; ///< COMMIT state frames 72 | ankerl::unordered_dense::map qos2Pending; ///< QoS-2 metadata 73 | std::shared_mutex q2Mx; ///< Mutex for QoS-2 state 74 | 75 | /* İstatistik / quota için toplam bayt */ 76 | std::size_t queuedBytes{ 0 }; ///< Total bytes queued for statistics/quota 77 | }; 78 | 79 | } -------------------------------------------------------------------------------- /src/core/util/thread_pool.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file thread_pool.cpp 3 | * @brief Implementation of the ThreadPool class. 4 | * 5 | * @author Efecan 6 | * @date 2025 7 | */ 8 | #include "binaryrpc/core/util/thread_pool.hpp" 9 | #include 10 | #include 11 | 12 | namespace binaryrpc { 13 | 14 | ThreadPool::ThreadPool(size_t thread_count) 15 | : stop_(false), pending_tasks_(0) { 16 | 17 | // If thread_count is 0, use hardware concurrency 18 | if (thread_count == 0) { 19 | thread_count = std::thread::hardware_concurrency(); 20 | if (thread_count == 0) { 21 | thread_count = 2; // Fallback to 2 threads 22 | } 23 | } 24 | 25 | // Create worker threads 26 | for (size_t i = 0; i < thread_count; ++i) { 27 | threads_.emplace_back(&ThreadPool::workerFunction, this); 28 | } 29 | } 30 | 31 | ThreadPool::~ThreadPool() { 32 | join(); 33 | } 34 | 35 | void ThreadPool::add(std::function task) { 36 | { 37 | std::lock_guard lock(queue_mutex_); 38 | if (stop_) { 39 | throw std::runtime_error("ThreadPool is stopped"); 40 | } 41 | 42 | tasks_.emplace(std::move(task)); 43 | ++pending_tasks_; 44 | } 45 | 46 | condition_.notify_one(); 47 | } 48 | 49 | void ThreadPool::join() { 50 | { 51 | std::lock_guard lock(queue_mutex_); 52 | stop_ = true; 53 | } 54 | 55 | condition_.notify_all(); 56 | 57 | // Wait for all threads to finish 58 | for (auto& thread : threads_) { 59 | if (thread.joinable()) { 60 | thread.join(); 61 | } 62 | } 63 | 64 | threads_.clear(); 65 | } 66 | 67 | size_t ThreadPool::getPendingTaskCount() const { 68 | std::lock_guard lock(queue_mutex_); 69 | return pending_tasks_; 70 | } 71 | 72 | void ThreadPool::workerFunction() { 73 | while (true) { 74 | std::function task; 75 | 76 | { 77 | std::unique_lock lock(queue_mutex_); 78 | 79 | // Wait for a task or stop signal 80 | condition_.wait(lock, [this] { 81 | return stop_ || !tasks_.empty(); 82 | }); 83 | 84 | // If stopped and no tasks, exit 85 | if (stop_ && tasks_.empty()) { 86 | return; 87 | } 88 | 89 | // Get the next task 90 | if (!tasks_.empty()) { 91 | task = std::move(tasks_.front()); 92 | tasks_.pop(); 93 | --pending_tasks_; 94 | } 95 | } 96 | 97 | // Execute the task 98 | if (task) { 99 | try { 100 | task(); 101 | } catch (...) { 102 | // Task exceptions are not propagated to avoid thread pool corruption 103 | // In a production environment, you might want to log these exceptions 104 | } 105 | } 106 | } 107 | } 108 | 109 | } // namespace binaryrpc 110 | -------------------------------------------------------------------------------- /test_server/advanced_general_server.cpp: -------------------------------------------------------------------------------- 1 | #include "binaryrpc/core/rpc/rpc_context.hpp" 2 | #include "binaryrpc/core/protocol/simple_text_protocol.hpp" 3 | #include "binaryrpc/core/util/logger.hpp" 4 | #include 5 | #include "binaryrpc/core/app.hpp" 6 | #include "binaryrpc/core/framework_api.hpp" 7 | #include "binaryrpc/core/protocol/simple_text_protocol.hpp" 8 | #include "binaryrpc/transports/websocket/websocket_transport.hpp" 9 | 10 | using namespace binaryrpc; 11 | 12 | int main() { 13 | 14 | App& app = App::getInstance(); 15 | 16 | SessionManager& sm = app.getSessionManager(); 17 | auto ws = std::make_unique(sm, /*idleTimeoutSec=*/30); 18 | 19 | ReliableOptions opts; 20 | opts.level = QoSLevel::None; 21 | 22 | ws->setReliable(opts); 23 | 24 | app.setTransport(std::move(ws)); 25 | 26 | auto& api = app.getFrameworkApi(); 27 | 28 | // === Middleware: log session ID ve requestCount === 29 | app.use([](Session& s, const std::string& method, std::vector& payload, NextFunc next) { 30 | int count = 0; 31 | try { count = s.get("requestCount"); } catch (...) {} 32 | s.set("requestCount", count + 1); 33 | LOG_INFO("[MW] Session " + s.id() + " requestCount=" + std::to_string(count + 1)); 34 | next(); 35 | }); 36 | 37 | // === RPC: set_data_indexed_false === 38 | app.registerRPC("set_data_indexed_false", [&api](const std::vector& req, RpcContext& ctx) { 39 | std::string sid = ctx.session().id(); 40 | std::string val(req.begin(), req.end()); 41 | api.setField(sid, "username", val, false); 42 | LOG_INFO("[RPC] set_data_indexed_false: " + val); 43 | ctx.reply(std::vector{'O','K'}); 44 | }); 45 | 46 | // === RPC: set_data_indexed_true === 47 | app.registerRPC("set_data_indexed_true", [&api](const std::vector& req, RpcContext& ctx) { 48 | std::string sid = ctx.session().id(); 49 | std::string val(req.begin(), req.end()); 50 | api.setField(sid, "username", val, true); 51 | LOG_INFO("[RPC] set_data_indexed_true: " + val); 52 | ctx.reply(std::vector{'O','K'}); 53 | }); 54 | 55 | // === RPC: get_data === 56 | app.registerRPC("get_data", [&api](const std::vector&, RpcContext& ctx) { 57 | std::string sid = ctx.session().id(); 58 | auto val = api.getField(sid, "username"); 59 | if (val.has_value()) { 60 | LOG_INFO("[RPC] get_data: found " + val.value()); 61 | std::vector out(val->begin(), val->end()); 62 | ctx.reply(out); 63 | } else { 64 | LOG_WARN("[RPC] get_data: no value"); 65 | ctx.reply({'N','O','N','E'}); 66 | } 67 | }); 68 | 69 | // === RPC: find_by === 70 | app.registerRPC("find_by", [&api](const std::vector& req, RpcContext& ctx) { 71 | std::string key = "username"; 72 | std::string val(req.begin(), req.end()); 73 | auto sessions = api.findBy(key, val); // <<< template argüman yok! 74 | std::string ids; 75 | for (const auto& s : sessions) { 76 | ids += s->id() + ","; 77 | } 78 | if (!ids.empty()) ids.pop_back(); // remove last comma 79 | LOG_INFO("[RPC] find_by: " + ids); 80 | std::vector out(ids.begin(), ids.end()); 81 | ctx.reply(out); 82 | }); 83 | 84 | 85 | 86 | app.run(9000); 87 | 88 | std::cin.get(); 89 | return 0; 90 | } 91 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 📝 Description 2 | 3 | Brief description of the changes made in this pull request. 4 | 5 | ## 🔄 Type of Change 6 | 7 | Please delete options that are not relevant: 8 | 9 | - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) 10 | - [ ] ✨ New feature (non-breaking change which adds functionality) 11 | - [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - [ ] 📚 Documentation update 13 | - [ ] 🧹 Code refactoring (no functional changes) 14 | - [ ] ⚡ Performance improvement 15 | - [ ] 🧪 Test addition or update 16 | - [ ] 🔧 Build system or CI/CD change 17 | 18 | ## 🎯 Related Issue 19 | 20 | Closes #[issue number] 21 | Related to #[issue number] 22 | 23 | ## 🧪 Testing 24 | 25 | Please describe the tests that you ran to verify your changes: 26 | 27 | - [ ] Unit tests pass 28 | - [ ] Integration tests pass 29 | - [ ] Manual testing completed 30 | - [ ] Performance tests (if applicable) 31 | - [ ] Memory leak tests (if applicable) 32 | 33 | ### Test Environment 34 | 35 | - **OS:** [e.g., Windows 10, Ubuntu 20.04, macOS 12.0] 36 | - **Compiler:** [e.g., GCC 11.2, Clang 13.0, MSVC 2019] 37 | - **Dependencies:** [e.g., uWebSockets version, Folly version] 38 | 39 | ## 📊 Breaking Changes 40 | 41 | If this PR introduces breaking changes, please describe them and provide migration steps: 42 | 43 | - **Breaking Change 1:** [Description] 44 | - Migration: [Steps to migrate] 45 | - **Breaking Change 2:** [Description] 46 | - Migration: [Steps to migrate] 47 | 48 | ## 🔍 Code Quality 49 | 50 | - [ ] Code follows the project's style guidelines 51 | - [ ] Self-review of code has been completed 52 | - [ ] Code is self-documenting 53 | - [ ] No new warnings are generated 54 | - [ ] Documentation has been updated (if applicable) 55 | 56 | ## 📚 Documentation 57 | 58 | - [ ] README.md has been updated (if applicable) 59 | - [ ] API documentation has been updated (if applicable) 60 | - [ ] Examples have been updated (if applicable) 61 | - [ ] CHANGELOG.md has been updated (if applicable) 62 | 63 | ## 🔒 Security 64 | 65 | - [ ] No security vulnerabilities introduced 66 | - [ ] Security implications have been considered 67 | - [ ] Authentication/authorization changes properly tested 68 | 69 | ## 📈 Performance 70 | 71 | - [ ] Performance impact has been considered 72 | - [ ] No performance regressions introduced 73 | - [ ] Performance tests pass (if applicable) 74 | 75 | ## 🧹 Code Review Checklist 76 | 77 | ### Before Submitting 78 | 79 | - [ ] Code compiles without errors 80 | - [ ] All tests pass 81 | - [ ] No memory leaks (checked with valgrind/sanitizers) 82 | - [ ] Code is properly formatted 83 | - [ ] Commit messages are clear and descriptive 84 | - [ ] Branch is up to date with master/main 85 | 86 | ### Code Quality 87 | 88 | - [ ] Functions are small and focused 89 | - [ ] Variable names are descriptive 90 | - [ ] Comments explain "why" not "what" 91 | - [ ] Error handling is appropriate 92 | - [ ] No code duplication 93 | - [ ] Proper use of RAII and smart pointers 94 | 95 | ## 📋 Additional Notes 96 | 97 | Add any other context about the pull request here. 98 | 99 | ## 🎉 Screenshots (if applicable) 100 | 101 | If the changes include UI updates, please add screenshots. 102 | 103 | ## 🔗 Related Links 104 | 105 | - [Documentation](link-to-docs) 106 | - [Issue Discussion](link-to-discussion) 107 | - [Related PR](link-to-related-pr) 108 | 109 | --- 110 | 111 | **Note:** Please ensure all checkboxes are completed before submitting the PR. This helps maintain code quality and ensures a smooth review process. -------------------------------------------------------------------------------- /RoadMap.md: -------------------------------------------------------------------------------- 1 | # 🛣️ BinaryRPC Roadmap 2 | 3 | Welcome! This is the official development roadmap for BinaryRPC. 4 | This project reached #1 on Hacker News on July 12, 2025 — thank you all for the incredible feedback. 🙏 5 | We're currently working on delivering a faster, leaner, and more developer-friendly core. 6 | 7 | --- 8 | 9 | ## 🔥 v0.2.0 - Minimal Core Release (Work in Progress) 10 | 11 | Goal: Strip down all unnecessary dependencies and provide a clean, production-ready, plug-and-play core. 12 | 13 | ### ✅ Core Changes 14 | - [ ] Remove `folly`, `glog`, `gflags` dependencies completely 15 | - [ ] Replace `F14Map` with `absl::flat_hash_map` 16 | - [ ] Introduce a minimal RAII-friendly `ThreadPool` implementation 17 | - [ ] Add basic error-handling macros and assertions 18 | - [ ] Clean CMake targets with options like `-DBINARYRPC_BUILD_EXAMPLES=ON` 19 | 20 | ### 🚧 API & Usability Improvements 21 | - [ ] Simplified Payload API: `payload["key"]` with type inference 22 | - [ ] `payload.has("key")`, `.as()`, `.toJson()` sugar syntax 23 | - [ ] Optional custom handler registration with variadic template helper 24 | - [ ] Single-header `binaryrpc.hpp` version (core only, no QoS) 25 | 26 | ### 📦 Distribution & Packaging 27 | - [ ] Add `vcpkg.json` manifest to lock dependency versions 28 | - [ ] Package as `binaryrpc-core` and `binaryrpc-full` 29 | - [ ] CMake install targets & `find_package` support 30 | 31 | --- 32 | 33 | ## 🎯 v0.3.0 - Transport Abstraction & Raw TCP 34 | 35 | Goal: Provide alternative transport backends beyond WebSockets. 36 | 37 | - [ ] `TransportInterface` abstraction (WebSocket, Raw TCP, QUIC) 38 | - [ ] Add Raw TCP backend (epoll-based) 39 | - [ ] Graceful fallback from WebSocket to Raw TCP 40 | - [ ] Optional TLS (OpenSSL wrapper, cert pinning support) 41 | - [ ] Start QUIC research (experimental branch) 42 | 43 | --- 44 | 45 | ## 📊 v0.4.0 - Observability & Benchmarks 46 | 47 | Goal: Empower developers to debug and measure performance easily. 48 | 49 | - [ ] Internal latency measurement (per message) 50 | - [ ] Integrated Benchmark Tool (BinaryRPC vs gRPC testbed) 51 | - [ ] CPU and memory usage stats (optional) 52 | - [ ] Build GitHub Actions CI job to run benchmarks on PRs 53 | 54 | --- 55 | 56 | ## 🤝 v0.5.0 - Community & Extensibility 57 | 58 | Goal: Foster a strong developer community around BinaryRPC. 59 | 60 | - [ ] Define public plugin interface (e.g., for logging, auth) 61 | - [ ] Open Discussions for feature voting 62 | - [ ] Add good-first-issue tags to GitHub Issues 63 | - [ ] Publish a “Build Your First Real-time App with BinaryRPC” tutorial 64 | - [ ] Create a Discord/Matrix or GitHub Discussions board 65 | 66 | --- 67 | 68 | ## 🌌 Beyond 1.0 (Ideas and Experiments) 69 | 70 | - [ ] QUIC Transport Backend (through msquic or ngtcp2) 71 | - [ ] Binary serialization option (e.g., Flatbuffers or custom) 72 | - [ ] Actor-style task scheduling layer (optional) 73 | - [ ] Codegen from `.rpc` schema files (TBD) 74 | - [ ] Language bindings: Python, Rust, Go, etc. 75 | 76 | --- 77 | 78 | ## 🧠 Inspiration & Philosophy 79 | 80 | BinaryRPC is designed for: 81 | - **Real-time systems**: multiplayer games, dashboards, financial feeds 82 | - **Low-latency environments**: sub-millisecond response time 83 | - **Minimal footprint**: no reflection, no slow abstraction soup 84 | - **Explicit over magic**: if you see it, you can debug it. 85 | 86 | Thanks to everyone who supported the project on Hacker News. 87 | Your feedback directly shapes this roadmap — feel free to open a [Discussion](https://github.com/efecan0/binaryrpc-framework/discussions) or submit an [Issue](https://github.com/efecan0/binaryrpc-framework/issues) if you want to help! 88 | 89 | Stay tuned, 90 | — Efecan 91 | -------------------------------------------------------------------------------- /src/internal/core/qos/duplicate_filter.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file duplicate_filter.hpp 3 | * @brief DuplicateFilter for QoS-1 deduplication in BinaryRPC. 4 | * 5 | * Provides a deduplication filter for RPC calls, preventing duplicate processing 6 | * of the same payload within a configurable TTL window. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace binaryrpc::qos { 19 | 20 | /** 21 | * @class DuplicateFilter 22 | * @brief Deduplication filter for QoS-1 RPC calls. 23 | * 24 | * Tracks recently seen RPC payloads and prevents duplicate processing within a TTL window. 25 | * Used to ensure at-least-once delivery semantics without duplicate side effects. 26 | */ 27 | class DuplicateFilter { 28 | public: 29 | /** 30 | * @brief Accept or reject an RPC call based on recent duplicates. 31 | * 32 | * @param rpcPayload The payload of the RPC call (e.g., "counter:inc", "abc:inc") 33 | * @param ttl If the same payload is received within this duration, it is considered a duplicate 34 | * @return true → RPC call is new or TTL has expired 35 | * false → Duplicate RPC call (within TTL window) 36 | */ 37 | bool accept(const std::vector& rpcPayload, 38 | std::chrono::milliseconds ttl = std::chrono::milliseconds(1000)) // 1 second default 39 | { 40 | using clock = std::chrono::steady_clock; 41 | const auto now = clock::now(); 42 | 43 | // Convert RPC payload to string and hash it 44 | std::string rpcStr(rpcPayload.begin(), rpcPayload.end()); 45 | auto rpcHash = std::hash{}(rpcStr); 46 | 47 | // Remove expired RPC entries from the TTL window 48 | while (!order_.empty() && now - order_.front().second > ttl) { 49 | seen_.erase(order_.front().first); 50 | order_.pop_front(); 51 | } 52 | 53 | // Has this RPC been called before? 54 | auto it = seen_.find(rpcHash); 55 | if (it == seen_.end()) { 56 | // New RPC call 57 | seen_.insert(rpcHash); 58 | order_.emplace_back(rpcHash, now); 59 | return true; 60 | } 61 | 62 | // Duplicate RPC call, check TTL 63 | for (const auto& [hash, timestamp] : order_) { 64 | if (hash == rpcHash) { 65 | if (now - timestamp > ttl) { 66 | // TTL expired → accept and refresh 67 | seen_.erase(hash); 68 | order_.erase(std::remove_if(order_.begin(), order_.end(), 69 | [hash](const auto& pair) { return pair.first == hash; }), 70 | order_.end()); 71 | seen_.insert(rpcHash); 72 | order_.emplace_back(rpcHash, now); 73 | return true; 74 | } 75 | return false; // Still within TTL → reject 76 | } 77 | } 78 | 79 | return true; // Should not reach here, but return true for safety 80 | } 81 | 82 | private: 83 | std::unordered_set seen_; ///< Set of hashes for deduplication 84 | std::deque> order_; ///< Hash and timestamp pairs for TTL tracking 85 | static constexpr std::size_t WINDOW = 2048; ///< Memory window limit (approx. 16KB) 86 | }; 87 | 88 | } -------------------------------------------------------------------------------- /include/binaryrpc/core/interfaces/itransport.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file itransport.hpp 3 | * @brief Interface for transport layers in BinaryRPC. 4 | * 5 | * Defines the ITransport interface for implementing custom transport layers 6 | * (e.g., WebSocket, TCP) for BinaryRPC communication. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include 13 | #include 14 | #include 15 | #include "binaryrpc/core/session/session.hpp" 16 | #include "binaryrpc/core/util/qos.hpp" 17 | 18 | namespace binaryrpc { 19 | 20 | /** 21 | * @typedef DataCallback 22 | * @brief Callback type for receiving data from the transport. 23 | */ 24 | using DataCallback = std::function&, std::shared_ptr, void*)>; 25 | /** 26 | * @typedef SessionRegisterCallback 27 | * @brief Callback type for registering a new session. 28 | */ 29 | using SessionRegisterCallback = std::function)>; 30 | /** 31 | * @typedef DisconnectCallback 32 | * @brief Callback type for handling client disconnections. 33 | */ 34 | using DisconnectCallback = std::function)>; 35 | 36 | /** 37 | * @class ITransport 38 | * @brief Interface for custom transport layers in BinaryRPC. 39 | * 40 | * Implement this interface to provide custom transport mechanisms (e.g., WebSocket, TCP). 41 | */ 42 | class ITransport { 43 | public: 44 | virtual ~ITransport() = default; 45 | /** 46 | * @brief Start the transport on the specified port. 47 | * @param port Port number to listen on 48 | */ 49 | virtual void start(uint16_t port) = 0; 50 | /** 51 | * @brief Stop the transport. 52 | */ 53 | virtual void stop() = 0; 54 | /** 55 | * @brief Send data to all clients. 56 | * @param data Data to send 57 | */ 58 | virtual void send(const std::vector& data) = 0; 59 | /** 60 | * @brief Send data to a specific client connection. 61 | * @param connection Pointer to the client connection 62 | * @param data Data to send 63 | */ 64 | virtual void sendToClient(void* connection, const std::vector& data) = 0; 65 | /** 66 | * @brief Send data to a specific session. 67 | * @param session Shared pointer to the session 68 | * @param data Data to send 69 | */ 70 | virtual void sendToSession(std::shared_ptr session, const std::vector& data) = 0; 71 | /** 72 | * @brief Disconnect a specific client connection. 73 | * @param connection Pointer to the client connection 74 | */ 75 | virtual void disconnectClient(void* connection) = 0; 76 | /** 77 | * @brief Set the callback for receiving data from clients. 78 | * @param callback DataCallback function 79 | */ 80 | virtual void setCallback(DataCallback callback) = 0; 81 | /** 82 | * @brief Set the callback for session registration. 83 | * @param cb SessionRegisterCallback function 84 | */ 85 | virtual void setSessionRegisterCallback(SessionRegisterCallback cb) = 0; 86 | /** 87 | * @brief Set the callback for client disconnections. 88 | * @param cb DisconnectCallback function 89 | */ 90 | virtual void setDisconnectCallback(DisconnectCallback cb) = 0; 91 | /** 92 | * @brief Set reliable delivery options for the transport. 93 | * @param options ReliableOptions struct 94 | */ 95 | virtual void setReliable(const ReliableOptions&) = 0; 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /tests/session_manager.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "binaryrpc/core/session/session_manager.hpp" 3 | #include 4 | #include 5 | #include 6 | 7 | using namespace binaryrpc; 8 | 9 | // Yardımcı fonksiyon: Test için ClientIdentity oluşturur 10 | static ClientIdentity makeIdent(const std::string& id = "test", uint64_t device = 0) { 11 | ClientIdentity cid; 12 | cid.clientId = id; 13 | cid.deviceId = device; 14 | return cid; 15 | } 16 | 17 | TEST_CASE("SessionManager createSession() benzersiz ID üretir", "[session]") { 18 | SessionManager sm; 19 | auto s1 = sm.createSession(makeIdent("s1"), 0); 20 | auto s2 = sm.createSession(makeIdent("s2"), 0); 21 | REQUIRE(s1 != nullptr); 22 | REQUIRE(s2 != nullptr); 23 | REQUIRE(s1->id() != s2->id()); 24 | } 25 | 26 | TEST_CASE("indexedSet() ikincil indekse ekler ve find() döner", "[session][index]") { 27 | SessionManager sm; 28 | auto s = sm.createSession(makeIdent("a"), 0); 29 | 30 | sm.indexedSet(s, "userId", 42); // int ➜ "42" 31 | 32 | auto ids_ptr = sm.findIndexed("userId", "42"); 33 | REQUIRE(ids_ptr); 34 | REQUIRE(ids_ptr->size() == 1); 35 | REQUIRE(ids_ptr->count(s->id()) == 1); 36 | } 37 | 38 | TEST_CASE("removeSession() kaydı ve indeksleri temizler", "[session][index]") { 39 | SessionManager sm; 40 | auto s = sm.createSession(makeIdent("b"), 0); 41 | sm.indexedSet(s, "room", std::string("lobby")); 42 | 43 | sm.removeSession(s->id()); 44 | REQUIRE(sm.getSession(s->id()) == nullptr); 45 | REQUIRE_FALSE(sm.findIndexed("room", "lobby")); 46 | } 47 | 48 | TEST_CASE("indexedSet overwrites previous value but maintains consistency", "[session][index]") { 49 | SessionManager sm; 50 | auto s = sm.createSession(makeIdent("c"), 0); 51 | sm.indexedSet(s, "room", std::string("lobby")); 52 | sm.indexedSet(s, "room", std::string("garden")); // overwrite 53 | 54 | REQUIRE_FALSE(sm.findIndexed("room", "lobby")); 55 | auto ids_ptr = sm.findIndexed("room", "garden"); 56 | REQUIRE(ids_ptr); 57 | REQUIRE(ids_ptr->count(s->id()) == 1); 58 | } 59 | 60 | TEST_CASE("find() returns multiple sessions sharing same key", "[session][index]") { 61 | SessionManager sm; 62 | auto a = sm.createSession(makeIdent("a1"), 0); 63 | auto b = sm.createSession(makeIdent("a2"), 0); 64 | sm.indexedSet(a, "tenant", 5); 65 | sm.indexedSet(b, "tenant", 5); 66 | 67 | auto ids_ptr = sm.findIndexed("tenant", "5"); 68 | REQUIRE(ids_ptr); 69 | REQUIRE(ids_ptr->size() == 2); 70 | REQUIRE(ids_ptr->count(a->id()) == 1); 71 | REQUIRE(ids_ptr->count(b->id()) == 1); 72 | } 73 | 74 | TEST_CASE("removeSession on unknown id is no‑op", "[session]") { 75 | SessionManager sm; 76 | REQUIRE_NOTHROW(sm.removeSession("9999")); 77 | } 78 | 79 | // 🔄 Concurrency testi – sadeleştirilmiş 80 | TEST_CASE("Concurrent indexedSet is thread‑safe", "[session][concurrency]") { 81 | SessionManager sm; 82 | auto s = sm.createSession(makeIdent("conc"), 0); 83 | 84 | constexpr int N = 1000; 85 | std::atomic done{ 0 }; 86 | std::vector threads; 87 | 88 | for (int i = 0; i < N; ++i) { 89 | threads.emplace_back([&, i] { 90 | sm.indexedSet(s, "counter", i); 91 | done.fetch_add(1, std::memory_order_relaxed); 92 | }); 93 | } 94 | for (auto& t : threads) t.join(); 95 | 96 | REQUIRE(done == N); 97 | } 98 | 99 | 100 | TEST_CASE("indices() snapshot reflects current state after erase", "[session][index]") { 101 | SessionManager sm; 102 | auto s = sm.createSession(makeIdent("snap"), 0); 103 | sm.indexedSet(s, "user", 1); 104 | sm.removeSession(s->id()); 105 | 106 | auto ids_ptr = sm.findIndexed("user", "1"); 107 | REQUIRE_FALSE(ids_ptr); 108 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | project(binaryrpc LANGUAGES CXX) 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 7 | 8 | # MSVC-specific settings, applied only if using MSVC 9 | if(MSVC) 10 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") 11 | add_compile_options(/EHsc /W4 /permissive- /analyze /utf-8) 12 | add_definitions(-D_WIN32_WINNT=0x0601 -DWIN32_LEAN_AND_MEAN -DNOMINMAX) 13 | endif() 14 | 15 | # Since the vcpkg toolchain file is used (via CMakePresets.json), 16 | # vcpkg will automatically find the packages. We just need to declare them. 17 | find_package(unofficial-uwebsockets CONFIG REQUIRED) 18 | find_package(Threads REQUIRED) 19 | find_package(jwt-cpp REQUIRED) 20 | 21 | find_package(msgpack-cxx REQUIRED) 22 | find_package(nlohmann_json QUIET) # Optional dependency for examples 23 | find_package(OpenSSL QUIET) # Optional, mainly for non-windows 24 | 25 | # ---------------------- 26 | # Source files 27 | # ---------------------- 28 | file(GLOB_RECURSE BINARYRPC_CORE_SOURCES CONFIGURE_DEPENDS src/*.cpp src/**/*.cpp) 29 | set(_EXTRA_CANDIDATES 30 | src/core/session/generic_index.cpp 31 | src/core/session/session_manager.cpp 32 | src/plugins/room_plugin.cpp 33 | src/core/protocol/msgpack_protocol.cpp 34 | src/plugins/reliable_plugin.cpp) 35 | foreach(_f IN LISTS _EXTRA_CANDIDATES) 36 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${_f}") 37 | list(APPEND BINARYRPC_CORE_SOURCES ${_f}) 38 | endif() 39 | endforeach() 40 | if(BINARYRPC_CORE_SOURCES STREQUAL "") 41 | message(FATAL_ERROR "No .cpp files found for binaryrpc_core — check source paths.") 42 | endif() 43 | 44 | add_library(binaryrpc_core STATIC ${BINARYRPC_CORE_SOURCES}) 45 | 46 | # PUBLIC includes are handled by vcpkg's targets. We just need to add our own project's include path. 47 | target_include_directories(binaryrpc_core 48 | PUBLIC 49 | ${CMAKE_CURRENT_SOURCE_DIR}/include 50 | ${CMAKE_CURRENT_SOURCE_DIR}/third_party/unordered_dense/include 51 | PRIVATE 52 | ${CMAKE_CURRENT_SOURCE_DIR}/src 53 | ) 54 | 55 | # Link all libraries using their modern imported targets provided by vcpkg. 56 | target_link_libraries(binaryrpc_core PUBLIC 57 | unofficial::uwebsockets::uwebsockets 58 | Threads::Threads 59 | jwt-cpp::jwt-cpp 60 | msgpack-cxx 61 | ) 62 | # Conditionally link optional libraries if found 63 | if(nlohmann_json_FOUND) 64 | target_link_libraries(binaryrpc_core PUBLIC nlohmann_json::nlohmann_json) 65 | endif() 66 | if(OpenSSL_FOUND) 67 | target_link_libraries(binaryrpc_core PUBLIC OpenSSL::SSL OpenSSL::Crypto) 68 | endif() 69 | 70 | 71 | 72 | # ---------------------- 73 | # Tests and example applications 74 | # ---------------------- 75 | option(BINARYRPC_BUILD_TESTS "Build unit tests with Catch2" ON) 76 | if (BINARYRPC_BUILD_TESTS) 77 | enable_testing() 78 | add_subdirectory(tests) 79 | target_compile_definitions(binaryrpc_core PRIVATE BINARYRPC_TEST) 80 | endif() 81 | 82 | option(BUILD_EXAMPLES "Build example programs" ON) 83 | if(BUILD_EXAMPLES) 84 | add_subdirectory(example_server) 85 | endif() 86 | 87 | # ---------------------- 88 | # Integration/regression test executables 89 | # ---------------------- 90 | foreach(entry 91 | binaryrpc_middleware_integration_test:test_server/middleware_test.cpp 92 | binaryrpc_session_integration_test:test_server/session_test.cpp 93 | binaryrpc_advanced_general_integration_test:test_server/advanced_general_server.cpp 94 | binaryrpc_error_propagation_integration_test:test_server/error_propagation_test.cpp 95 | binaryrpc_qos1_integration_test:test_server/qos1_test.cpp 96 | binaryrpc_qos2_integration_test:test_server/qos2_test.cpp 97 | binaryrpc_main_test:test_server/main.cpp) 98 | string(REPLACE ":" ";" pair ${entry}) 99 | list(GET pair 0 tgt) 100 | list(GET pair 1 src) 101 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${src}" AND NOT TARGET ${tgt}) 102 | add_executable(${tgt} ${src}) 103 | target_link_libraries(${tgt} PRIVATE binaryrpc_core) 104 | endif() 105 | endforeach() 106 | -------------------------------------------------------------------------------- /include/binaryrpc/core/framework_api.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file framework_api.hpp 3 | * @brief FrameworkAPI provides helper functions for accessing and manipulating sessions from outside the core. 4 | * 5 | * This API allows sending data to sessions, disconnecting sessions, listing session IDs, and managing session state fields. 6 | * It acts as a bridge between the core session/transport management and user-level logic. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace binaryrpc { 19 | // Forward declarations 20 | class Session; 21 | class SessionManager; 22 | class ITransport; 23 | 24 | /** 25 | * @class FrameworkAPI 26 | * @brief Helper API for accessing and manipulating sessions from outside the core. 27 | * 28 | * Provides methods for sending data, disconnecting sessions, listing session IDs, and managing session state fields. 29 | */ 30 | class FrameworkAPI { 31 | public: 32 | /** 33 | * @brief Construct a FrameworkAPI instance. 34 | * @param sm Pointer to the SessionManager 35 | * @param tr Pointer to the ITransport instance 36 | */ 37 | FrameworkAPI(SessionManager* sm, ITransport* tr); 38 | 39 | /** 40 | * @brief Destructor for the FrameworkAPI class. 41 | */ 42 | ~FrameworkAPI(); 43 | 44 | /** 45 | * @brief Send data to a session by session ID. 46 | * @param sid Session ID 47 | * @param data Data to send 48 | * @return True if the data was sent successfully, false otherwise 49 | */ 50 | bool sendTo(const std::string& sid, const std::vector& data) const; 51 | 52 | /** 53 | * @brief Send data to a specific session object. 54 | * @param session Shared pointer to the session 55 | * @param data Data to send 56 | */ 57 | void sendToSession(std::shared_ptr session, const std::vector& data); 58 | 59 | /** 60 | * @brief Disconnect a session by session ID. 61 | * @param sid Session ID 62 | * @return True if the session was disconnected, false otherwise 63 | */ 64 | bool disconnect(const std::string& sid) const; 65 | 66 | /** 67 | * @brief List all active session IDs. 68 | * @return Vector of session ID strings 69 | */ 70 | std::vector listSessionIds() const; 71 | 72 | /** 73 | * @brief Set a custom field for a session. 74 | * @tparam T Type of the value 75 | * @param sid Session ID 76 | * @param key Field key 77 | * @param value Value to set 78 | * @param indexed Whether the field should be indexed for fast lookup 79 | * @return True if the field was set successfully, false otherwise 80 | */ 81 | template 82 | bool setField(const std::string& sid, 83 | const std::string& key, 84 | const T& value, 85 | bool indexed = false); 86 | 87 | /** 88 | * @brief Get a custom field value from a session. 89 | * @tparam T Type of the value 90 | * @param sid Session ID 91 | * @param key Field key 92 | * @return Optional containing the value if present, std::nullopt otherwise 93 | */ 94 | template 95 | std::optional getField(const std::string& sid, 96 | const std::string& key) const; 97 | 98 | /** 99 | * @brief Find sessions by a field key and value. 100 | * @param key Field key to search by 101 | * @param value Value to match 102 | * @return Vector of shared pointers to matching sessions 103 | */ 104 | std::vector> 105 | findBy(const std::string& key, 106 | const std::string& value) const; 107 | 108 | private: 109 | // PIMPL idiom 110 | struct Impl; 111 | std::unique_ptr pImpl_; 112 | }; 113 | 114 | } 115 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to BinaryRPC will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | - Initial open source release 12 | - WebSocket transport implementation 13 | - QoS (Quality of Service) system with three levels 14 | - Session management with TTL support 15 | - Plugin system for extensibility 16 | - Middleware chain for request processing 17 | - JWT authentication middleware 18 | - Rate limiting middleware 19 | - Room plugin for group communication 20 | - MsgPack and SimpleText protocol support 21 | - Comprehensive test suite 22 | - CMake build system 23 | - vcpkg integration 24 | 25 | ### Changed 26 | - N/A 27 | 28 | ### Deprecated 29 | - N/A 30 | 31 | ### Removed 32 | - N/A 33 | 34 | ### Fixed 35 | - N/A 36 | 37 | ### Security 38 | - N/A 39 | 40 | ## [1.0.0] - 2026-01-XX 41 | 42 | ### Added 43 | - Core framework architecture 44 | - Interface-based design for transports, protocols, and plugins 45 | - Session management with automatic cleanup 46 | - RPC context system for request handling 47 | - Framework API for session manipulation 48 | - Handshake inspector system for authentication 49 | - Duplicate filtering for QoS-1 and QoS-2 50 | - Backoff strategies (Linear, Exponential) 51 | - Connection state management 52 | - Offline message queuing 53 | - Generic index system for session queries 54 | - Comprehensive logging system 55 | - Error handling and propagation 56 | - Thread pool integration with Folly 57 | - Memory leak detection and prevention 58 | - Performance optimizations 59 | 60 | ### Technical Details 61 | - C++20 standard compliance 62 | - Header-only core design 63 | - Zero-copy payload handling 64 | - Thread-safe session operations 65 | - RAII resource management 66 | - Exception safety guarantees 67 | - Cross-platform compatibility (Windows, Linux, macOS) 68 | 69 | ### Dependencies 70 | - uWebSockets for WebSocket transport 71 | - Folly for concurrent data structures 72 | - JWT-CPP for authentication 73 | - CPR for HTTP client operations 74 | - nlohmann/json for JSON handling 75 | - Google logging (glog) for logging 76 | - Google flags (gflags) for configuration 77 | - fmt for string formatting 78 | 79 | --- 80 | 81 | ## Version History 82 | 83 | ### Version Numbering 84 | - **MAJOR**: Breaking changes in public API 85 | - **MINOR**: New features (backward compatible) 86 | - **PATCH**: Bug fixes and improvements (backward compatible) 87 | 88 | ### Release Types 89 | - **Alpha**: Early development, unstable 90 | - **Beta**: Feature complete, testing phase 91 | - **RC**: Release candidate, final testing 92 | - **Stable**: Production ready 93 | 94 | ### Support Policy 95 | - Latest stable version: Full support 96 | - Previous major version: Security fixes only 97 | - Older versions: No support 98 | 99 | --- 100 | 101 | ## Migration Guides 102 | 103 | ### From Pre-1.0 Versions 104 | - Session API has been updated to use ClientIdentity 105 | - Transport interface has been simplified 106 | - Plugin system has been standardized 107 | - QoS options have been reorganized 108 | 109 | ### Breaking Changes in 1.0.0 110 | - Session constructor now requires ClientIdentity 111 | - Transport callbacks have been updated 112 | - Plugin initialization has changed 113 | - Some deprecated methods have been removed 114 | 115 | --- 116 | 117 | ## Contributors 118 | 119 | Thank you to all contributors who have helped make BinaryRPC what it is today! 120 | 121 | ### Core Contributors 122 | - [Your Name] - Initial architecture and implementation 123 | - [Other Contributors] - Various improvements and fixes 124 | 125 | ### Community Contributors 126 | - [List community contributors here] 127 | 128 | --- 129 | 130 | ## Acknowledgments 131 | 132 | - **uWebSockets** team for the excellent WebSocket library 133 | - **Facebook Folly** team for the concurrent data structures 134 | - **Catch2** team for the testing framework 135 | - **vcpkg** team for the package manager 136 | - **CMake** team for the build system 137 | 138 | --- 139 | 140 | *This changelog is maintained by the BinaryRPC development team.* -------------------------------------------------------------------------------- /include/binaryrpc/core/util/thread_pool.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file thread_pool.hpp 3 | * @brief Simple thread pool implementation to replace Folly's CPUThreadPoolExecutor. 4 | * 5 | * Provides a lightweight, efficient thread pool for asynchronous task execution. 6 | * Designed to be a drop-in replacement for Folly's thread pool functionality. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | namespace binaryrpc { 24 | 25 | /** 26 | * @class ThreadPool 27 | * @brief Simple thread pool implementation for asynchronous task execution. 28 | * 29 | * This class provides a thread pool that can execute tasks asynchronously. 30 | * It's designed to be a lightweight replacement for Folly's CPUThreadPoolExecutor. 31 | */ 32 | class ThreadPool { 33 | public: 34 | /** 35 | * @brief Constructor for ThreadPool. 36 | * @param thread_count Number of worker threads to create. If 0, uses hardware concurrency. 37 | */ 38 | explicit ThreadPool(size_t thread_count = 0); 39 | 40 | /** 41 | * @brief Destructor for ThreadPool. 42 | * 43 | * Waits for all threads to finish and joins them. 44 | */ 45 | ~ThreadPool(); 46 | 47 | // Delete copy constructor and assignment operator 48 | ThreadPool(const ThreadPool&) = delete; 49 | ThreadPool& operator=(const ThreadPool&) = delete; 50 | 51 | /** 52 | * @brief Add a task to the thread pool. 53 | * @param task Function to execute 54 | * @return Future containing the result of the task 55 | */ 56 | template 57 | auto add(F&& task, Args&&... args) -> std::future; 58 | 59 | /** 60 | * @brief Add a task to the thread pool (void return type). 61 | * @param task Function to execute 62 | */ 63 | void add(std::function task); 64 | 65 | /** 66 | * @brief Wait for all tasks to complete. 67 | */ 68 | void join(); 69 | 70 | /** 71 | * @brief Get the number of worker threads. 72 | * @return Number of worker threads 73 | */ 74 | size_t getThreadCount() const { return threads_.size(); } 75 | 76 | /** 77 | * @brief Get the number of pending tasks. 78 | * @return Number of pending tasks 79 | */ 80 | size_t getPendingTaskCount() const; 81 | 82 | private: 83 | /** 84 | * @brief Worker thread function. 85 | */ 86 | void workerFunction(); 87 | 88 | std::vector threads_; ///< Worker threads 89 | std::queue> tasks_; ///< Task queue 90 | mutable std::mutex queue_mutex_; ///< Mutex for task queue 91 | std::condition_variable condition_; ///< Condition variable for task signaling 92 | std::atomic stop_; ///< Stop flag 93 | std::atomic pending_tasks_; ///< Number of pending tasks 94 | }; 95 | 96 | // Template implementation for add method 97 | template 98 | auto ThreadPool::add(F&& task, Args&&... args) -> std::future { 99 | using return_type = decltype(task(args...)); 100 | 101 | auto packaged_task = std::make_shared>( 102 | std::bind(std::forward(task), std::forward(args)...) 103 | ); 104 | 105 | std::future result = packaged_task->get_future(); 106 | 107 | { 108 | std::lock_guard lock(queue_mutex_); 109 | if (stop_) { 110 | throw std::runtime_error("ThreadPool is stopped"); 111 | } 112 | 113 | tasks_.emplace([packaged_task]() { (*packaged_task)(); }); 114 | ++pending_tasks_; 115 | } 116 | 117 | condition_.notify_one(); 118 | return result; 119 | } 120 | 121 | } // namespace binaryrpc 122 | -------------------------------------------------------------------------------- /src/core/framework_api.cpp: -------------------------------------------------------------------------------- 1 | #include "binaryrpc/core/framework_api.hpp" 2 | #include "binaryrpc/core/session/session_manager.hpp" 3 | #include "binaryrpc/core/interfaces/itransport.hpp" 4 | #include "binaryrpc/core/util/logger.hpp" 5 | #include 6 | 7 | namespace binaryrpc { 8 | 9 | // Define the implementation struct 10 | struct FrameworkAPI::Impl { 11 | SessionManager* sm_; 12 | ITransport* tr_; 13 | 14 | Impl(SessionManager* sm, ITransport* tr) : sm_(sm), tr_(tr) {} 15 | }; 16 | 17 | // FrameworkAPI methods delegating to the implementation 18 | FrameworkAPI::FrameworkAPI(SessionManager* sm, ITransport* tr) 19 | : pImpl_(std::make_unique(sm, tr)) {} 20 | 21 | FrameworkAPI::~FrameworkAPI() = default; 22 | 23 | bool FrameworkAPI::sendTo(const std::string& sid, 24 | const std::vector& data) const 25 | { 26 | if (auto s = pImpl_->sm_->getSession(sid)) { 27 | if (!s->liveWs()) return false; 28 | pImpl_->tr_->sendToClient(s->liveWs(), data); 29 | return true; 30 | } 31 | return false; 32 | } 33 | 34 | void FrameworkAPI::sendToSession(std::shared_ptr session, const std::vector& data) { 35 | if (!session) { 36 | LOG_ERROR("Invalid session"); 37 | return; 38 | } 39 | pImpl_->tr_->sendToSession(session, data); 40 | } 41 | 42 | bool FrameworkAPI::disconnect(const std::string& sid) const 43 | { 44 | if (auto s = pImpl_->sm_->getSession(sid)) { 45 | pImpl_->tr_->disconnectClient(s->liveWs()); 46 | return true; 47 | } 48 | return false; 49 | } 50 | 51 | std::vector FrameworkAPI::listSessionIds() const 52 | { 53 | return pImpl_->sm_->listSessionIds(); 54 | } 55 | 56 | std::vector> 57 | FrameworkAPI::findBy(const std::string& key, 58 | const std::string& value) const 59 | { 60 | if (!pImpl_ || !pImpl_->sm_) { 61 | LOG_ERROR("[FrameworkAPI] pImpl_ or SessionManager is null!"); 62 | return {}; 63 | } 64 | std::vector> out; 65 | auto sids_ptr = pImpl_->sm_->findIndexed(key, value); 66 | if (sids_ptr) { 67 | out.reserve(sids_ptr->size()); 68 | for (const auto& sid : *sids_ptr) { 69 | if (auto s = pImpl_->sm_->getSession(sid)) { 70 | out.push_back(s); 71 | } 72 | } 73 | } 74 | return out; 75 | } 76 | 77 | template 78 | bool FrameworkAPI::setField(const std::string& sid, 79 | const std::string& key, 80 | const T& value, 81 | bool indexed) 82 | { 83 | return pImpl_->sm_->setField(sid, key, value, indexed); 84 | } 85 | 86 | template 87 | std::optional FrameworkAPI::getField(const std::string& sid, 88 | const std::string& key) const 89 | { 90 | return pImpl_->sm_->getField(sid, key); 91 | } 92 | 93 | // Explicit template instantiations must be defined in the .cpp file 94 | template bool FrameworkAPI::setField(const std::string&, const std::string&, const std::string&, bool); 95 | template bool FrameworkAPI::setField(const std::string&, const std::string&, const bool&, bool); 96 | template bool FrameworkAPI::setField(const std::string&, const std::string&, const int&, bool); 97 | template bool FrameworkAPI::setField(const std::string&, const std::string&, const uint64_t&, bool); 98 | template bool FrameworkAPI::setField>(const std::string&, const std::string&, const std::vector&, bool); 99 | 100 | template std::optional FrameworkAPI::getField(const std::string&, const std::string&) const; 101 | template std::optional FrameworkAPI::getField(const std::string&, const std::string&) const; 102 | template std::optional FrameworkAPI::getField(const std::string&, const std::string&) const; 103 | template std::optional FrameworkAPI::getField(const std::string&, const std::string&) const; 104 | template std::optional> FrameworkAPI::getField>(const std::string&, const std::string&) const; 105 | 106 | } 107 | -------------------------------------------------------------------------------- /include/binaryrpc/core/app.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file app.hpp 3 | * @brief Main application (App) class and entry point for the BinaryRPC framework. 4 | * 5 | * Handles application startup, transport and protocol configuration, middleware and plugin management, 6 | * and other core functions. Designed as a singleton. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | #include "binaryrpc/core/types.hpp" 13 | #include 14 | #include 15 | #include 16 | 17 | 18 | 19 | namespace binaryrpc { 20 | 21 | // Forward declarations for public interfaces and types 22 | class ITransport; 23 | class IPlugin; 24 | class IProtocol; 25 | class SessionManager; // This will be opaque 26 | class FrameworkAPI; 27 | 28 | /** 29 | * @class App 30 | * @brief Main application class (singleton) for the BinaryRPC framework. 31 | * 32 | * Handles application startup, transport and protocol configuration, middleware and plugin management, 33 | * and other core functions. Serves as the main entry point for the entire framework. 34 | */ 35 | class App { 36 | public: 37 | /** 38 | * @brief Get the singleton instance. 39 | * @return Reference to the single App instance 40 | */ 41 | static App& getInstance(); 42 | 43 | // Delete copy and assignment operators 44 | App(const App&) = delete; 45 | App& operator=(const App&) = delete; 46 | 47 | /** 48 | * @brief Destructor for the App class. 49 | */ 50 | ~App(); 51 | 52 | /** 53 | * @brief Start the application on the specified port. 54 | * @param port Port number to listen on 55 | */ 56 | void run(uint16_t port); 57 | /** 58 | * @brief Stop the application. 59 | */ 60 | void stop(); 61 | /** 62 | * @brief Set the transport layer. 63 | * @param transport Transport instance to use (e.g., WebSocket) 64 | */ 65 | void setTransport(std::unique_ptr transport); 66 | /** 67 | * @brief Add and initialize a plugin using a shared_ptr. 68 | * @param plugin Plugin instance to add 69 | */ 70 | void usePlugin(std::shared_ptr plugin); 71 | /** 72 | * @brief Add a global middleware. 73 | * @param mw Middleware function to add 74 | */ 75 | void use(Middleware mw); 76 | /** 77 | * @brief Add middleware for a specific method. 78 | * @param method Target method name 79 | * @param mw Middleware function to add 80 | */ 81 | void useFor(const std::string& method, Middleware mw); 82 | /** 83 | * @brief Add middleware for multiple methods. 84 | * @param methods List of target method names 85 | * @param mw Middleware function to add 86 | */ 87 | void useForMulti(const std::vector& methods, Middleware mw); 88 | /** 89 | * @brief Register a new RPC method. 90 | * @param method Name of the method 91 | * @param handler Handler function to execute when the RPC is called 92 | */ 93 | void registerRPC(const std::string& method, RpcContextHandler handler); 94 | /** 95 | * @brief Get the active transport instance. 96 | * @return Pointer to the ITransport instance 97 | */ 98 | ITransport* getTransport() const; 99 | /** 100 | * @brief Get a reference to the SessionManager. 101 | * @return Reference to the SessionManager 102 | */ 103 | SessionManager& getSessionManager(); 104 | /** 105 | * @brief Get a const reference to the SessionManager. 106 | * @return Const reference to the SessionManager 107 | */ 108 | const SessionManager& getSessionManager() const; 109 | /** 110 | * @brief Get a reference to the FrameworkAPI. 111 | * @return Reference to the FrameworkAPI 112 | */ 113 | FrameworkAPI& getFrameworkApi(); 114 | /** 115 | * @brief Set the protocol. 116 | * @param proto Protocol instance to use 117 | */ 118 | void setProtocol(std::shared_ptr proto); 119 | /** 120 | * @brief Get the active protocol instance. 121 | * @return Pointer to the IProtocol instance 122 | */ 123 | IProtocol* getProtocol() const; 124 | 125 | private: 126 | App(); // Private constructor for singleton 127 | 128 | // PIMPL idiom 129 | struct Impl; 130 | std::unique_ptr pImpl_; 131 | }; 132 | } 133 | -------------------------------------------------------------------------------- /src/core/protocol/msgpack_protocol.cpp: -------------------------------------------------------------------------------- 1 | #include "binaryrpc/core/protocol/msgpack_protocol.hpp" 2 | #include "binaryrpc/core/interfaces/iprotocol.hpp" 3 | #include "binaryrpc/core/util/error_types.hpp" // Corrected path 4 | 5 | 6 | namespace binaryrpc { 7 | 8 | //------------------------------------------------------------------------------ 9 | // Safe narrowing conversion: size_t → uint32_t 10 | // Returns false if src > UINT32_MAX 11 | template 12 | [[nodiscard]] static bool safe_u32(S src, uint32_t& dst) noexcept { 13 | if constexpr (std::is_unsigned_v) { 14 | if (src > std::numeric_limits::max()) return false; 15 | } 16 | else { 17 | if (src < 0 || static_cast(src) > std::numeric_limits::max()) return false; 18 | } 19 | dst = static_cast(src); 20 | return true; 21 | } 22 | //------------------------------------------------------------------------------ 23 | 24 | ParsedRequest MsgPackProtocol::parse(const std::vector& data) { 25 | ParsedRequest req; 26 | 27 | try { 28 | msgpack::object_handle oh = 29 | msgpack::unpack(reinterpret_cast(data.data()), 30 | data.size()); 31 | msgpack::object obj = oh.get(); 32 | if (obj.type != msgpack::type::MAP) return req; 33 | 34 | for (uint32_t i = 0; i < obj.via.map.size; ++i) { 35 | auto& kv = obj.via.map.ptr[i]; 36 | std::string key; kv.key.convert(key); 37 | 38 | /* ---- method ---- */ 39 | if (key == "method") { 40 | if (kv.val.type == msgpack::type::STR) { 41 | kv.val.convert(req.methodName); 42 | } 43 | else if (kv.val.type == msgpack::type::BIN) { 44 | req.methodName.assign( 45 | kv.val.via.bin.ptr, 46 | kv.val.via.bin.ptr + kv.val.via.bin.size); 47 | } 48 | } 49 | /* ---- payload ---- */ 50 | else if (key == "payload") { 51 | if (kv.val.type == msgpack::type::MAP) { 52 | // MAP'i binary'e çevir 53 | msgpack::sbuffer sbuf; 54 | msgpack::pack(sbuf, kv.val); 55 | req.payload.assign(sbuf.data(), sbuf.data() + sbuf.size()); 56 | } 57 | else if (kv.val.type == msgpack::type::BIN) { 58 | req.payload.assign( 59 | reinterpret_cast(kv.val.via.bin.ptr), 60 | reinterpret_cast(kv.val.via.bin.ptr) 61 | + kv.val.via.bin.size); 62 | } 63 | else if (kv.val.type == msgpack::type::STR) { 64 | std::string s; kv.val.convert(s); 65 | req.payload.assign(s.begin(), s.end()); 66 | } 67 | } 68 | } 69 | } 70 | catch (...) {} 71 | return req; 72 | } 73 | 74 | std::vector MsgPackProtocol::serialize( 75 | const std::string& method, 76 | const std::vector& payload) 77 | { 78 | msgpack::sbuffer buf; 79 | msgpack::packer pk(&buf); 80 | 81 | pk.pack_map(2); 82 | pk.pack(std::string("method")); 83 | pk.pack(method); 84 | 85 | pk.pack(std::string("payload")); 86 | { 87 | uint32_t len32; 88 | if (!safe_u32(payload.size(), len32)) { 89 | throw std::overflow_error("MsgPackProtocol::serialize: payload size exceeds 4 GiB"); 90 | } 91 | pk.pack_bin(len32); 92 | pk.pack_bin_body(reinterpret_cast(payload.data()), len32); 93 | } 94 | 95 | return { buf.data(), buf.data() + buf.size() }; 96 | } 97 | 98 | std::vector MsgPackProtocol::serializeError(const ErrorObj& e) { 99 | msgpack::sbuffer buf; 100 | msgpack::packer pk(&buf); 101 | 102 | const bool hasData = !e.data.empty(); 103 | pk.pack_map(hasData ? 3 : 2); 104 | pk.pack("code"); pk.pack(static_cast(e.code)); 105 | pk.pack("msg"); pk.pack(e.msg); 106 | if (hasData) { 107 | pk.pack("data"); 108 | { 109 | uint32_t len32; 110 | if (!safe_u32(e.data.size(), len32)) { 111 | throw std::overflow_error("MsgPackProtocol::serializeError: data size exceeds 4 GiB"); 112 | } 113 | pk.pack_bin(len32); 114 | pk.pack_bin_body(reinterpret_cast(e.data.data()), len32); 115 | } 116 | } 117 | return { buf.data(), buf.data() + buf.size() }; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /test_server/qos2_test.cpp: -------------------------------------------------------------------------------- 1 | // test_server.cpp 2 | #include "binaryrpc/core/rpc/rpc_context.hpp" 3 | #include "binaryrpc/core/protocol/simple_text_protocol.hpp" 4 | #include "binaryrpc/core/util/logger.hpp" 5 | #include 6 | #include "binaryrpc/core/app.hpp" 7 | #include "binaryrpc/transports/websocket/websocket_transport.hpp" 8 | #include "binaryrpc/core/util/qos.hpp" 9 | #include "binaryrpc/core/strategies/linear_backoff.hpp" 10 | #include "binaryrpc/core/framework_api.hpp" 11 | #include "binaryrpc/core/util/logger.hpp" 12 | #include 13 | 14 | using namespace binaryrpc; 15 | 16 | int main() { 17 | Logger::inst().setLevel(LogLevel::Debug); 18 | App& app = App::getInstance(); 19 | SessionManager& sm = app.getSessionManager(); 20 | auto ws = std::make_unique(sm, /*idleTimeoutSec=*/30); 21 | ReliableOptions opts; 22 | opts.level = QoSLevel::ExactlyOnce; 23 | opts.baseRetryMs = 50; 24 | opts.maxRetry = 3; 25 | opts.maxBackoffMs = 200; 26 | opts.sessionTtlMs = 3'000; 27 | opts.backoffStrategy = std::make_shared( 28 | std::chrono::milliseconds(opts.baseRetryMs), 29 | std::chrono::milliseconds(opts.maxBackoffMs) 30 | ); 31 | 32 | ws->setReliable(opts); 33 | app.setTransport(std::move(ws)); 34 | 35 | auto& api = app.getFrameworkApi(); 36 | 37 | 38 | app.registerRPC("echo", [](const std::vector& req, RpcContext& ctx) { 39 | ctx.reply(req); 40 | }); 41 | 42 | app.registerRPC("counter", 43 | [&api](const std::vector req, RpcContext& ctx) 44 | { 45 | LOG_DEBUG("[Counter RPC] Received request: " + std::string((char*)req.data(), req.size())); 46 | 47 | if (std::string_view((char*)req.data(), req.size()) != "inc") { 48 | LOG_DEBUG("[Counter RPC] Invalid request, sending empty response"); 49 | ctx.reply({}); 50 | return; 51 | } 52 | 53 | const std::string sid = ctx.session().id(); 54 | const std::string key = "_cnt"; 55 | LOG_DEBUG("[Counter RPC] Processing request for session " + sid); 56 | 57 | /* ---- GET as int ---- */ 58 | int val = 0; 59 | if (auto opt = api.getField(sid, key); opt) { 60 | val = *opt; 61 | LOG_DEBUG("[Counter RPC] Got existing value: " + std::to_string(val)); 62 | } 63 | else { 64 | LOG_DEBUG("[Counter RPC] No existing value, starting from 0"); 65 | } 66 | 67 | ++val; 68 | LOG_DEBUG("[Counter RPC] Incremented value to: " + std::to_string(val)); 69 | 70 | /* ---- SET as int ---- */ 71 | bool success = api.setField(sid, key, val, /*indexed=*/false); 72 | if (!success) { 73 | LOG_ERROR("[Counter RPC] Failed to set counter state for session " + sid); 74 | ctx.reply({}); 75 | return; 76 | } 77 | LOG_DEBUG("[Counter RPC] Successfully saved new value: " + std::to_string(val)); 78 | 79 | /* ---- REPLY ---- */ 80 | std::string txt = std::to_string(val); 81 | LOG_DEBUG("[Counter RPC] Sending response: " + txt); 82 | ctx.reply({ txt.begin(), txt.end() }); 83 | }); 84 | 85 | 86 | // Premium kullanıcı testi için yeni RPC'ler 87 | app.registerRPC("login", [&](auto const& req, RpcContext& ctx) { 88 | std::string p(req.begin(), req.end()); 89 | auto pos = p.find(':'); 90 | if (pos == std::string::npos) return; 91 | 92 | std::string user = p.substr(0, pos); 93 | std::string role = p.substr(pos + 1); 94 | 95 | LOG_INFO("User logged in: " + user + " with role: " + role); 96 | 97 | // Kullanıcı bilgilerini kaydet 98 | api.setField(ctx.session().id(), "username", user, /*indexed=*/true); 99 | api.setField(ctx.session().id(), "role", role, /*indexed=*/true); 100 | 101 | // X kullanıcısı için premium özelliğini ekle 102 | if (user == "X") { 103 | api.setField(ctx.session().id(), "premium", std::string("1"), /*indexed=*/true); 104 | LOG_INFO("Set premium=true for user X"); 105 | } 106 | 107 | ctx.reply({}); 108 | }); 109 | 110 | app.registerRPC("sendToPremium", [&](auto const& req, RpcContext& ctx) { 111 | std::string message(req.begin(), req.end()); 112 | LOG_INFO("Sending message to premium users: " + message); 113 | 114 | // Premium kullanıcıları bul 115 | auto premiumUsers = api.findBy("premium", "1"); 116 | LOG_INFO("Found " + std::to_string(premiumUsers.size()) + " premium users"); 117 | 118 | // Her premium kullanıcıya gönder 119 | for (auto& session : premiumUsers) { 120 | api.sendToSession(session, req); 121 | LOG_INFO("Message sent to premium user: " + session->id()); 122 | } 123 | 124 | ctx.reply({}); 125 | }); 126 | app.run(9010); 127 | 128 | std::cin.get(); 129 | return 0; 130 | } 131 | -------------------------------------------------------------------------------- /test_server/qos1_test.cpp: -------------------------------------------------------------------------------- 1 | // test_server.cpp 2 | #include "binaryrpc/core/rpc/rpc_context.hpp" 3 | #include "binaryrpc/core/protocol/simple_text_protocol.hpp" 4 | #include "binaryrpc/core/util/logger.hpp" 5 | #include 6 | #include "binaryrpc/core/app.hpp" 7 | #include "binaryrpc/transports/websocket/websocket_transport.hpp" 8 | #include "binaryrpc/core/util/qos.hpp" 9 | #include "binaryrpc/core/strategies/linear_backoff.hpp" 10 | #include "binaryrpc/core/framework_api.hpp" 11 | #include "binaryrpc/core/util/logger.hpp" 12 | #include 13 | 14 | using namespace binaryrpc; 15 | 16 | int main(){ 17 | Logger::inst().setLevel(LogLevel::Debug); 18 | App& app = App::getInstance(); 19 | SessionManager& sm = app.getSessionManager(); 20 | auto ws = std::make_unique(sm, /*idleTimeoutSec=*/30); 21 | ReliableOptions opts; 22 | opts.level = QoSLevel::AtLeastOnce; 23 | opts.baseRetryMs = 50; 24 | opts.maxRetry = 3; 25 | opts.maxBackoffMs = 200; 26 | opts.sessionTtlMs = 3'000; 27 | opts.backoffStrategy = std::make_shared( 28 | std::chrono::milliseconds(opts.baseRetryMs), 29 | std::chrono::milliseconds(opts.maxBackoffMs) 30 | ); 31 | 32 | ws->setReliable(opts); 33 | app.setTransport(std::move(ws)); 34 | 35 | auto& api = app.getFrameworkApi(); 36 | 37 | 38 | app.registerRPC("echo", [](const std::vector& req, RpcContext& ctx) { 39 | ctx.reply(req); 40 | }); 41 | 42 | app.registerRPC("counter", 43 | [&api](const std::vector req, RpcContext& ctx) 44 | { 45 | LOG_DEBUG("[Counter RPC] Received request: " + std::string((char*)req.data(), req.size())); 46 | 47 | if (std::string_view((char*)req.data(), req.size()) != "inc") { 48 | LOG_DEBUG("[Counter RPC] Invalid request, sending empty response"); 49 | ctx.reply({}); 50 | return; 51 | } 52 | 53 | const std::string sid = ctx.session().id(); 54 | const std::string key = "_cnt"; 55 | LOG_DEBUG("[Counter RPC] Processing request for session " + sid); 56 | 57 | /* ---- GET as int ---- */ 58 | int val = 0; 59 | if (auto opt = api.getField(sid, key); opt) { 60 | val = *opt; 61 | LOG_DEBUG("[Counter RPC] Got existing value: " + std::to_string(val)); 62 | } else { 63 | LOG_DEBUG("[Counter RPC] No existing value, starting from 0"); 64 | } 65 | 66 | ++val; 67 | LOG_DEBUG("[Counter RPC] Incremented value to: " + std::to_string(val)); 68 | 69 | /* ---- SET as int ---- */ 70 | bool success = api.setField(sid, key, val, /*indexed=*/false); 71 | if (!success) { 72 | LOG_ERROR("[Counter RPC] Failed to set counter state for session " + sid); 73 | ctx.reply({}); 74 | return; 75 | } 76 | LOG_DEBUG("[Counter RPC] Successfully saved new value: " + std::to_string(val)); 77 | 78 | /* ---- REPLY ---- */ 79 | std::string txt = std::to_string(val); 80 | LOG_DEBUG("[Counter RPC] Sending response: " + txt); 81 | ctx.reply({ txt.begin(), txt.end() }); 82 | }); 83 | 84 | // Premium kullanıcı testi için yeni RPC'ler 85 | app.registerRPC("login", [&](auto const& req, RpcContext& ctx) { 86 | std::string p(req.begin(), req.end()); 87 | auto pos = p.find(':'); 88 | if (pos == std::string::npos) return; 89 | 90 | std::string user = p.substr(0, pos); 91 | std::string role = p.substr(pos + 1); 92 | 93 | LOG_INFO("User logged in: " + user + " with role: " + role); 94 | 95 | // Kullanıcı bilgilerini kaydet 96 | api.setField(ctx.session().id(), "username", user, /*indexed=*/true); 97 | api.setField(ctx.session().id(), "role", role, /*indexed=*/true); 98 | 99 | // X kullanıcısı için premium özelliğini ekle 100 | if (user == "X") { 101 | api.setField(ctx.session().id(), "premium", std::string("1"), /*indexed=*/true); 102 | LOG_INFO("Set premium=true for user X"); 103 | } 104 | 105 | ctx.reply({}); 106 | }); 107 | 108 | app.registerRPC("sendToPremium", [&](auto const& req, RpcContext& ctx) { 109 | std::string message(req.begin(), req.end()); 110 | LOG_INFO("Sending message to premium users: " + message); 111 | 112 | // Premium kullanıcıları bul 113 | auto premiumUsers = api.findBy("premium", "1"); 114 | LOG_INFO("Found " + std::to_string(premiumUsers.size()) + " premium users"); 115 | 116 | // Her premium kullanıcıya gönder 117 | for (auto& session : premiumUsers) { 118 | api.sendToSession(session, req); 119 | LOG_INFO("Message sent to premium user: " + session->id()); 120 | } 121 | 122 | ctx.reply({}); 123 | }); 124 | 125 | app.run(9010); 126 | 127 | std::cin.get(); 128 | return 0; 129 | } 130 | -------------------------------------------------------------------------------- /example_server/utils/parser.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace msgpack { 10 | MSGPACK_API_VERSION_NAMESPACE(v1) { 11 | template 12 | inline packer& operator<<(packer& o, const std::string& v) { 13 | o.pack_str(v.size()); 14 | o.pack_str_body(v.data(), v.size()); 15 | return o; 16 | } 17 | 18 | template 19 | inline packer& operator<<(packer& o, const char* v) { 20 | o.pack_str(strlen(v)); 21 | o.pack_str_body(v, strlen(v)); 22 | return o; 23 | } 24 | 25 | // Sabit uzunluklu karakter dizileri için özel tanımlamalar 26 | template 27 | inline packer& operator<<(packer& o, const char (&v)[N]) { 28 | o.pack_str(N-1); // N-1 çünkü son karakter null terminator 29 | o.pack_str_body(v, N-1); 30 | return o; 31 | } 32 | 33 | // object sınıfı için özel tanımlamalar 34 | template 35 | inline void operator<<(object& o, const char (&v)[N]) { 36 | o.type = type::STR; 37 | o.via.str.size = N-1; 38 | o.via.str.ptr = v; 39 | } 40 | } 41 | 42 | MSGPACK_API_VERSION_NAMESPACE(v2) { 43 | template 44 | inline packer& operator<<(packer& o, const std::string& v) { 45 | o.pack_str(v.size()); 46 | o.pack_str_body(v.data(), v.size()); 47 | return o; 48 | } 49 | 50 | template 51 | inline packer& operator<<(packer& o, const char* v) { 52 | o.pack_str(strlen(v)); 53 | o.pack_str_body(v, strlen(v)); 54 | return o; 55 | } 56 | 57 | // Sabit uzunluklu karakter dizileri için özel tanımlamalar 58 | template 59 | inline packer& operator<<(packer& o, const char (&v)[N]) { 60 | o.pack_str(N-1); // N-1 çünkü son karakter null terminator 61 | o.pack_str_body(v, N-1); 62 | return o; 63 | } 64 | 65 | // object sınıfı için özel tanımlamalar 66 | template 67 | inline void operator<<(object& o, const char (&v)[N]) { 68 | o.type = type::STR; 69 | o.via.str.size = N-1; 70 | o.via.str.ptr = v; 71 | } 72 | } 73 | } 74 | 75 | // Recursive olarak MsgPack objesini JSON'a dönüştüren yardımcı fonksiyon 76 | inline nlohmann::json convertMsgPackToJson(const msgpack::object& obj) { 77 | switch (obj.type) { 78 | case msgpack::type::STR: { 79 | std::string str; 80 | obj.convert(str); 81 | return str; 82 | } 83 | case msgpack::type::POSITIVE_INTEGER: 84 | case msgpack::type::NEGATIVE_INTEGER: { 85 | int64_t num; 86 | obj.convert(num); 87 | return num; 88 | } 89 | case msgpack::type::FLOAT: { 90 | double num; 91 | obj.convert(num); 92 | return num; 93 | } 94 | case msgpack::type::BOOLEAN: { 95 | bool b; 96 | obj.convert(b); 97 | return b; 98 | } 99 | case msgpack::type::ARRAY: { 100 | nlohmann::json::array_t arr; 101 | for (size_t i = 0; i < obj.via.array.size; i++) { 102 | arr.push_back(convertMsgPackToJson(obj.via.array.ptr[i])); 103 | } 104 | return arr; 105 | } 106 | case msgpack::type::MAP: { 107 | nlohmann::json::object_t map; 108 | for (size_t i = 0; i < obj.via.map.size; i++) { 109 | const auto& pair = obj.via.map.ptr[i]; 110 | std::string key; 111 | pair.key.convert(key); 112 | map[key] = convertMsgPackToJson(pair.val); 113 | } 114 | return map; 115 | } 116 | default: 117 | return nlohmann::json::object(); 118 | } 119 | } 120 | 121 | // MsgPack payload'ı map olarak parse eden yardımcı fonksiyon 122 | inline nlohmann::json parseMsgPackPayload(const std::vector& req) { 123 | try { 124 | // Önce msgpack'i parse et 125 | msgpack::object_handle oh = msgpack::unpack( 126 | reinterpret_cast(req.data()), 127 | req.size() 128 | ); 129 | 130 | // Debug için 131 | LOG_DEBUG("Raw payload size: " + std::to_string(req.size())); 132 | 133 | // Objeyi al 134 | msgpack::object obj = oh.get(); 135 | 136 | // Recursive olarak objeyi JSON'a dönüştür 137 | return convertMsgPackToJson(obj); 138 | } 139 | catch (const std::exception& ex) { 140 | LOG_ERROR("Parse error: " + std::string(ex.what())); 141 | return nlohmann::json::object(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /test_server/middleware_test.cpp: -------------------------------------------------------------------------------- 1 | #include "binaryrpc/core/rpc/rpc_context.hpp" 2 | #include "binaryrpc/core/protocol/simple_text_protocol.hpp" 3 | #include "binaryrpc/core/util/logger.hpp" 4 | #include 5 | #include "binaryrpc/core/app.hpp" 6 | #include "binaryrpc/transports/websocket/websocket_transport.hpp" 7 | #include "binaryrpc/core/util/qos.hpp" 8 | #include 9 | 10 | using namespace binaryrpc; 11 | 12 | int main() { 13 | Logger::inst().setLevel(LogLevel::Debug); 14 | App& app = App::getInstance(); 15 | auto ws = std::make_unique(app.getSessionManager(), /*idleTimeoutSec=*/60); 16 | 17 | ReliableOptions opts; 18 | opts.level = QoSLevel::None; 19 | 20 | ws->setReliable(opts); 21 | 22 | 23 | app.setTransport(std::move(ws)); 24 | 25 | app.use([](Session& s, const std::string&, std::vector&, NextFunc next) { 26 | std::cout << "[MW1] çalıştı\n"; 27 | s.set("step1", true); 28 | next(); 29 | }); 30 | 31 | app.use([](Session& s, const std::string&, std::vector&, NextFunc next) { 32 | std::cout << "[MW2] çalıştı\n"; 33 | bool step1 = s.get("step1"); 34 | if (!step1) throw std::runtime_error("Middleware1 atlanmış!"); 35 | s.set("step2", true); 36 | next(); 37 | }); 38 | 39 | app.use([](Session& s, const std::string&, std::vector&, NextFunc next) { 40 | std::cout << "[Global MW] çalıştı\n"; 41 | s.set("global", true); 42 | next(); 43 | }); 44 | 45 | app.useFor("login", [](Session& s, const std::string&, std::vector&, NextFunc next) { 46 | std::cout << "[MW for login] çalıştı\n"; 47 | s.set("loginMW", true); 48 | next(); 49 | }); 50 | 51 | app.useForMulti({"login", "test.middleware"}, [](Session& s, const std::string&, std::vector&, NextFunc next) { 52 | std::cout << "[MW for login & test.middleware] çalıştı\n"; 53 | s.set("multiMW", true); 54 | next(); 55 | }); 56 | 57 | 58 | app.registerRPC("login", [&](const std::vector& payload, RpcContext& ctx) { 59 | std::cout << "[Handler] login çağrıldı\n"; 60 | bool global = ctx.session().get("global"); 61 | bool loginMW = ctx.session().get("loginMW"); 62 | bool multiMW = ctx.session().get("multiMW"); 63 | 64 | if (!global || !loginMW || !multiMW) 65 | throw std::runtime_error("Login middleware zinciri incomplete!"); 66 | 67 | std::string response = "login all middlewares passed!"; 68 | std::vector payload_vec(response.begin(), response.end()); 69 | ctx.reply(app.getProtocol()->serialize("login", payload_vec)); 70 | }); 71 | 72 | app.registerRPC("test.middleware", [&](const std::vector& payload, RpcContext& ctx) { 73 | std::cout << "[Handler] test.middleware çağrıldı\n"; 74 | bool global = ctx.session().get("global"); 75 | bool loginMW = ctx.session().get("loginMW"); // burada çalışmamalı 76 | bool multiMW = ctx.session().get("multiMW"); 77 | 78 | if (!global || loginMW || !multiMW) // loginMW çalışmamalı! 79 | throw std::runtime_error("test.middleware middleware zinciri incomplete!"); 80 | 81 | std::string response = "test.middleware all middlewares passed!"; 82 | std::vector payload_vec(response.begin(), response.end()); 83 | ctx.reply(app.getProtocol()->serialize("test.middleware", payload_vec)); 84 | }); 85 | 86 | 87 | // Bu middleware intentionally next() çağırmamalı (zinciri durduracak) 88 | app.useFor("stuck.method", [](Session& s, const std::string&, std::vector&, NextFunc next) { 89 | std::cout << "[MW stuck] çalıştı ama next() yok!\n"; 90 | // next() çağırmıyoruz → zincir burada durmalı 91 | }); 92 | 93 | // Bu middleware exception atıyor 94 | app.useFor("throw.method", [](Session& s, const std::string&, std::vector&, NextFunc next) { 95 | std::cout << "[MW throw] çalıştı ve exception fırlatıyor!\n"; 96 | throw std::runtime_error("MW throw exception!"); 97 | }); 98 | 99 | app.registerRPC("stuck.method", [&](const std::vector& payload, RpcContext& ctx) { 100 | std::cout << "[Handler] stuck.method çağrıldı (BU ÇAĞRILMAMALI)\n"; 101 | std::string response = "should not reach here!"; 102 | std::vector payload_vec(response.begin(), response.end()); 103 | ctx.reply(app.getProtocol()->serialize("stuck.method", payload_vec)); 104 | }); 105 | 106 | app.registerRPC("throw.method", [&](const std::vector& payload, RpcContext& ctx) { 107 | std::cout << "[Handler] throw.method çağrıldı (BU ÇAĞRILMAMALI)\n"; 108 | std::string response = "should not reach here!"; 109 | std::vector payload_vec(response.begin(), response.end()); 110 | ctx.reply(app.getProtocol()->serialize("throw.method", payload_vec)); 111 | }); 112 | 113 | 114 | 115 | 116 | 117 | 118 | app.run(9000); 119 | std::cout << "Server listening on port 9000\n"; 120 | 121 | std::cin.get(); 122 | return 1; 123 | } 124 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | efecanerdem@proton.me . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /include/binaryrpc/core/session/session.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file session.hpp 3 | * @brief Session class and connection state for BinaryRPC. 4 | * 5 | * Defines the Session class, which holds QoS buffers, user data, and connection state 6 | * for each client identity (ip + clientId + deviceId) in the BinaryRPC framework. 7 | * 8 | * @author Efecan 9 | * @date 2025 10 | */ 11 | #pragma once 12 | 13 | #include "binaryrpc/core/auth/ClientIdentity.hpp" 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | // If you don't want to include uWebSockets directly, use forward declaration: 22 | namespace uWS { template class WebSocket; } 23 | struct PerSocketData; 24 | 25 | namespace binaryrpc { 26 | 27 | struct ConnState; 28 | 29 | /** 30 | * @enum ConnectionState 31 | * @brief Connection state for a session. 32 | * 33 | * - ONLINE: Client is actively connected 34 | * - OFFLINE: Client connection is dropped 35 | */ 36 | enum class ConnectionState { 37 | ONLINE, ///< Client is actively connected 38 | OFFLINE ///< Client connection is dropped 39 | }; 40 | 41 | /** 42 | * @class Session 43 | * @brief Holds QoS buffers, user data, and connection state for a client identity. 44 | * 45 | * Manages the lifetime of a client session, including connection state, QoS buffers, 46 | * key-value storage, and duplicate filtering for reliable message delivery. 47 | */ 48 | class Session { 49 | public: 50 | /** 51 | * @brief Construct a new Session with a client identity and legacy session ID. 52 | * @param ident Client identity (clientId, deviceId, sessionToken) 53 | * @param legacySid Legacy session ID (for backward compatibility) 54 | */ 55 | Session(ClientIdentity ident, std::string legacySid); 56 | 57 | /** 58 | * @brief Destructor for the Session class. 59 | */ 60 | ~Session(); 61 | 62 | /** 63 | * @brief Get the legacy session ID. 64 | * @return Reference to the legacy session ID string 65 | */ 66 | const std::string& id() const; 67 | /** 68 | * @brief Get the client identity. 69 | * @return Reference to the ClientIdentity 70 | */ 71 | const ClientIdentity& identity() const; 72 | 73 | /** 74 | * @brief Rebind the session to a new WebSocket connection. 75 | * 76 | * Resets the duplicate filter on new connection. 77 | * @param ws Pointer to the new WebSocket connection 78 | */ 79 | using WS = uWS::WebSocket; 80 | void rebind(WS* ws); 81 | /** 82 | * @brief Get the current live WebSocket connection. 83 | * @return Pointer to the live WebSocket connection (or nullptr if offline) 84 | */ 85 | WS* liveWs() const; 86 | 87 | /** 88 | * @brief Shared pointer to QoS state (used by transport layer). 89 | */ 90 | std::shared_ptr qosState; 91 | 92 | /** 93 | * @brief Expiry time in milliseconds (0 means not expired). 94 | */ 95 | uint64_t expiryMs = 0; 96 | 97 | /** 98 | * @brief Set the legacy connection pointer (deprecated). 99 | * @param conn Pointer to the legacy connection 100 | * @deprecated Use rebind() instead. 101 | */ 102 | [[deprecated("use rebind()")]] 103 | void setConnection(void* conn); 104 | /** 105 | * @brief Get the legacy connection pointer (deprecated). 106 | * @return Pointer to the legacy connection 107 | * @deprecated Use liveWs() instead. 108 | */ 109 | [[deprecated("use liveWs()")]] 110 | void* getConnection() const; 111 | 112 | /** 113 | * @brief Set a key-value pair in the session's data store. 114 | * @tparam T Type of the value 115 | * @param key Key string 116 | * @param value Value to set 117 | */ 118 | template 119 | void set(const std::string& key, T value) { 120 | set_any(key, std::any(std::move(value))); 121 | } 122 | /** 123 | * @brief Get a value from the session's data store by key. 124 | * @tparam T Type of the value 125 | * @param key Key string 126 | * @return Value of type T if present, default-constructed T otherwise 127 | */ 128 | template 129 | T get(const std::string& key) const { 130 | std::any val = get_any(key); 131 | if (val.has_value()) { 132 | try { 133 | return std::any_cast(val); 134 | } 135 | catch (const std::bad_any_cast&) { 136 | return T{}; 137 | } 138 | } 139 | return T{}; 140 | } 141 | 142 | /** 143 | * @brief Check for duplicate QoS-1 messages using the duplicate filter. 144 | * @param rpcPayload Payload of the RPC message 145 | * @param ttl Time-to-live for duplicate detection 146 | * @return True if the message is not a duplicate, false otherwise 147 | */ 148 | bool acceptDuplicate(const std::vector& rpcPayload, std::chrono::milliseconds ttl); 149 | 150 | /** 151 | * @brief Current connection state (ONLINE or OFFLINE). 152 | */ 153 | ConnectionState connectionState = ConnectionState::OFFLINE; 154 | private: 155 | void set_any(const std::string& key, std::any value); 156 | std::any get_any(const std::string& key) const; 157 | 158 | // PIMPL idiom to hide internal implementation details 159 | struct Impl; 160 | std::unique_ptr pImpl_; 161 | }; 162 | 163 | } 164 | -------------------------------------------------------------------------------- /include/binaryrpc/transports/websocket/websocket_transport.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file websocket_transport.hpp 3 | * @brief WebSocket transport layer for BinaryRPC. 4 | */ 5 | #pragma once 6 | 7 | #include "binaryrpc/core/interfaces/itransport.hpp" 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace binaryrpc { 14 | // Forward declarations for opaque types used in the public API 15 | class SessionManager; 16 | class IHandshakeInspector; 17 | struct ReliableOptions; 18 | class Session; 19 | 20 | enum FrameType : uint8_t { 21 | FRAME_DATA = 0x00, 22 | FRAME_ACK = 0x01, 23 | FRAME_PREPARE = 0x02, 24 | FRAME_PREPARE_ACK = 0x03, 25 | FRAME_COMMIT = 0x04, 26 | FRAME_COMPLETE = 0x05 27 | }; 28 | 29 | /** 30 | * @class WebSocketTransport 31 | * @brief ITransport implementation using uWebSockets. 32 | * 33 | * Handles session management, Quality of Service (QoS), message delivery, and connection lifecycle for BinaryRPC over WebSocket. 34 | * Supports pluggable handshake inspection, offline message queuing, and advanced reliability options. 35 | */ 36 | class WebSocketTransport : public ITransport { 37 | public: 38 | /** 39 | * @brief Constructs a WebSocketTransport instance. 40 | * @param sm Reference to the SessionManager. 41 | * @param idleTimeoutSec Idle timeout in seconds (default: 45). 42 | * @param maxPayloadBytes Maximum payload size in bytes (default: 10 MB). 43 | */ 44 | WebSocketTransport(SessionManager& sm, uint16_t idleTimeoutSec = 45, uint32_t maxPayloadBytes = 10 * 1024 * 1024); 45 | 46 | /** 47 | * @brief Destructor. 48 | */ 49 | ~WebSocketTransport() override; 50 | 51 | /** 52 | * @brief Starts the WebSocket server and begins listening on the specified port. 53 | * @param port The port number to listen on. 54 | */ 55 | void start(uint16_t port) override; 56 | 57 | /** 58 | * @brief Stops the WebSocket server and releases all resources. 59 | */ 60 | void stop() override; 61 | 62 | /** 63 | * @brief Sends data to all connected clients. 64 | * @param data The data to send. 65 | */ 66 | void send(const std::vector& data) override; 67 | 68 | /** 69 | * @brief Sends data to a specific client connection. 70 | * @param connection The client connection pointer. 71 | * @param data The data to send. 72 | */ 73 | void sendToClient(void* connection, const std::vector& data) override; 74 | 75 | /** 76 | * @brief Sends data to a specific session. 77 | * @param session The target session. 78 | * @param data The data to send. 79 | */ 80 | void sendToSession(std::shared_ptr session, const std::vector& data) override; 81 | 82 | /** 83 | * @brief Disconnects a specific client connection. 84 | * @param connection The client connection pointer. 85 | */ 86 | void disconnectClient(void* connection) override; 87 | 88 | /** 89 | * @brief Configures reliability and QoS options for message delivery. 90 | * @param options The reliability options to use. 91 | */ 92 | void setReliable(const ReliableOptions& options) override; 93 | 94 | /** 95 | * @brief Sets the callback to be invoked when data is received from a client. 96 | * @param cb The data callback function. 97 | */ 98 | void setCallback(DataCallback cb) override; 99 | 100 | /** 101 | * @brief Sets the callback to be invoked when a session is registered. 102 | * @param cb The session registration callback function. 103 | */ 104 | void setSessionRegisterCallback(SessionRegisterCallback cb) override; 105 | 106 | /** 107 | * @brief Sets the callback to be invoked when a client disconnects. 108 | * @param cb The disconnect callback function. 109 | */ 110 | void setDisconnectCallback(DisconnectCallback cb) override; 111 | 112 | /** 113 | * @brief Sets the handshake inspector for custom authentication or validation during WebSocket upgrade. 114 | * @param inspector The handshake inspector instance. 115 | */ 116 | void setHandshakeInspector(std::shared_ptr inspector); 117 | 118 | #ifdef BINARYRPC_TEST 119 | /** 120 | * @brief Simulates receiving a raw frame from a client (for testing purposes). 121 | * @param frame The raw frame data. 122 | */ 123 | void onRawFrameFromClient(const std::vector& frame); 124 | 125 | /** 126 | * @brief Sets a send interceptor callback for testing outgoing frames. 127 | * @param interceptor The interceptor function. 128 | */ 129 | void setSendInterceptor(std::function&)> interceptor); 130 | 131 | /** 132 | * @brief Creates a test frame for unit testing. 133 | * @param type The frame type. 134 | * @param id The message ID. 135 | * @param payload The payload data. 136 | * @return The constructed frame as a byte vector. 137 | */ 138 | static std::vector test_makeFrame(FrameType type, uint64_t id, const std::vector& payload); 139 | 140 | /** 141 | * @brief Registers a message ID as seen for duplicate detection (for testing). 142 | * @param connStatePtr Pointer to the connection state. 143 | * @param id The message ID. 144 | * @param ttlMs Time-to-live in milliseconds. 145 | * @return True if the ID was newly registered, false if it was already seen. 146 | */ 147 | static bool test_registerSeen(void* connStatePtr, uint64_t id, uint32_t ttlMs); 148 | #endif 149 | private: 150 | /** 151 | * @brief Private implementation (PIMPL) to hide internal details and reduce compile-time dependencies. 152 | */ 153 | class Impl; 154 | std::unique_ptr pImpl_; 155 | std::shared_ptr getInspector(); 156 | #ifdef BINARYRPC_TEST 157 | void handleFrame(const uint8_t* data, std::size_t length); 158 | std::function&)> sendInterceptor_; 159 | #endif 160 | }; 161 | 162 | } 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to BinaryRPC 2 | 3 | Thank you for your interest in contributing to BinaryRPC! This document provides guidelines and information for contributors. 4 | 5 | ## 🚀 Quick Start 6 | 7 | 1. **Fork** the repository 8 | 2. **Clone** your fork locally 9 | 3. **Create** a feature branch 10 | 4. **Make** your changes 11 | 5. **Test** your changes thoroughly 12 | 6. **Submit** a pull request 13 | 14 | ## 📋 Development Setup 15 | 16 | ### Prerequisites 17 | 18 | - **C++20** compatible compiler (GCC 10+, Clang 12+, MSVC 2019+) 19 | - **CMake** 3.16 or higher 20 | - **Python 3.8+** (for integration tests) 21 | 22 | ### Building from Source 23 | 24 | ```bash 25 | # Clone the repository 26 | git clone https://github.com/your-username/binaryrpc.git --recurse-submodules 27 | cd binaryrpc 28 | 29 | cmake --preset development 30 | cmake --build build 31 | 32 | # Run tests 33 | ctest --output-on-failure 34 | ``` 35 | 36 | ## 🧪 Testing 37 | 38 | ### Running Tests 39 | 40 | ```bash 41 | # Unit tests 42 | cd build 43 | ctest -R unit_tests 44 | 45 | # Integration tests 46 | cd tests/integration_tests_py 47 | python -m pytest 48 | 49 | # Memory leak tests 50 | cd tests/Memory_leak_stress_test_py 51 | python memory_leak_test.py 52 | ``` 53 | 54 | ### Writing Tests 55 | 56 | - **Unit tests**: Use Catch2 framework in `tests/` directory 57 | - **Integration tests**: Use Python pytest in `tests/integration_tests_py/` 58 | - **Performance tests**: Add benchmarks in `tests/benchmarks/` 59 | 60 | ## 📝 Code Style 61 | 62 | ### C++ Guidelines 63 | 64 | - Follow **Google C++ Style Guide** with exceptions: 65 | - Use **snake_case** for functions and variables 66 | - Use **PascalCase** for classes and structs 67 | - Use **UPPER_CASE** for constants and macros 68 | - Maximum line length: **120 characters** 69 | 70 | ### Header Organization 71 | 72 | ```cpp 73 | // 1. Include guards 74 | #pragma once 75 | 76 | // 2. System includes 77 | #include 78 | #include 79 | 80 | // 3. Third-party includes 81 | #include 82 | 83 | // 4. Project includes 84 | #include "binaryrpc/core/interfaces/itransport.hpp" 85 | 86 | // 5. Namespace 87 | namespace binaryrpc { 88 | // Your code here 89 | } 90 | ``` 91 | 92 | ### Documentation 93 | 94 | - Use **Doxygen** comments for public APIs 95 | - Include **examples** in documentation 96 | - Document **exceptions** and error conditions 97 | 98 | ## 🔧 Architecture Guidelines 99 | 100 | ### Adding New Features 101 | 102 | 1. **Interface First**: Define interfaces in `include/binaryrpc/core/interfaces/` 103 | 2. **Implementation**: Add implementation in `src/core/` 104 | 3. **Tests**: Write comprehensive tests 105 | 4. **Documentation**: Update README and add examples 106 | 107 | ### Plugin Development 108 | 109 | ```cpp 110 | // Example plugin structure 111 | class MyPlugin : public IPlugin { 112 | public: 113 | void initialize() override; 114 | const char* name() const override { return "MyPlugin"; } 115 | 116 | private: 117 | // Plugin-specific members 118 | }; 119 | ``` 120 | 121 | ### Transport Implementation 122 | 123 | ```cpp 124 | // Example transport structure 125 | class MyTransport : public ITransport { 126 | public: 127 | void start(uint16_t port) override; 128 | void stop() override; 129 | void send(const std::vector& data) override; 130 | // ... other interface methods 131 | }; 132 | ``` 133 | 134 | ## 🐛 Bug Reports 135 | 136 | ### Before Submitting 137 | 138 | 1. **Search** existing issues 139 | 2. **Reproduce** the bug consistently 140 | 3. **Test** with latest master branch 141 | 4. **Include** minimal reproduction code 142 | 143 | ### Bug Report Template 144 | 145 | ```markdown 146 | **Description** 147 | Brief description of the issue 148 | 149 | **Steps to Reproduce** 150 | 1. Step 1 151 | 2. Step 2 152 | 3. Step 3 153 | 154 | **Expected Behavior** 155 | What should happen 156 | 157 | **Actual Behavior** 158 | What actually happens 159 | 160 | **Environment** 161 | - OS: [e.g., Windows 10, Ubuntu 20.04] 162 | - Compiler: [e.g., GCC 11.2, MSVC 2019] 163 | - BinaryRPC Version: [e.g., commit hash] 164 | 165 | **Additional Context** 166 | Any other relevant information 167 | ``` 168 | 169 | ## 🔄 Pull Request Process 170 | 171 | ### Before Submitting PR 172 | 173 | 1. **Rebase** on latest master 174 | 2. **Run** all tests locally 175 | 3. **Update** documentation if needed 176 | 4. **Add** tests for new features 177 | 5. **Check** code style compliance 178 | 179 | ### PR Template 180 | 181 | ```markdown 182 | **Description** 183 | Brief description of changes 184 | 185 | **Type of Change** 186 | - [ ] Bug fix 187 | - [ ] New feature 188 | - [ ] Breaking change 189 | - [ ] Documentation update 190 | 191 | **Testing** 192 | - [ ] Unit tests pass 193 | - [ ] Integration tests pass 194 | - [ ] Manual testing completed 195 | 196 | **Breaking Changes** 197 | List any breaking changes and migration steps 198 | 199 | **Additional Notes** 200 | Any additional information 201 | ``` 202 | 203 | ## 📚 Documentation 204 | 205 | ### API Documentation 206 | 207 | - Use **Doxygen** for C++ API documentation 208 | - Include **usage examples** in comments 209 | - Document **thread safety** guarantees 210 | - Specify **exception safety** levels 211 | 212 | ### User Documentation 213 | 214 | - **README.md**: Quick start and overview 215 | - **docs/**: Detailed documentation 216 | - **examples/**: Working code examples 217 | - **CHANGELOG.md**: Version history 218 | 219 | ## 🏷️ Versioning 220 | 221 | BinaryRPC follows **Semantic Versioning** (SemVer): 222 | 223 | - **MAJOR**: Breaking changes 224 | - **MINOR**: New features (backward compatible) 225 | - **PATCH**: Bug fixes (backward compatible) 226 | 227 | ## 🤝 Community 228 | 229 | ### Communication 230 | 231 | - **Issues**: Use GitHub issues for bugs and feature requests 232 | - **Discussions**: Use GitHub Discussions for questions and ideas 233 | - **Code Review**: All PRs require review from maintainers 234 | 235 | ### Code of Conduct 236 | 237 | - Be **respectful** and inclusive 238 | - Focus on **technical merit** 239 | - Help **new contributors** 240 | - Follow **project guidelines** 241 | 242 | ## 📄 License 243 | 244 | By contributing to BinaryRPC, you agree that your contributions will be licensed under the MIT License. 245 | 246 | --- 247 | 248 | Thank you for contributing to BinaryRPC! 🚀 249 | --------------------------------------------------------------------------------