├── .clangd ├── .gitignore ├── .projectile ├── CMakeLists.txt ├── FUNDING.yml ├── LICENSE ├── README.md ├── Rakefile ├── flake.lock ├── flake.nix ├── jcon-cpp.pro └── src ├── .dir-locals.el ├── CMakeLists.txt ├── example.pro ├── example_service.cpp ├── example_service.h ├── include.pri ├── jcon ├── CMakeLists.txt ├── jcon.h ├── jcon.pro ├── jcon_assert.h ├── json_rpc_client.cpp ├── json_rpc_client.h ├── json_rpc_debug_logger.cpp ├── json_rpc_debug_logger.h ├── json_rpc_endpoint.cpp ├── json_rpc_endpoint.h ├── json_rpc_error.cpp ├── json_rpc_error.h ├── json_rpc_file_logger.cpp ├── json_rpc_file_logger.h ├── json_rpc_logger.cpp ├── json_rpc_logger.h ├── json_rpc_request.cpp ├── json_rpc_request.h ├── json_rpc_result.h ├── json_rpc_server.cpp ├── json_rpc_server.h ├── json_rpc_socket.h ├── json_rpc_success.cpp ├── json_rpc_success.h ├── json_rpc_tcp_client.cpp ├── json_rpc_tcp_client.h ├── json_rpc_tcp_server.cpp ├── json_rpc_tcp_server.h ├── json_rpc_tcp_socket.cpp ├── json_rpc_tcp_socket.h ├── json_rpc_websocket.cpp ├── json_rpc_websocket.h ├── json_rpc_websocket_client.cpp ├── json_rpc_websocket_client.h ├── json_rpc_websocket_server.cpp ├── json_rpc_websocket_server.h ├── string_util.cpp └── string_util.h ├── main.cpp ├── notification_service.cpp ├── notification_service.h ├── other_service.cpp └── other_service.h /.clangd: -------------------------------------------------------------------------------- 1 | CompileFlags: 2 | CompilationDatabase: _build 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | GPATH 2 | GRTAGS 3 | GSYMS 4 | GTAGS 5 | \#* 6 | .\#* 7 | *.sw[opn] 8 | 9 | _build/ 10 | build/ 11 | .idea 12 | cmake-build* 13 | *.user 14 | 15 | 16 | # Compiled Object files 17 | *.slo 18 | *.lo 19 | *.o 20 | *.obj 21 | 22 | # Precompiled Headers 23 | *.gch 24 | *.pch 25 | 26 | # Compiled Dynamic libraries 27 | *.so 28 | *.dylib 29 | *.dll 30 | 31 | # Fortran module files 32 | *.mod 33 | 34 | # Compiled Static libraries 35 | *.lai 36 | *.la 37 | *.a 38 | *.lib 39 | 40 | # Executables 41 | *.exe 42 | *.out 43 | *.app 44 | 45 | /.cache/ 46 | /.direnv/ 47 | /.envrc 48 | /compile_commands.json 49 | /rtags* 50 | -------------------------------------------------------------------------------- /.projectile: -------------------------------------------------------------------------------- 1 | -/_build/ 2 | -/build/ 3 | -/rtags-* -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5...3.27.8) 2 | cmake_policy(SET CMP0043 NEW) 3 | cmake_policy(SET CMP0053 NEW) 4 | 5 | set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_LIST_DIR}/cmake") 6 | 7 | project(jcon-cpp) 8 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 9 | 10 | # add_definitions(-DBOOST_RESULT_OF_USE_DECLTYPE) 11 | 12 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 13 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 14 | 15 | if(APPLE) 16 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) 17 | else() 18 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 19 | endif() 20 | 21 | if(MINGW) 22 | # -enable-stdcall-fixup is to avoid FMOD linker warning 23 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-local-typedefs -Wl,--enable-stdcall-fixup -Wno-unused-but-set-variable -Wno-unused-variable") 24 | endif() 25 | 26 | if(APPLE OR UNIX) 27 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror -Wno-deprecated-declarations -Wno-conversion -Wno-sign-compare -Wno-strict-aliasing") 28 | endif() 29 | 30 | if(APPLE) 31 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-sign-conversion -Wno-shorten-64-to-32 -Wno-unused-private-field") 32 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pagezero_size 10000 -image_base 100000000") 33 | endif() 34 | 35 | if(UNIX) 36 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -Wno-unused-local-typedefs -Wno-unused-variable") 37 | endif() 38 | 39 | set(VERSION_MAJOR "0") 40 | set(VERSION_MINOR "0") 41 | set(VERSION_PATCH "1") 42 | set(VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}") 43 | 44 | include(CTest) 45 | 46 | if(APPLE) 47 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") 48 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -stdlib=libc++") 49 | endif() 50 | 51 | add_subdirectory(src) 52 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: joncol 2 | patreon: joncol 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jonas Collberg 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. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## jcon-cpp 2 | 3 | jcon-cpp is a portable C++ JSON RPC 2.0 library that depends on Qt 6. 4 | 5 | ## Introduction 6 | 7 | If you're using **C++ 17** and **Qt**, and want to create a **JSON RPC 2.0** 8 | client or server, using either **TCP** or **WebSockets** as underlying transport 9 | layer, then **jcon-cpp** might prove useful. 10 | 11 | In all of the following, replace "Tcp" with "WebSocket" to change the transport 12 | method. 13 | 14 | Platforms supported are: Linux, Windows, Mac OS, and Android. 15 | 16 | ## Building the Library 17 | 18 | 1. Get the source. 19 | 2. Create a `build` directory in the top directory. 20 | 3. Change to the `build` directory: `cd build`. 21 | 4. `cmake ..` 22 | 5. `make -j4` 23 | 24 | The build depends on the build directive `CMAKE_PREFIX_PATH` to find the 25 | required Qt dependencies, so if your CMake doesn't pick up on where to find Qt, 26 | try adding `cmake -DCMAKE_PREFIX_PATH= ..` in step 4 above. 27 | 28 | ## Include Files 29 | 30 | Depending on if you're implementing a server or a client and whether you're 31 | using TCP or WebSockets, you need to include some of these files: 32 | ```c++ 33 | #include 34 | #include 35 | #include 36 | #include 37 | ``` 38 | 39 | ## Example Code 40 | 41 | There's example code of both a server and a client in the file `src/main.cpp`. 42 | 43 | ## Creating a Server 44 | 45 | ```c++ 46 | auto rpc_server = new jcon::JsonRpcTcpServer(parent); 47 | 48 | // optionally, enable sending unsolicited notifications 49 | // PS: not part of JSON-RPC standard 50 | rpc_server->enableSendNotification(true); 51 | ``` 52 | 53 | Create a service (a collection of invokable methods): 54 | 55 | 1. Make your service class inherit `QObject` 56 | 2. Make sure your service method is accessible by the Qt meta object system 57 | (either by using the `Q_INVOKABLE` macro or by putting the method in a 58 | `public slots:` section). 59 | 3. (optionally) Provide unsolicited notifications by emitting `sendUnsolicitedNotification`. 60 | Requires calling `enableSendNotification(true)` on the server. 61 | 62 | For instance: 63 | 64 | ```c++ 65 | class ExampleService : public QObject 66 | { 67 | Q_OBJECT 68 | 69 | public: 70 | ExampleService(QObject* parent = nullptr); 71 | virtual ~ExampleService(); 72 | 73 | Q_INVOKABLE int getRandomInt(int limit); 74 | 75 | signals: 76 | void sendUnsolicitedNotification(const QString&, const QVariant&); 77 | }; 78 | ``` 79 | 80 | Parameters and return types of methods are automatically matched against the JSON RPC call, 81 | using the Qt Meta object system, and you can use lists (`QVariantList`) and 82 | dictionary type objects (`QVariantMap`) in addition to the standard primitive 83 | types such as `QString`, `bool`, `int`, `float`, etc. 84 | 85 | However, `sendUnsolicitedNotification` signal must be declared exactly as shown in the example. 86 | The first argument is the notification `key`, and the second argument is the actual unsolicited notification. 87 | 88 | Register your service with: 89 | 90 | ```c++ 91 | rpc_server->registerServices({ new ExampleService() }); 92 | ``` 93 | 94 | Note that (as of 2018-11-21) there is also a variant of `registerServices` that 95 | takes a `QMap`, where the keys are the services, and the 96 | values are strings that will need to be used as prefixes when calling the 97 | corresponding RPC methods. This can be used as a simple namespace mechanism. 98 | Please refer to the example code in `src/main.cpp`. 99 | 100 | The server will take over ownership of the service object, and the memory will 101 | be freed at shutdown. Note that the `registerServices` method changed its 102 | signature 2016-10-20, from being a variadic template expecting `unique_ptrs`, to 103 | taking a `QObjectList`. 104 | 105 | Finally, start listening for client connections by: 106 | 107 | ```c++ 108 | rpc_server->listen(6001); 109 | ``` 110 | 111 | Specify whatever port you want to use. 112 | 113 | 114 | ## Creating a Client 115 | 116 | Simple: 117 | 118 | ```c++ 119 | auto rpc_client = new jcon::JsonRpcTcpClient(parent); 120 | rpc_client->connectToServer("127.0.0.1", 6001); 121 | 122 | // optionally, enable receiving unsolicited notifications 123 | // PS: not part of JSON-RPC standard 124 | rpc_client->enableSendNotification(true); 125 | ``` 126 | 127 | (No need to use a smart pointer here, since the destructor will be called as 128 | long as a non-null parent `QObject` is provided.) 129 | 130 | 131 | ### Invoking a Remote Method Asynchronously 132 | 133 | ```c++ 134 | auto req = rpc_client->callAsync("getRandomInt", 10); 135 | ``` 136 | 137 | The returned object (of type `std::shared_ptr`) can be used to 138 | set up a callback, that is invoked when the result of the JSON RPC call is 139 | ready: 140 | 141 | ```c++ 142 | req->connect(req.get(), &jcon::JsonRpcRequest::result, 143 | [](const QVariant& result) { 144 | qDebug() << "result of RPC call:" << result; 145 | qApp->exit(); 146 | }); 147 | ``` 148 | 149 | To handle errors: 150 | 151 | ```c++ 152 | req->connect(req.get(), &jcon::JsonRpcRequest::error, 153 | [](int code, const QString& message, const QVariant& data) { 154 | qDebug() << "RPC error: " << message << " (" << code << ")"; 155 | qApp->exit(); 156 | }); 157 | ``` 158 | 159 | 160 | ### Invoking a Remote Method Synchronously 161 | 162 | ```c++ 163 | auto result = rpc_client->call("getRandomInt", 10); 164 | 165 | if (result->isSuccess()) { 166 | QVariant res = result->result(); 167 | } else { 168 | QString err_str = result->toString(); 169 | } 170 | ``` 171 | 172 | ### Handling unsolicited notification 173 | 174 | ```c++ 175 | QObject::connect(rpc_client, &jcon::JsonRpcClient::notificationReceived, 176 | &app, [](const QString& key, const QVariant& value){ 177 | qDebug() << "Received notification:" 178 | << "Key:" << key 179 | << "Value:" << value; 180 | }); 181 | ``` 182 | 183 | ### Expanding a List of Arguments 184 | 185 | If you want to expand a list of arguments (instead of passing the list as a 186 | single argument), use `callExpandArgs` and `callAsyncExpandArgs`. 187 | 188 | 189 | ### Named Parameters 190 | 191 | If you want to call a function using named parameters, use `callNamedParams` and 192 | `callAsyncNamedParams`. This will pass the parameters as a `QVariantMap` instead 193 | of converting it to a `QVariantList`. 194 | 195 | Named parameters are automatically handled on the (jcon-cpp) server side. 196 | 197 | ## Known Issues 198 | 199 | * Does not yet support batch requests/responses 200 | 201 | 202 | ## Contributing 203 | 204 | Bug reports and pull requests are welcome on GitHub at 205 | https://github.com/joncol/jcon-cpp. 206 | 207 | Please follow these 208 | [guidelines](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 209 | for creating commit messages. 210 | 211 | Also make sure to follow the existing code style. No lines with more than 80 212 | characters, spaces instead of tabs for instance. 213 | 214 | 215 | ## License 216 | 217 | The library is available as open source under the terms of the [MIT 218 | License](http://opensource.org/licenses/MIT). 219 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | task default: :copy_changed_files 4 | 5 | desc 'Copy changed source files from ORZ_TOP' 6 | task :copy_changed_files do 7 | raise 'Environment variable ORZ_TOP must be set' if ENV['ORZ_TOP'].nil? 8 | orz_top = ENV['ORZ_TOP'].tr('\\', '/') 9 | raise 'ORZ_TOP not set' unless orz_top 10 | dir_name = 'changed_source_files' 11 | mkdir(dir_name) unless File.directory?(dir_name) 12 | 13 | temp_dir = 'temp' 14 | mkdir(temp_dir) unless File.directory?(temp_dir) 15 | 16 | src_dir = File.join(orz_top, 'src', 'Libs', 'OrzJsonRpc') 17 | 18 | Dir[File.join(src_dir, '*.{cpp,h}')] 19 | .reject { |n| n =~ /pch/ } 20 | .reject { |n| n =~ %r{/jcon.h$} } 21 | .reject { |n| n =~ /jcon_assert\.h/ }.each do |file| 22 | basename = File.basename(file) 23 | dest_file = File.join(temp_dir, basename) 24 | FileUtils.cp(file, dest_file) 25 | system(%(sed -i 'N;N;s/#include "pch\.h"\\n\\n//' #{dest_file})) 26 | end 27 | 28 | Dir[File.join(temp_dir, '*')].each do |file| 29 | basename = File.basename(file) 30 | dest_file = File.join('src', 'jcon', basename) 31 | 32 | copy = if !File.exist?(dest_file) 33 | true 34 | elsif FileUtils.compare_file(file, dest_file) 35 | false 36 | else 37 | true 38 | end 39 | 40 | FileUtils.copy(file, dest_file, verbose: true) if copy 41 | end 42 | 43 | FileUtils.rm_rf(temp_dir) 44 | end 45 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-parts": { 4 | "inputs": { 5 | "nixpkgs-lib": "nixpkgs-lib" 6 | }, 7 | "locked": { 8 | "lastModified": 1704982712, 9 | "narHash": "sha256-2Ptt+9h8dczgle2Oo6z5ni5rt/uLMG47UFTR1ry/wgg=", 10 | "owner": "hercules-ci", 11 | "repo": "flake-parts", 12 | "rev": "07f6395285469419cf9d078f59b5b49993198c00", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "hercules-ci", 17 | "repo": "flake-parts", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1705856552, 24 | "narHash": "sha256-JXfnuEf5Yd6bhMs/uvM67/joxYKoysyE3M2k6T3eWbg=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "612f97239e2cc474c13c9dafa0df378058c5ad8d", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs-lib": { 38 | "locked": { 39 | "dir": "lib", 40 | "lastModified": 1703961334, 41 | "narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=", 42 | "owner": "NixOS", 43 | "repo": "nixpkgs", 44 | "rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "dir": "lib", 49 | "owner": "NixOS", 50 | "ref": "nixos-unstable", 51 | "repo": "nixpkgs", 52 | "type": "github" 53 | } 54 | }, 55 | "root": { 56 | "inputs": { 57 | "flake-parts": "flake-parts", 58 | "nixpkgs": "nixpkgs" 59 | } 60 | } 61 | }, 62 | "root": "root", 63 | "version": 7 64 | } 65 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "jcon-cpp"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | flake-parts.url = "github:hercules-ci/flake-parts"; 7 | }; 8 | 9 | outputs = inputs@{ flake-parts, ... }: 10 | flake-parts.lib.mkFlake { inherit inputs; } { 11 | systems = 12 | [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ]; 13 | perSystem = { config, self', inputs', pkgs, system, ... }: { 14 | devShells.default = 15 | pkgs.mkShell.override { stdenv = pkgs.clangStdenv; } { 16 | packages = with pkgs; [ 17 | clang-tools 18 | cmake 19 | qt6.qtbase 20 | qt6.qtwebsockets 21 | ]; 22 | }; 23 | }; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /jcon-cpp.pro: -------------------------------------------------------------------------------- 1 | CONFIG = ordered 2 | 3 | TEMPLATE = subdirs 4 | 5 | SUBDIRS = \ 6 | src/jcon/jcon.pro \ 7 | src/example.pro 8 | 9 | -------------------------------------------------------------------------------- /src/.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((c-mode . ((mode . c++)))) 2 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(jcon_ex) 2 | 3 | include_directories(${CMAKE_SOURCE_DIR}/src) 4 | 5 | file(GLOB ${PROJECT_NAME}_headers *.h) 6 | file(GLOB ${PROJECT_NAME}_sources *.cpp) 7 | 8 | set(USE_QT true) 9 | 10 | if(USE_QT) 11 | set(CMAKE_AUTOMOC ON) 12 | set(CMAKE_AUTOUIC ON) 13 | set(CMAKE_AUTORCC ON) 14 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 15 | find_program(QT_UIC_EXECUTABLE uic) 16 | endif() 17 | 18 | if(APPLE) 19 | # icon 20 | if(EXISTS "${PROJECT_NAME}.icns") 21 | # set how it shows up in the Info.plist file 22 | set(MACOSX_BUNDLE_ICON_FILE ${PROJECT_NAME}.icns) 23 | 24 | # set where in the bundle to put the icns file 25 | set_source_files_properties(${PROJECT_NAME}.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources) 26 | 27 | # include the icns file in the target 28 | set(${PROJECT_NAME}_sources ${${PROJECT_NAME}_sources} ${PROJECT_NAME}.icns) 29 | endif() 30 | 31 | add_executable(${PROJECT_NAME} MACOSX_BUNDLE ${${PROJECT_NAME}_headers} ${${PROJECT_NAME}_sources} ${UI_HEADERS}) 32 | elseif(WIN32) 33 | add_executable(${PROJECT_NAME} WIN32 ${${PROJECT_NAME}_headers} ${${PROJECT_NAME}_sources} ${UI_HEADERS}) 34 | else() 35 | add_executable(${PROJECT_NAME} ${${PROJECT_NAME}_headers} ${${PROJECT_NAME}_sources} ${UI_HEADERS}) 36 | endif() 37 | 38 | target_link_libraries(${PROJECT_NAME} jcon) 39 | 40 | if(USE_QT) 41 | find_package(Qt6 COMPONENTS Core Network WebSockets) 42 | target_link_libraries(${PROJECT_NAME} Qt6::Core Qt6::Network Qt6::WebSockets) 43 | endif() 44 | 45 | if(APPLE) 46 | set(APPS "\${CMAKE_INSTALL_PREFIX}/bin/${PROJECT_NAME}") 47 | 48 | set(plugin_dest_dir ${PROJECT_NAME}.app/Contents/MacOS) 49 | set(APPS "\${CMAKE_INSTALL_PREFIX}/${PROJECT_NAME}.app") 50 | 51 | install(CODE " 52 | include(BundleUtilities) 53 | fixup_bundle(\"${APPS}\" \"\" \"${DIRS}\") 54 | " COMPONENT Runtime) 55 | 56 | else() 57 | install(TARGETS ${EXECUTABLE_NAME} 58 | RUNTIME DESTINATION bin 59 | LIBRARY DESTINATION lib 60 | ARCHIVE DESTINATION lib/static) 61 | endif() 62 | 63 | if(MINGW) 64 | get_filename_component(MINGW_DIR ${CMAKE_C_COMPILER} PATH) 65 | add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD 66 | COMMAND ${CMAKE_COMMAND} -E copy_if_different 67 | "${MINGW_DIR}/libgcc_s_dw2-1.dll" 68 | $ 69 | COMMAND ${CMAKE_COMMAND} -E copy_if_different 70 | "${MINGW_DIR}/libstdc++-6.dll" 71 | $) 72 | endif() 73 | 74 | add_subdirectory(jcon) 75 | -------------------------------------------------------------------------------- /src/example.pro: -------------------------------------------------------------------------------- 1 | QT += core network 2 | 3 | TEMPLATE = app 4 | 5 | # The lib jcon is built before the example in jcon-cpp.pro 6 | LIBS += -Ljcon -ljcon 7 | 8 | HEADERS = *.h 9 | SOURCES = *.cpp 10 | 11 | OTHER_FILES = CMakeLists.txt 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/example_service.cpp: -------------------------------------------------------------------------------- 1 | #include "example_service.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | ExampleService::ExampleService() = default; 9 | 10 | ExampleService::~ExampleService() = default; 11 | 12 | int ExampleService::getRandomInt(int limit) 13 | { 14 | qDebug().noquote() << QString("-> getRandomInt: '%1' (client IP: %2)") 15 | .arg(limit) 16 | .arg(jcon::JsonRpcServer::clientEndpoint()->peerAddress().toString()); 17 | return QRandomGenerator::global()->generate() % limit; 18 | } 19 | 20 | QString ExampleService::printMessage(const QString& msg) 21 | { 22 | qDebug().noquote() << QString("-> printMessage: '%1'").arg(msg); 23 | return QString("Return: '%1'").arg(msg); 24 | } 25 | 26 | void ExampleService::printNotification(const QString &msg) { 27 | qDebug().noquote() << QString("-> printNotification: '%1'").arg(msg); 28 | } 29 | 30 | void ExampleService::namedParams(QString& msg, int answer) 31 | { 32 | qDebug().noquote() << QString("-> namedParams"); 33 | qDebug().noquote() << " msg: " << msg; 34 | qDebug().noquote() << " answer: " << answer; 35 | } 36 | -------------------------------------------------------------------------------- /src/example_service.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | class ExampleService : public QObject 5 | { 6 | Q_OBJECT 7 | 8 | public: 9 | ExampleService(); 10 | virtual ~ExampleService(); 11 | 12 | Q_INVOKABLE int getRandomInt(int limit); 13 | Q_INVOKABLE QString printMessage(const QString& msg); 14 | Q_INVOKABLE void printNotification(const QString& msg); 15 | Q_INVOKABLE void namedParams(QString& msg, int answer); 16 | }; 17 | -------------------------------------------------------------------------------- /src/include.pri: -------------------------------------------------------------------------------- 1 | 2 | #SOURCES += $$PWD/jcon/string_util.cpp \ 3 | # $$PWD/jcon/json_rpc_websocket_server.cpp \ 4 | # $$PWD/jcon/json_rpc_websocket_client.cpp \ 5 | # $$PWD/jcon/json_rpc_websocket.cpp \ 6 | # $$PWD/jcon/json_rpc_tcp_socket.cpp \ 7 | # $$PWD/jcon/json_rpc_tcp_server.cpp \ 8 | # $$PWD/jcon/json_rpc_tcp_client.cpp \ 9 | # $$PWD/jcon/json_rpc_success.cpp \ 10 | # $$PWD/jcon/json_rpc_server.cpp \ 11 | # $$PWD/jcon/json_rpc_request.cpp \ 12 | # $$PWD/jcon/json_rpc_logger.cpp \ 13 | # $$PWD/jcon/json_rpc_file_logger.cpp \ 14 | # $$PWD/jcon/json_rpc_error.cpp \ 15 | # $$PWD/jcon/json_rpc_endpoint.cpp \ 16 | # $$PWD/jcon/json_rpc_debug_logger.cpp \ 17 | # $$PWD/jcon/json_rpc_client.cpp 18 | 19 | HEADERS += \ 20 | $$PWD/jcon/string_util.h \ 21 | $$PWD/jcon/json_rpc_websocket_server.h \ 22 | $$PWD/jcon/json_rpc_websocket_client.h \ 23 | $$PWD/jcon/json_rpc_websocket.h \ 24 | $$PWD/jcon/json_rpc_tcp_socket.h \ 25 | $$PWD/jcon/json_rpc_tcp_server.h \ 26 | $$PWD/jcon/json_rpc_tcp_client.h \ 27 | $$PWD/jcon/json_rpc_success.h \ 28 | $$PWD/jcon/json_rpc_socket.h \ 29 | $$PWD/jcon/json_rpc_server.h \ 30 | $$PWD/jcon/json_rpc_result.h \ 31 | $$PWD/jcon/json_rpc_request.h \ 32 | $$PWD/jcon/json_rpc_logger.h \ 33 | $$PWD/jcon/json_rpc_file_logger.h \ 34 | $$PWD/jcon/json_rpc_error.h \ 35 | $$PWD/jcon/json_rpc_endpoint.h \ 36 | $$PWD/jcon/json_rpc_debug_logger.h \ 37 | $$PWD/jcon/json_rpc_client.h \ 38 | $$PWD/jcon/jcon_assert.h \ 39 | $$PWD/jcon/jcon.h 40 | 41 | -------------------------------------------------------------------------------- /src/jcon/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(jcon) 2 | 3 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 4 | 5 | file(GLOB ${PROJECT_NAME}_headers *.h) 6 | file(GLOB ${PROJECT_NAME}_sources *.cpp) 7 | 8 | add_definitions(-DJCON_DLL) 9 | 10 | add_library(${PROJECT_NAME} SHARED ${${PROJECT_NAME}_headers} ${${PROJECT_NAME}_sources}) 11 | set_target_properties(${PROJECT_NAME} PROPERTIES PUBLIC_HEADER "${${PROJECT_NAME}_headers}") 12 | find_package(Qt6 REQUIRED COMPONENTS Core Network WebSockets) 13 | qt_standard_project_setup() 14 | target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Core Qt6::Network) 15 | 16 | install(TARGETS ${PROJECT_NAME} 17 | RUNTIME DESTINATION bin 18 | LIBRARY DESTINATION lib 19 | PUBLIC_HEADER DESTINATION include/jcon 20 | ARCHIVE DESTINATION lib/static) 21 | -------------------------------------------------------------------------------- /src/jcon/jcon.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifndef JCON_DLL 6 | #define JCON_DLL 7 | #endif 8 | 9 | // Use the generic helper definitions above to define JCON_API, which is used 10 | // for the public API symbols. It either DLL imports or DLL exports (or does 11 | // nothing for static build). 12 | 13 | #ifdef JCON_DLL // defined if JCON is compiled as a DLL 14 | #ifdef jcon_EXPORTS // defined if building the DLL (vs. using it) 15 | #define JCON_API Q_DECL_EXPORT 16 | #else 17 | #define JCON_API Q_DECL_IMPORT 18 | #endif // jcon_EXPORTS 19 | #else // JCON_DLL is not defined: this means JCON is a static lib. 20 | #define JCON_API 21 | #endif // JCON_DLL 22 | -------------------------------------------------------------------------------- /src/jcon/jcon.pro: -------------------------------------------------------------------------------- 1 | QT += testlib websockets 2 | 3 | TEMPLATE = lib 4 | 5 | HEADERS = jcon*.h \ 6 | json*.h \ 7 | string_util.h 8 | SOURCES = json*.cpp \ 9 | string_util.cpp 10 | 11 | OTHER_FILES = CMakeLists.txt 12 | -------------------------------------------------------------------------------- /src/jcon/jcon_assert.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #define JCON_ASSERT assert 5 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_client.cpp: -------------------------------------------------------------------------------- 1 | #include "json_rpc_client.h" 2 | #include "json_rpc_file_logger.h" 3 | #include "json_rpc_success.h" 4 | #include "jcon_assert.h" 5 | #include "string_util.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | namespace jcon { 15 | 16 | JsonRpcClient::JsonRpcClient(std::shared_ptr socket, 17 | QObject* parent, 18 | std::shared_ptr logger, 19 | int call_timeout_ms) 20 | : QObject(parent) 21 | , m_logger(logger) 22 | , m_call_timeout_ms(call_timeout_ms) 23 | , m_outstanding_request_count(0) 24 | , m_allowNotification(false) 25 | { 26 | if (!m_logger) { 27 | m_logger = std::make_shared("json_client_log.txt"); 28 | } 29 | 30 | m_endpoint = std::make_shared(socket, m_logger, this); 31 | 32 | connect(m_endpoint.get(), &JsonRpcEndpoint::socketConnected, 33 | this, &JsonRpcClient::socketConnected); 34 | 35 | connect(m_endpoint.get(), &JsonRpcEndpoint::socketDisconnected, 36 | this, &JsonRpcClient::socketDisconnected); 37 | 38 | connect(m_endpoint.get(), &JsonRpcEndpoint::socketError, 39 | this, &JsonRpcClient::socketError); 40 | 41 | connect(m_endpoint.get(), &JsonRpcEndpoint::jsonObjectReceived, 42 | this, &JsonRpcClient::jsonResponseReceived); 43 | } 44 | 45 | JsonRpcClient::~JsonRpcClient() 46 | { 47 | disconnectFromServer(); 48 | } 49 | 50 | std::shared_ptr 51 | JsonRpcClient::waitForSyncCallbacks(const JsonRpcRequest* request) 52 | { 53 | connect(request, &JsonRpcRequest::result, 54 | [this, id = request->id()](const QVariant& result) { 55 | m_logger->logDebug( 56 | QString("Received success response to synchronous " 57 | "RPC call (ID: %1)").arg(qPrintable(id))); 58 | 59 | m_results[id] = std::make_shared(result); 60 | }); 61 | 62 | connect(request, &JsonRpcRequest::error, 63 | [this, id = request->id()](int code, 64 | const QString& message, 65 | const QVariant& data) 66 | { 67 | m_logger->logError( 68 | QString("Received error response to synchronous " 69 | "RPC call (ID: %1)").arg(qPrintable(id))); 70 | 71 | m_results[id] = 72 | std::make_shared(code, message, data); 73 | }); 74 | 75 | QElapsedTimer timer; 76 | timer.start(); 77 | while (m_outstanding_requests.contains(request->id()) && 78 | timer.elapsed() < m_call_timeout_ms) 79 | { 80 | QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); 81 | } 82 | 83 | if (m_results.contains(request->id())) { 84 | auto res = m_results[request->id()]; 85 | m_results.remove(request->id()); 86 | return res; 87 | } else { 88 | return std::make_shared( 89 | JsonRpcError::EC_InternalError, 90 | "RPC call timed out" 91 | ); 92 | } 93 | } 94 | 95 | std::shared_ptr 96 | JsonRpcClient::callExpandArgs(const QString& method, const QVariantList& args) 97 | { 98 | auto req = doCallExpandArgs(method, false, args); 99 | return waitForSyncCallbacks(req.get()); 100 | } 101 | 102 | std::shared_ptr 103 | JsonRpcClient::callAsyncExpandArgs(const QString& method, 104 | const QVariantList& args) 105 | { 106 | return doCallExpandArgs(method, true, args); 107 | } 108 | 109 | std::shared_ptr 110 | JsonRpcClient::doCallExpandArgs(const QString& method, 111 | bool async, 112 | const QVariantList& args) 113 | { 114 | std::shared_ptr request; 115 | QJsonObject req_json_obj; 116 | std::tie(request, req_json_obj) = prepareCall(method); 117 | 118 | if (!args.empty()) { 119 | req_json_obj["params"] = QJsonArray::fromVariantList(args); 120 | } 121 | 122 | m_logger->logInfo(formatLogMessage(method, args, async, request->id())); 123 | m_endpoint->send(QJsonDocument(req_json_obj)); 124 | 125 | return request; 126 | } 127 | 128 | std::shared_ptr 129 | JsonRpcClient::callNamedParams(const QString& method, const QVariantMap& args) 130 | { 131 | auto req = doCallNamedParams(method, false, args); 132 | return waitForSyncCallbacks(req.get()); 133 | } 134 | 135 | std::shared_ptr 136 | JsonRpcClient::callAsyncNamedParams(const QString& method, 137 | const QVariantMap& args) 138 | { 139 | return doCallNamedParams(method, true, args); 140 | } 141 | 142 | std::shared_ptr 143 | JsonRpcClient::doCallNamedParams(const QString& method, 144 | bool async, 145 | const QVariantMap& args) 146 | { 147 | std::shared_ptr request; 148 | QJsonObject req_json_obj; 149 | std::tie(request, req_json_obj) = prepareCall(method); 150 | 151 | if (!args.empty()) { 152 | req_json_obj["params"] = QJsonObject::fromVariantMap(args); 153 | } 154 | 155 | m_logger->logInfo(formatLogMessage(method, args, async, request->id())); 156 | m_endpoint->send(QJsonDocument(req_json_obj)); 157 | 158 | return request; 159 | } 160 | 161 | int JsonRpcClient::outstandingRequestCount() const 162 | { 163 | return m_outstanding_request_count; 164 | } 165 | 166 | void JsonRpcClient::verifyConnected(const QString& method) 167 | { 168 | if (!isConnected()) { 169 | auto msg = QString("cannot call RPC method (%1) when not connected") 170 | .arg(method); 171 | m_logger->logError(msg); 172 | } 173 | } 174 | 175 | std::pair, QJsonObject> 176 | JsonRpcClient::prepareCall(const QString& method) 177 | { 178 | std::shared_ptr request; 179 | RequestId id; 180 | std::tie(request, id) = createRequest(); 181 | m_outstanding_requests[id] = request; 182 | ++m_outstanding_request_count; 183 | QJsonObject req_json_obj = createRequestJsonObject(method, id); 184 | return std::make_pair(request, req_json_obj); 185 | } 186 | 187 | std::pair, JsonRpcClient::RequestId> 188 | JsonRpcClient::createRequest() 189 | { 190 | auto id = createUuid(); 191 | auto request = std::make_shared(this, id); 192 | return std::make_pair(request, id); 193 | } 194 | 195 | JsonRpcClient::RequestId JsonRpcClient::createUuid() 196 | { 197 | RequestId id = QUuid::createUuid().toString(); 198 | int len = id.length(); 199 | id = id.left(len - 1).right(len - 2); 200 | return id; 201 | } 202 | 203 | QJsonObject JsonRpcClient::createRequestJsonObject(const QString& method, 204 | const QString& id) 205 | { 206 | return QJsonObject { 207 | { "jsonrpc", "2.0" }, 208 | { "method", method }, 209 | { "id", id } 210 | }; 211 | } 212 | 213 | QJsonObject JsonRpcClient::createNotificationJsonObject(const QString& method) 214 | { 215 | return QJsonObject { 216 | { "jsonrpc", "2.0" }, 217 | { "method", method } 218 | }; 219 | } 220 | 221 | bool JsonRpcClient::connectToServer(const QString& host, int port) 222 | { 223 | if (!m_endpoint->connectToHost(host, port)) { 224 | return false; 225 | } 226 | return true; 227 | } 228 | 229 | void JsonRpcClient::connectToServerAsync(const QString& host, int port) 230 | { 231 | m_endpoint->connectToHostAsync(host, port); 232 | } 233 | 234 | bool JsonRpcClient::connectToServer(const QUrl& url) 235 | { 236 | if (!m_endpoint->connectToUrl(url)) { 237 | return false; 238 | } 239 | return true; 240 | } 241 | 242 | void JsonRpcClient::disconnectFromServer() 243 | { 244 | m_endpoint->disconnectFromHost(); 245 | } 246 | 247 | bool JsonRpcClient::isConnected() const 248 | { 249 | return m_endpoint->isConnected(); 250 | } 251 | 252 | QHostAddress JsonRpcClient::clientAddress() const 253 | { 254 | return m_endpoint->localAddress(); 255 | } 256 | 257 | int JsonRpcClient::clientPort() const 258 | { 259 | return m_endpoint->localPort(); 260 | } 261 | 262 | QHostAddress JsonRpcClient::serverAddress() const 263 | { 264 | return m_endpoint->peerAddress(); 265 | } 266 | 267 | int JsonRpcClient::serverPort() const 268 | { 269 | return m_endpoint->peerPort(); 270 | } 271 | 272 | void JsonRpcClient::enableReceiveNotification(bool enabled) 273 | { 274 | m_allowNotification = enabled; 275 | } 276 | 277 | void JsonRpcClient::jsonResponseReceived(const QJsonObject& response) 278 | { 279 | JCON_ASSERT(response["jsonrpc"].toString() == "2.0"); 280 | 281 | if (response.value("jsonrpc").toString() != "2.0") { 282 | logError("invalid protocol tag"); 283 | return; 284 | } 285 | 286 | if (response.value("error").isObject()) { 287 | int code; 288 | QString msg; 289 | QVariant data; 290 | getJsonErrorInfo(response, code, msg, data); 291 | logError(QString("(%1) - %2").arg(code).arg(msg)); 292 | 293 | RequestId id = response.value("id").toString(InvalidRequestId); 294 | if (id != InvalidRequestId) { 295 | auto it = m_outstanding_requests.find(id); 296 | if (it == m_outstanding_requests.end()) { 297 | logError(QString("got error response for non-existing " 298 | "request: %1").arg(id)); 299 | return; 300 | } 301 | emit it.value()->error(code, msg, data); 302 | m_outstanding_requests.erase(it); 303 | --m_outstanding_request_count; 304 | } 305 | 306 | return; 307 | } 308 | 309 | if (m_allowNotification && !response.contains("id")) { 310 | if (response["method"].isUndefined() || response["params"].isUndefined()) { 311 | logError("method/params is undefined"); 312 | return; 313 | } 314 | 315 | QString key = response.value("method").toString(); 316 | QVariant value = response.value("params").toVariant(); 317 | 318 | emit notificationReceived(key, value); 319 | return; 320 | } 321 | 322 | if (response["result"].isUndefined()) { 323 | logError("result is undefined"); 324 | return; 325 | } 326 | 327 | RequestId id = response.value("id").toString(InvalidRequestId); 328 | if (id == InvalidRequestId) { 329 | logError("response ID is undefined"); 330 | return; 331 | } 332 | 333 | QVariant result = response.value("result").toVariant(); 334 | 335 | auto it = m_outstanding_requests.find(id); 336 | if (it == m_outstanding_requests.end()) { 337 | logError(QString("got response to non-existing request: %1").arg(id)); 338 | return; 339 | } 340 | 341 | emit it.value()->result(result); 342 | m_outstanding_requests.erase(it); 343 | --m_outstanding_request_count; 344 | } 345 | 346 | void JsonRpcClient::getJsonErrorInfo(const QJsonObject& response, 347 | int& code, 348 | QString& message, 349 | QVariant& data) 350 | { 351 | QJsonObject error = response["error"].toObject(); 352 | code = error["code"].toInt(); 353 | message = error["message"].toString("unknown error"); 354 | data = error.value("data").toVariant(); 355 | } 356 | 357 | QString JsonRpcClient::formatLogMessage(const QString& method, 358 | const QVariantList& args, 359 | bool async, 360 | const QString& request_id) 361 | { 362 | auto msg = QString("Calling (%1) RPC method: '%2' ") 363 | .arg(async ? "async" : "sync").arg(method); 364 | 365 | if (args.empty()) { 366 | msg += "without arguments"; 367 | } else { 368 | msg += QString("with argument%1: %2") 369 | .arg(args.size() == 1 ? "" : "s") 370 | .arg(variantListToStringList(args).join(", ")); 371 | } 372 | msg += QString(" (request ID: %1)").arg(request_id); 373 | return msg; 374 | } 375 | 376 | QString JsonRpcClient::formatLogMessage(const QString& method, 377 | const QVariantMap& args, 378 | bool async, 379 | const QString& request_id) 380 | { 381 | auto msg = QString("Calling (%1) RPC method: '%2' ") 382 | .arg(async ? "async" : "sync").arg(method); 383 | 384 | msg += QString("with named parameters: %1") 385 | .arg(variantMapToStringList(args).join(", ")); 386 | msg += QString(" (request ID: %1)").arg(request_id); 387 | return msg; 388 | } 389 | 390 | void JsonRpcClient::logError(const QString& msg) 391 | { 392 | m_logger->logError("JSON RPC client error: " + msg); 393 | } 394 | 395 | } 396 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_client.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | #include "json_rpc_endpoint.h" 5 | #include "json_rpc_error.h" 6 | #include "json_rpc_logger.h" 7 | #include "json_rpc_request.h" 8 | #include "json_rpc_result.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | 18 | namespace jcon { 19 | 20 | class JsonRpcSocket; 21 | 22 | class JCON_API JsonRpcClient : public QObject 23 | { 24 | Q_OBJECT 25 | 26 | public: 27 | using RequestId = QString ; 28 | 29 | JsonRpcClient(std::shared_ptr socket, 30 | QObject* parent = nullptr, 31 | std::shared_ptr logger = nullptr, 32 | int call_timeout_ms = 60000); 33 | 34 | virtual ~JsonRpcClient(); 35 | 36 | /// @return true if connection was successful 37 | bool connectToServer(const QString& host, int port); 38 | void connectToServerAsync(const QString& host, int port); 39 | bool connectToServer(const QUrl& url); 40 | void disconnectFromServer(); 41 | 42 | bool isConnected() const; 43 | 44 | QHostAddress clientAddress() const; 45 | int clientPort() const; 46 | 47 | QHostAddress serverAddress() const; 48 | int serverPort() const; 49 | 50 | /// Allow a client to receive unsolicited notifications from server 51 | void enableReceiveNotification(bool enabled); 52 | 53 | template 54 | std::shared_ptr call(const QString& method, T&&... args); 55 | 56 | template 57 | std::shared_ptr callAsync(const QString& method, 58 | T&&... args); 59 | 60 | /// Expand arguments in list before making the RPC call 61 | std::shared_ptr callExpandArgs(const QString& method, 62 | const QVariantList& args); 63 | 64 | /// Expand arguments in list before making the RPC call 65 | std::shared_ptr 66 | callAsyncExpandArgs(const QString& method, const QVariantList& args); 67 | 68 | /// Named parameters 69 | std::shared_ptr 70 | callAsyncNamedParams(const QString& method, const QVariantMap& args); 71 | 72 | /// Named parameters 73 | std::shared_ptr 74 | callNamedParams(const QString& method, const QVariantMap& args); 75 | 76 | template 77 | void notification(const QString& method, T&&... args); 78 | 79 | int outstandingRequestCount() const; 80 | 81 | signals: 82 | /// Emitted when a connection has been made to the server. 83 | void socketConnected(QObject* socket); 84 | 85 | /// Emitted when connection to server is lost. 86 | void socketDisconnected(QObject* socket); 87 | 88 | /// Emitted when the RPC socket has an error. 89 | void socketError(QObject* socket, QAbstractSocket::SocketError error); 90 | 91 | /// Emitted when an unsolicited notification is received 92 | void notificationReceived(const QString& key, const QVariant& value); 93 | 94 | protected: 95 | void logError(const QString& msg); 96 | 97 | private slots: 98 | void jsonResponseReceived(const QJsonObject& obj); 99 | 100 | private: 101 | const QString InvalidRequestId = ""; 102 | 103 | static QString formatLogMessage(const QString& method, 104 | const QVariantList& args, 105 | bool async, 106 | const QString& request_id); 107 | 108 | static QString formatLogMessage(const QString& method, 109 | const QVariantMap& args, 110 | bool async, 111 | const QString& request_id); 112 | 113 | std::shared_ptr 114 | waitForSyncCallbacks(const JsonRpcRequest* request); 115 | 116 | template 117 | std::shared_ptr doCall(const QString& method, 118 | bool async, 119 | T&&... args); 120 | 121 | std::shared_ptr 122 | doCallExpandArgs(const QString& method, 123 | bool async, 124 | const QVariantList& args); 125 | 126 | std::shared_ptr 127 | doCallNamedParams(const QString& method, 128 | bool async, 129 | const QVariantMap& args); 130 | 131 | template 132 | void doNotification(const QString& method, T&&... args); 133 | 134 | void verifyConnected(const QString& method); 135 | 136 | std::pair, QJsonObject> 137 | prepareCall(const QString& method); 138 | 139 | std::pair, RequestId> createRequest(); 140 | static RequestId createUuid(); 141 | QJsonObject createRequestJsonObject(const QString& method, 142 | const QString& id); 143 | 144 | QJsonObject createNotificationJsonObject(const QString& method); 145 | 146 | void convertToQVariantList(QVariantList& /*result*/) {} 147 | 148 | template 149 | void convertToQVariantList(QVariantList& result, T&& x); 150 | 151 | template 152 | void convertToQVariantList(QVariantList& result, T&& head, Ts&&... tail); 153 | 154 | static void getJsonErrorInfo(const QJsonObject& response, 155 | int& code, 156 | QString& message, 157 | QVariant& data); 158 | 159 | std::shared_ptr m_logger; 160 | std::shared_ptr m_endpoint; 161 | 162 | int m_call_timeout_ms; 163 | 164 | using RequestMap = QMap>; 165 | RequestMap m_outstanding_requests; 166 | int m_outstanding_request_count; 167 | 168 | using ResultMap = QMap>; 169 | ResultMap m_results; 170 | 171 | bool m_allowNotification; 172 | }; 173 | 174 | template 175 | std::shared_ptr 176 | JsonRpcClient::call(const QString& method, Ts&&... args) 177 | { 178 | auto req = doCall(method, false, std::forward(args)...); 179 | return waitForSyncCallbacks(req.get()); 180 | } 181 | 182 | template 183 | std::shared_ptr 184 | JsonRpcClient::callAsync(const QString& method, Ts&&... args) 185 | { 186 | return doCall(method, true, std::forward(args)...); 187 | } 188 | 189 | template 190 | void JsonRpcClient::notification(const QString& method, Ts&&... args) 191 | { 192 | doNotification(method, std::forward(args)...); 193 | } 194 | 195 | template 196 | std::shared_ptr 197 | JsonRpcClient::doCall(const QString& method, bool async, Ts&&... args) 198 | { 199 | verifyConnected(method); 200 | 201 | std::shared_ptr request; 202 | QJsonObject req_json_obj; 203 | std::tie(request, req_json_obj) = prepareCall(method); 204 | 205 | QVariantList param_list; 206 | constexpr std::size_t arg_count = sizeof...(Ts); 207 | if (arg_count != 0) { 208 | convertToQVariantList(param_list, std::forward(args)...); 209 | req_json_obj["params"] = QJsonArray::fromVariantList(param_list); 210 | } 211 | 212 | m_logger->logInfo( 213 | formatLogMessage(method, param_list, async, request->id())); 214 | 215 | m_endpoint->send(QJsonDocument(req_json_obj)); 216 | 217 | return request; 218 | } 219 | 220 | template 221 | void JsonRpcClient::doNotification(const QString& method, Ts&&... args) 222 | { 223 | verifyConnected(method); 224 | 225 | QJsonObject req_json_obj; 226 | req_json_obj = createNotificationJsonObject(method); 227 | 228 | QVariantList param_list; 229 | constexpr std::size_t arg_count = sizeof...(Ts); 230 | if (arg_count != 0) { 231 | convertToQVariantList(param_list, std::forward(args)...); 232 | req_json_obj["params"] = QJsonArray::fromVariantList(param_list); 233 | } 234 | 235 | m_endpoint->send(QJsonDocument(req_json_obj)); 236 | } 237 | 238 | template 239 | void JsonRpcClient::convertToQVariantList(QVariantList& result, T&& x) 240 | { 241 | result.push_front(x); 242 | } 243 | 244 | template 245 | void JsonRpcClient::convertToQVariantList(QVariantList& result, 246 | T&& head, Ts&&... tail) 247 | { 248 | convertToQVariantList(result, std::forward(tail)...); 249 | result.push_front(head); 250 | } 251 | 252 | } 253 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_debug_logger.cpp: -------------------------------------------------------------------------------- 1 | #include "json_rpc_debug_logger.h" 2 | 3 | #include 4 | 5 | namespace jcon { 6 | 7 | void JsonRpcDebugLogger::logDebug(const QString& message) 8 | { 9 | qDebug().noquote() << message; 10 | } 11 | 12 | void JsonRpcDebugLogger::logInfo(const QString& message) 13 | { 14 | qDebug().noquote() << message; 15 | } 16 | 17 | void JsonRpcDebugLogger::logWarning(const QString& message) 18 | { 19 | qDebug().noquote() << message; 20 | } 21 | 22 | void JsonRpcDebugLogger::logError(const QString& message) 23 | { 24 | qDebug().noquote() << message; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_debug_logger.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | #include "json_rpc_logger.h" 5 | 6 | namespace jcon { 7 | 8 | class JCON_API JsonRpcDebugLogger : public JsonRpcLogger 9 | { 10 | public: 11 | void logDebug(const QString& message) override; 12 | void logInfo(const QString& message) override; 13 | void logWarning(const QString& message) override; 14 | void logError(const QString& message) override; 15 | }; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_endpoint.cpp: -------------------------------------------------------------------------------- 1 | #include "json_rpc_endpoint.h" 2 | #include "json_rpc_socket.h" 3 | #include "jcon_assert.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace jcon { 11 | 12 | JsonRpcEndpoint::JsonRpcEndpoint(std::shared_ptr socket, 13 | std::shared_ptr logger, 14 | QObject* parent) 15 | : QObject(parent) 16 | , m_logger(logger) 17 | , m_socket(socket) 18 | { 19 | connect(m_socket.get(), &JsonRpcSocket::socketConnected, 20 | this, &JsonRpcEndpoint::socketConnected); 21 | 22 | connect(m_socket.get(), &JsonRpcSocket::socketDisconnected, 23 | this, &JsonRpcEndpoint::socketDisconnected); 24 | 25 | connect(m_socket.get(), &JsonRpcSocket::dataReceived, 26 | this, &JsonRpcEndpoint::dataReady); 27 | 28 | connect(m_socket.get(), &JsonRpcSocket::socketError, 29 | this, &JsonRpcEndpoint::socketError); 30 | } 31 | 32 | JsonRpcEndpoint::~JsonRpcEndpoint() 33 | { 34 | m_socket->disconnect(this); 35 | } 36 | 37 | bool JsonRpcEndpoint::connectToHost(const QString& host, int port, int msecs) 38 | { 39 | m_logger->logInfo(QString("connecting to JSON RPC server at %1:%2") 40 | .arg(host).arg(port)); 41 | 42 | m_socket->connectToHost(host, port); 43 | 44 | if (!m_socket->waitForConnected(msecs)) { 45 | m_logger->logError("could not connect to JSON RPC server: " + 46 | m_socket->errorString()); 47 | return false; 48 | } 49 | 50 | m_logger->logInfo(QString("connected to JSON RPC server %1:%2 " 51 | "(local port: %3)") 52 | .arg(host).arg(port).arg(m_socket->localPort())); 53 | return true; 54 | } 55 | 56 | void JsonRpcEndpoint::connectToHostAsync(const QString& host, int port) 57 | { 58 | m_socket->connectToHost(host, port); 59 | } 60 | 61 | bool JsonRpcEndpoint::connectToUrl(const QUrl& url, int msecs) 62 | { 63 | m_logger->logInfo(QString("connecting to JSON RPC server at %1") 64 | .arg(url.toString())); 65 | 66 | m_socket->connectToUrl(url); 67 | 68 | if (!m_socket->waitForConnected(msecs)) { 69 | m_logger->logError("could not connect to JSON RPC server: " + 70 | m_socket->errorString()); 71 | return false; 72 | } 73 | 74 | m_logger->logInfo(QString("connected to JSON RPC server %1 " 75 | "(local port: %3)") 76 | .arg(url.toString()).arg(m_socket->localPort())); 77 | return true; 78 | } 79 | 80 | void JsonRpcEndpoint::connectToUrlAsync(const QUrl& url) 81 | { 82 | m_socket->connectToUrl(url); 83 | } 84 | 85 | void JsonRpcEndpoint::disconnectFromHost() 86 | { 87 | m_socket->disconnectFromHost(); 88 | } 89 | 90 | bool JsonRpcEndpoint::isConnected() const 91 | { 92 | return m_socket->isConnected(); 93 | } 94 | 95 | QHostAddress JsonRpcEndpoint::localAddress() const 96 | { 97 | return m_socket->localAddress(); 98 | } 99 | 100 | int JsonRpcEndpoint::localPort() const 101 | { 102 | return m_socket->localPort(); 103 | } 104 | 105 | QHostAddress JsonRpcEndpoint::peerAddress() const 106 | { 107 | return m_socket->peerAddress(); 108 | } 109 | 110 | int JsonRpcEndpoint::peerPort() const 111 | { 112 | return m_socket->peerPort(); 113 | } 114 | 115 | void JsonRpcEndpoint::send(const QJsonDocument& doc) 116 | { 117 | QByteArray bytes = doc.toJson(); 118 | m_socket->send(bytes); 119 | } 120 | 121 | void JsonRpcEndpoint::dataReady(const QByteArray& bytes, QObject* socket) 122 | { 123 | JCON_ASSERT(bytes.length() > 0); 124 | // Copying data to new buffer, because the endpoint buffer may be 125 | // invalidated at any time by closing socket from outside which will cause 126 | // an exception. 127 | m_recv_buffer += QByteArray::fromRawData(bytes.data(), bytes.size()); 128 | m_recv_buffer = processBuffer(m_recv_buffer.trimmed(), socket); 129 | } 130 | 131 | QByteArray JsonRpcEndpoint::processBuffer(const QByteArray& buf, 132 | QObject* socket) 133 | { 134 | JCON_ASSERT(buf[0] == '{'); 135 | if (buf[0] != '{') { 136 | m_logger->logError("Malformed request"); 137 | return nullptr; 138 | } 139 | 140 | bool in_string = false, in_esc = false; 141 | int brace_nesting_level = 0; 142 | int start = 0; 143 | 144 | int i = 0; 145 | while (i < buf.length() ) { 146 | const char curr_ch = buf[i++]; 147 | 148 | if (curr_ch == '"' && !in_esc) { 149 | in_string = !in_string; 150 | continue; 151 | } 152 | 153 | if (!in_string) { 154 | if (curr_ch == '{') 155 | ++brace_nesting_level; 156 | else if (curr_ch == '}') { 157 | --brace_nesting_level; 158 | JCON_ASSERT(brace_nesting_level >= 0); 159 | 160 | if (brace_nesting_level == 0) { 161 | auto doc = QJsonDocument::fromJson(buf.mid(start, i - start)); 162 | JCON_ASSERT(!doc.isNull()); 163 | JCON_ASSERT(doc.isObject()); 164 | if (doc.isObject()) 165 | emit jsonObjectReceived(doc.object(), socket); 166 | start = i; 167 | continue; 168 | } 169 | } 170 | } else { 171 | // in_string == true, maintain in_esc flag (which can only be latched true when in_string) 172 | if (curr_ch == '\\' && !in_esc) 173 | in_esc = true; 174 | else 175 | in_esc = false; 176 | } 177 | } 178 | return start > 0 ? buf.mid(start) : buf; 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_endpoint.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | #include "json_rpc_logger.h" 5 | #include "json_rpc_socket.h" 6 | 7 | #include 8 | 9 | #include 10 | 11 | class QJsonObject; 12 | class QTcpSocket; 13 | class QUrl; 14 | 15 | namespace jcon { 16 | 17 | /** 18 | * Abstraction layer around JsonRpcSocket. Takes care of deserializing complete 19 | * JSON objects from byte stream. 20 | */ 21 | class JCON_API JsonRpcEndpoint : public QObject 22 | { 23 | Q_OBJECT 24 | 25 | public: 26 | JsonRpcEndpoint(std::shared_ptr socket, 27 | std::shared_ptr logger, 28 | QObject* parent = nullptr); 29 | virtual ~JsonRpcEndpoint(); 30 | 31 | bool connectToHost(const QString& host, int port, int msecs = 5000); 32 | void connectToHostAsync(const QString& host, int port); 33 | bool connectToUrl(const QUrl& url, int msecs = 5000); 34 | void connectToUrlAsync(const QUrl& url); 35 | void disconnectFromHost(); 36 | bool isConnected() const; 37 | 38 | QHostAddress localAddress() const; 39 | int localPort() const; 40 | QHostAddress peerAddress() const; 41 | int peerPort() const; 42 | 43 | void send(const QJsonDocument& doc); 44 | 45 | signals: 46 | /** 47 | * Emitted for every JSON object received. 48 | * 49 | * @param[in] obj The JSON object received. 50 | * @param[in] sender The socket identifier (e.g. a QTcpSocket*). 51 | */ 52 | void jsonObjectReceived(const QJsonObject& obj, QObject* sender); 53 | 54 | /// Emitted when the underlying socket is connected. 55 | void socketConnected(QObject* socket); 56 | 57 | /// Emitted when the underlying socket is disconnected. 58 | void socketDisconnected(QObject* socket); 59 | 60 | /// Emitted when the underlying socket has an error. 61 | void socketError(QObject* socket, QAbstractSocket::SocketError error); 62 | 63 | private slots: 64 | void dataReady(const QByteArray& bytes, QObject* socket); 65 | 66 | private: 67 | /** Check buffer for complete JSON objects, and emit jsonObjectReceived for 68 | each one. */ 69 | QByteArray processBuffer(const QByteArray& buf, QObject* socket); 70 | 71 | std::shared_ptr m_logger; 72 | std::shared_ptr m_socket; 73 | QByteArray m_recv_buffer; 74 | }; 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_error.cpp: -------------------------------------------------------------------------------- 1 | #include "json_rpc_error.h" 2 | 3 | namespace jcon { 4 | 5 | JsonRpcError::JsonRpcError(int code, 6 | const QString& message, 7 | const QVariant& data) 8 | : m_code(code) 9 | , m_message(message) 10 | , m_data(data) 11 | { 12 | } 13 | 14 | JsonRpcError::JsonRpcError(const JsonRpcError& other) 15 | : m_code(other.m_code) 16 | , m_message(other.m_message) 17 | , m_data(other.m_data) 18 | { 19 | } 20 | 21 | JsonRpcError::~JsonRpcError() 22 | { 23 | } 24 | 25 | QString JsonRpcError::toString() const 26 | { 27 | return QString("%1 (%2)").arg(message()).arg(code()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_error.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | #include "json_rpc_result.h" 5 | 6 | #include 7 | #include 8 | 9 | namespace jcon { 10 | 11 | class JCON_API JsonRpcError : public JsonRpcResult 12 | { 13 | public: 14 | enum ErrorCodes { 15 | EC_ParseError = -32700, 16 | EC_InvalidRequest = -32600, 17 | EC_MethodNotFound = -32601, 18 | EC_InvalidParams = -32602, 19 | EC_InternalError = -32603 20 | }; 21 | 22 | JsonRpcError(int code = 0, 23 | const QString& message = "", 24 | const QVariant& data = QVariant()); 25 | 26 | JsonRpcError(const JsonRpcError& other); 27 | 28 | virtual ~JsonRpcError(); 29 | 30 | bool isSuccess() const override { return false; } 31 | QVariant result() const override { return QVariant(); } 32 | QString toString() const override; 33 | 34 | private: 35 | int code() const { return m_code; } 36 | QString message() const { return m_message; } 37 | QVariant data() const { return m_data; } 38 | 39 | int m_code; 40 | QString m_message; 41 | QVariant m_data; 42 | }; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_file_logger.cpp: -------------------------------------------------------------------------------- 1 | #include "json_rpc_file_logger.h" 2 | 3 | namespace jcon { 4 | 5 | JsonRpcFileLogger::JsonRpcFileLogger(const QString& filename) 6 | : m_file(filename) 7 | { 8 | m_file.open(QIODevice::WriteOnly); 9 | m_stream.setDevice(&m_file); 10 | } 11 | 12 | JsonRpcFileLogger::~JsonRpcFileLogger() 13 | { 14 | m_file.close(); 15 | } 16 | 17 | void JsonRpcFileLogger::logDebug(const QString& message) 18 | { 19 | m_stream << message << "\n"; 20 | } 21 | 22 | void JsonRpcFileLogger::logInfo(const QString& message) 23 | { 24 | m_stream << message << "\n"; 25 | } 26 | 27 | void JsonRpcFileLogger::logWarning(const QString& message) 28 | { 29 | m_stream << message << "\n"; 30 | } 31 | 32 | void JsonRpcFileLogger::logError(const QString& message) 33 | { 34 | m_stream << message << "\n"; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_file_logger.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "json_rpc_logger.h" 4 | #include "jcon.h" 5 | 6 | #include 7 | #include 8 | 9 | namespace jcon { 10 | 11 | class JCON_API JsonRpcFileLogger : public JsonRpcLogger 12 | { 13 | public: 14 | JsonRpcFileLogger(const QString& filename); 15 | virtual ~JsonRpcFileLogger(); 16 | 17 | void logDebug(const QString& message) override; 18 | void logInfo(const QString& message) override; 19 | void logWarning(const QString& message) override; 20 | void logError(const QString& message) override; 21 | 22 | private: 23 | QFile m_file; 24 | QTextStream m_stream; 25 | }; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_logger.cpp: -------------------------------------------------------------------------------- 1 | #include "json_rpc_logger.h" 2 | 3 | namespace jcon { 4 | 5 | JsonRpcLogger::JsonRpcLogger() 6 | { 7 | } 8 | 9 | JsonRpcLogger::~JsonRpcLogger() 10 | { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_logger.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | 5 | class QString; 6 | 7 | namespace jcon { 8 | 9 | class JCON_API JsonRpcLogger 10 | { 11 | public: 12 | JsonRpcLogger(); 13 | virtual ~JsonRpcLogger(); 14 | 15 | virtual void logDebug(const QString& message) = 0; 16 | virtual void logInfo(const QString& message) = 0; 17 | virtual void logWarning(const QString& message) = 0; 18 | virtual void logError(const QString& message) = 0; 19 | }; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_request.cpp: -------------------------------------------------------------------------------- 1 | #include "json_rpc_request.h" 2 | 3 | namespace jcon { 4 | 5 | JsonRpcRequest::JsonRpcRequest(QObject* parent, 6 | QString id, 7 | QDateTime timestamp) 8 | : QObject(parent) 9 | , m_id(id) 10 | , m_timestamp(timestamp) 11 | { 12 | } 13 | 14 | JsonRpcRequest::~JsonRpcRequest() 15 | { 16 | } 17 | 18 | QString JsonRpcRequest::id() const 19 | { 20 | return m_id; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_request.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | 5 | #include 6 | #include 7 | 8 | namespace jcon { 9 | 10 | class JCON_API JsonRpcRequest : public QObject 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | JsonRpcRequest(QObject* parent, 16 | QString id, 17 | QDateTime timestamp = QDateTime::currentDateTime()); 18 | virtual ~JsonRpcRequest(); 19 | 20 | QString id() const; 21 | 22 | signals: 23 | void result(const QVariant& result); 24 | void error(int code, const QString& message, const QVariant& data); 25 | 26 | private: 27 | QString m_id; 28 | QDateTime m_timestamp; 29 | }; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_result.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | 5 | #include 6 | #include 7 | 8 | namespace jcon { 9 | 10 | class JCON_API JsonRpcResult 11 | { 12 | public: 13 | virtual ~JsonRpcResult() {} 14 | 15 | operator bool() const { return isSuccess(); } 16 | 17 | virtual bool isSuccess() const = 0; 18 | virtual QVariant result() const = 0; 19 | virtual QString toString() const = 0; 20 | }; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_server.cpp: -------------------------------------------------------------------------------- 1 | #include "json_rpc_server.h" 2 | #include "jcon_assert.h" 3 | #include "json_rpc_endpoint.h" 4 | #include "json_rpc_error.h" 5 | #include "json_rpc_file_logger.h" 6 | #include "string_util.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace { 16 | QString logInvoke(const QMetaMethod& meta_method, 17 | const QVariantList& args, 18 | const QVariant& return_value); 19 | } 20 | 21 | namespace jcon { 22 | 23 | JsonRpcEndpoint* JsonRpcServer::sm_client_endpoint = nullptr; 24 | 25 | JsonRpcServer::JsonRpcServer(QObject* parent, 26 | std::shared_ptr logger) 27 | : QObject(parent) 28 | , m_logger(logger) 29 | , m_allowNotification(false) 30 | { 31 | if (!m_logger) { 32 | m_logger = std::make_shared("json_server_log.txt"); 33 | } 34 | } 35 | 36 | JsonRpcServer::~JsonRpcServer() 37 | { 38 | } 39 | 40 | void JsonRpcServer::registerServices(const QObjectList& services) 41 | { 42 | m_services.clear(); 43 | 44 | // unsolicited notification signature 45 | QByteArray signature = QMetaObject::normalizedSignature( 46 | "sendUnsolicitedNotification(QString,QVariant)"); 47 | 48 | for (auto s : services) { 49 | m_services[s] = ""; 50 | /* 51 | * If the server allows sending unsolicited notifications, 52 | * and the service emits sendUnsolicitedNotification(QString,QVariant) 53 | * .... add a queued connection 54 | */ 55 | if (m_allowNotification) { 56 | int index = s->metaObject()->indexOfSignal(signature); 57 | if (index != -1) 58 | connect(s, SIGNAL(sendUnsolicitedNotification(QString,QVariant)), 59 | this, SLOT(serviceNotificationReceived(QString,QVariant)), 60 | Qt::QueuedConnection); 61 | } 62 | } 63 | 64 | m_ns_separator = ""; 65 | } 66 | 67 | void JsonRpcServer::registerServices(const ServiceMap& services, 68 | const QString& ns_separator) 69 | { 70 | m_services = services; 71 | m_ns_separator = ns_separator; 72 | 73 | // unsolicited notification signature 74 | QByteArray signature = QMetaObject::normalizedSignature( 75 | "sendUnsolicitedNotification(QString,QVariant)"); 76 | 77 | /* 78 | * If the server allows sending unsolicited notifications, 79 | * and the service emits sendUnsolicitedNotification(QString,QVariant) 80 | * .... add a queued connection 81 | */ 82 | if (m_allowNotification) { 83 | for (auto it = m_services.begin(); it != m_services.end(); ++it) { 84 | QObject* s = it.key(); 85 | int index = s->metaObject()->indexOfSignal(signature); 86 | 87 | if (index != -1) 88 | connect(s, SIGNAL(sendUnsolicitedNotification(QString,QVariant)), 89 | this, SLOT(serviceNotificationReceived(QString,QVariant)), 90 | Qt::QueuedConnection); 91 | } 92 | } 93 | } 94 | 95 | void JsonRpcServer::enableSendNotification(bool enabled) 96 | { 97 | m_allowNotification = enabled; 98 | } 99 | 100 | JsonRpcEndpoint* JsonRpcServer::clientEndpoint() 101 | { 102 | return sm_client_endpoint; 103 | } 104 | 105 | void JsonRpcServer::jsonRequestReceived(const QJsonObject& request, 106 | QObject* socket) 107 | { 108 | JCON_ASSERT(request.value("jsonrpc").toString() == "2.0"); 109 | 110 | if (request.value("jsonrpc").toString() != "2.0") { 111 | logError("invalid protocol tag"); 112 | return; 113 | } 114 | 115 | sm_client_endpoint = findClient(socket); 116 | 117 | QString method_name = request.value("method").toString(); 118 | if (method_name.isEmpty()) { 119 | logError("no method present in request"); 120 | } 121 | 122 | QVariant params = request.value("params").toVariant(); 123 | QString request_id = request.value("id").toVariant().toString(); 124 | 125 | QVariant return_value; 126 | if (!dispatch(method_name, params, return_value)) { 127 | auto msg = QString("method '%1' not found, check name and " 128 | "parameter types ").arg(method_name); 129 | logError(msg); 130 | 131 | // send error response if request had valid ID 132 | if (request_id != InvalidRequestId) { 133 | QJsonDocument error = 134 | createErrorResponse(request_id, 135 | JsonRpcError::EC_MethodNotFound, 136 | msg); 137 | 138 | if (!sm_client_endpoint) { 139 | logError("invalid client socket, cannot send response"); 140 | return; 141 | } 142 | 143 | sm_client_endpoint->send(error); 144 | return; 145 | } 146 | } 147 | 148 | // send response if request had valid ID 149 | if (request_id != InvalidRequestId) { 150 | QJsonDocument response = createResponse(request_id, 151 | return_value, 152 | method_name); 153 | 154 | if (!sm_client_endpoint) { 155 | logError("invalid client socket, cannot send response"); 156 | return; 157 | } 158 | 159 | sm_client_endpoint->send(response); 160 | } 161 | } 162 | 163 | bool JsonRpcServer::dispatch(const QString& method_name, 164 | const QVariant& params, 165 | QVariant& return_value) 166 | { 167 | QString method_ns; 168 | QString method_name_without_ns; 169 | std::tie(method_ns, method_name_without_ns) = 170 | namespaceAndMethodName(method_name); 171 | 172 | QObjectList services; 173 | for (auto it = m_services.begin(); it != m_services.end(); ++it) { 174 | QObject* s = it.key(); 175 | QString ns = it.value(); 176 | if (ns.isEmpty() || ns == method_ns) { 177 | services.push_back(s); 178 | } 179 | } 180 | 181 | for (auto s : services) { 182 | const QMetaObject* meta_obj = s->metaObject(); 183 | for (int i = meta_obj->methodOffset(); 184 | i < meta_obj->methodCount(); 185 | ++i) 186 | { 187 | auto meta_method = meta_obj->method(i); 188 | if (meta_method.name() == method_name_without_ns) { 189 | if (params.type() == QVariant::List || 190 | params.type() == QVariant::StringList) 191 | { 192 | if (call(s, meta_method, params.toList(), return_value)) { 193 | return true; 194 | } 195 | } else if (params.type() == QVariant::Map) { 196 | if (call(s, meta_method, params.toMap(), return_value)) { 197 | return true; 198 | } 199 | } else if (params.type() == QVariant::Invalid) { 200 | if (call(s, meta_method, QVariantList(), return_value)) { 201 | return true; 202 | } 203 | } 204 | } 205 | } 206 | } 207 | return false; 208 | } 209 | 210 | std::pair 211 | JsonRpcServer::namespaceAndMethodName(const QString& full_name) 212 | { 213 | if (m_ns_separator.isEmpty()) { 214 | return {"", full_name}; 215 | } 216 | QString ns; 217 | QString method_name; 218 | int li = full_name.lastIndexOf(m_ns_separator); 219 | if (li > 0) { 220 | ns = full_name.left(li); 221 | method_name = full_name.mid(li + m_ns_separator.length()); 222 | } else { 223 | return {"", full_name}; 224 | } 225 | return {ns, method_name}; 226 | } 227 | 228 | bool JsonRpcServer::call(QObject* object, 229 | const QMetaMethod& meta_method, 230 | const QVariantList& args, 231 | QVariant& return_value) 232 | { 233 | return_value = QVariant(); 234 | 235 | QVariantList converted_args; 236 | if (!convertArgs(meta_method, args, converted_args)) { 237 | return false; 238 | } 239 | 240 | return doCall(object, meta_method, converted_args, return_value); 241 | } 242 | 243 | bool JsonRpcServer::call(QObject* object, 244 | const QMetaMethod& meta_method, 245 | const QVariantMap& args, 246 | QVariant& return_value) 247 | { 248 | return_value = QVariant(); 249 | 250 | QVariantList converted_args; 251 | if (!convertArgs(meta_method, args, converted_args)) { 252 | return false; 253 | } 254 | 255 | return doCall(object, meta_method, converted_args, return_value); 256 | } 257 | 258 | bool JsonRpcServer::convertArgs(const QMetaMethod& meta_method, 259 | const QVariantList& args, 260 | QVariantList& converted_args) 261 | { 262 | QList param_types = meta_method.parameterTypes(); 263 | if (args.size() != param_types.size()) { 264 | logError(QString("wrong number of arguments to method %1 -- " 265 | "expected %2 arguments, but got %3") 266 | .arg(QString(meta_method.methodSignature())) 267 | .arg(meta_method.parameterCount()) 268 | .arg(args.size())); 269 | return false; 270 | } 271 | 272 | for (int i = 0; i < param_types.size(); i++) { 273 | const QVariant& arg = args.at(i); 274 | if (!arg.isValid()) { 275 | logError(QString("argument %1 of %2 to method %3 is invalid") 276 | .arg(i + 1) 277 | .arg(param_types.size()) 278 | .arg(QString(meta_method.methodSignature()))); 279 | return false; 280 | } 281 | 282 | QByteArray arg_type_name = arg.typeName(); 283 | QByteArray param_type_name = param_types.at(i); 284 | 285 | QVariant::Type param_type = QVariant::nameToType(param_type_name); 286 | 287 | QVariant copy = QVariant(arg); 288 | 289 | if (copy.type() != param_type) { 290 | if (copy.canConvert(param_type)) { 291 | if (!copy.convert(param_type)) { 292 | // qDebug() << "cannot convert" << arg_type_name 293 | // << "to" << param_type_name; 294 | return false; 295 | } 296 | } 297 | } 298 | 299 | converted_args << copy; 300 | } 301 | return true; 302 | } 303 | 304 | bool JsonRpcServer::convertArgs(const QMetaMethod& meta_method, 305 | const QVariantMap& args, 306 | QVariantList& converted_args) 307 | { 308 | QList param_types = meta_method.parameterTypes(); 309 | if (args.size() != param_types.size()) { 310 | logError(QString("wrong number of arguments to method %1 -- " 311 | "expected %2 arguments, but got %3") 312 | .arg(QString(meta_method.methodSignature())) 313 | .arg(meta_method.parameterCount()) 314 | .arg(args.size())); 315 | return false; 316 | } 317 | 318 | for (int i = 0; i < param_types.size(); i++) { 319 | QByteArray param_name = meta_method.parameterNames().at(i); 320 | if (args.find(param_name) == args.end()) { 321 | // no arg with param name found 322 | return false; 323 | } 324 | const QVariant& arg = args.value(param_name); 325 | if (!arg.isValid()) { 326 | logError(QString("argument %1 of %2 to method %3 is invalid") 327 | .arg(i + 1) 328 | .arg(param_types.size()) 329 | .arg(QString(meta_method.methodSignature()))); 330 | return false; 331 | } 332 | 333 | QByteArray arg_type_name = arg.typeName(); 334 | QByteArray param_type_name = param_types.at(i); 335 | 336 | QVariant::Type param_type = QVariant::nameToType(param_type_name); 337 | 338 | QVariant copy = QVariant(arg); 339 | 340 | if (copy.type() != param_type) { 341 | if (copy.canConvert(param_type)) { 342 | if (!copy.convert(param_type)) { 343 | // qDebug() << "cannot convert" << arg_type_name 344 | // << "to" << param_type_name; 345 | return false; 346 | } 347 | } 348 | } 349 | 350 | converted_args << copy; 351 | } 352 | return true; 353 | } 354 | 355 | // based on https://gist.github.com/andref/2838534. 356 | bool JsonRpcServer::doCall(QObject* object, 357 | const QMetaMethod& meta_method, 358 | QVariantList& converted_args, 359 | QVariant& return_value) 360 | { 361 | QList arguments; 362 | 363 | for (int i = 0; i < converted_args.size(); i++) { 364 | 365 | // Notice that we have to take a reference to the argument, else we'd be 366 | // pointing to a copy that will be destroyed when this loop exits. 367 | QVariant& argument = converted_args[i]; 368 | 369 | // A const_cast is needed because calling data() would detach the 370 | // QVariant. 371 | QGenericArgument generic_argument( 372 | QMetaType::typeName(argument.userType()), 373 | const_cast(argument.constData()) 374 | ); 375 | 376 | arguments << generic_argument; 377 | } 378 | 379 | const char* return_type_name = meta_method.typeName(); 380 | QMetaType return_type = QMetaType::fromName(return_type_name); 381 | if (return_type.id() != QMetaType::Void) { 382 | return_value = QVariant(return_type, nullptr); 383 | } 384 | 385 | QGenericReturnArgument return_argument( 386 | return_type_name, 387 | const_cast(return_value.constData()) 388 | ); 389 | 390 | // perform the call 391 | bool ok = meta_method.invoke( 392 | object, 393 | Qt::DirectConnection, 394 | return_argument, 395 | arguments.value(0), 396 | arguments.value(1), 397 | arguments.value(2), 398 | arguments.value(3), 399 | arguments.value(4), 400 | arguments.value(5), 401 | arguments.value(6), 402 | arguments.value(7), 403 | arguments.value(8), 404 | arguments.value(9) 405 | ); 406 | 407 | if (!ok) { 408 | // qDebug() << "calling" << meta_method.methodSignature() << "failed."; 409 | return false; 410 | } 411 | 412 | logInfo(logInvoke(meta_method, converted_args, return_value)); 413 | 414 | return true; 415 | } 416 | 417 | QJsonDocument JsonRpcServer::createResponse(const QString& request_id, 418 | const QVariant& return_value, 419 | const QString& method_name) 420 | { 421 | QJsonObject res_json_obj { 422 | { "jsonrpc", "2.0" }, 423 | { "id", request_id } 424 | }; 425 | 426 | if (return_value.type() == QVariant::Invalid) { 427 | res_json_obj["result"] = QJsonValue(); 428 | } else if (return_value.type() == QVariant::List) { 429 | auto ret_doc = QJsonDocument::fromVariant(return_value); 430 | res_json_obj["result"] = ret_doc.array(); 431 | } else if (return_value.type() == QVariant::Map) { 432 | auto ret_doc = QJsonDocument::fromVariant(return_value); 433 | res_json_obj["result"] = ret_doc.object(); 434 | } else if (return_value.type() == QVariant::Int) { 435 | res_json_obj["result"] = return_value.toInt(); 436 | } else if (return_value.type() == QVariant::LongLong) { 437 | res_json_obj["result"] = return_value.toLongLong(); 438 | } else if (return_value.type() == QVariant::Double) { 439 | res_json_obj["result"] = return_value.toDouble(); 440 | } else if (return_value.type() == QVariant::Bool) { 441 | res_json_obj["result"] = return_value.toBool(); 442 | } else if (return_value.type() == QVariant::String) { 443 | res_json_obj["result"] = return_value.toString(); 444 | } else { 445 | auto msg = 446 | QString("method '%1' has unknown return type: %2") 447 | .arg(method_name) 448 | .arg(return_value.type()); 449 | logError(msg); 450 | return createErrorResponse(request_id, 451 | JsonRpcError::EC_InvalidRequest, 452 | msg); 453 | } 454 | 455 | return QJsonDocument(res_json_obj); 456 | } 457 | 458 | QJsonDocument JsonRpcServer::createErrorResponse(const QString& request_id, 459 | int code, 460 | const QString& message) 461 | { 462 | QJsonObject error_object { 463 | { "code", code }, 464 | { "message", message } 465 | }; 466 | 467 | QJsonObject res_json_obj { 468 | { "jsonrpc", "2.0" }, 469 | { "error", error_object }, 470 | { "id", request_id } 471 | }; 472 | return QJsonDocument(res_json_obj); 473 | } 474 | 475 | QJsonDocument JsonRpcServer::createNotification(const QString& key, 476 | const QVariant& value) 477 | { 478 | QJsonObject noti_json_obj { 479 | { "jsonrpc", "2.0" }, 480 | { "method", key } 481 | }; 482 | 483 | if (value.type() == QVariant::Invalid) { 484 | noti_json_obj["params"] = QJsonValue(); 485 | } else if (value.type() == QVariant::List) { 486 | auto ret_doc = QJsonDocument::fromVariant(value); 487 | noti_json_obj["params"] = ret_doc.array(); 488 | } else if (value.type() == QVariant::Map) { 489 | auto ret_doc = QJsonDocument::fromVariant(value); 490 | noti_json_obj["params"] = ret_doc.object(); 491 | } else if (value.type() == QVariant::Int) { 492 | noti_json_obj["params"] = value.toInt(); 493 | } else if (value.type() == QVariant::LongLong) { 494 | noti_json_obj["params"] = value.toLongLong(); 495 | } else if (value.type() == QVariant::Double) { 496 | noti_json_obj["params"] = value.toDouble(); 497 | } else if (value.type() == QVariant::Bool) { 498 | noti_json_obj["params"] = value.toBool(); 499 | } else if (value.type() == QVariant::String) { 500 | noti_json_obj["params"] = value.toString(); 501 | } else { 502 | auto msg = 503 | QString("unknown return type: %1") 504 | .arg(value.type()); 505 | logError(msg); 506 | return QJsonDocument(); 507 | } 508 | 509 | return QJsonDocument(noti_json_obj); 510 | } 511 | 512 | void JsonRpcServer::logInfo(const QString& msg) 513 | { 514 | m_logger->logInfo("JSON RPC server: " + msg); 515 | } 516 | 517 | void JsonRpcServer::logError(const QString& msg) 518 | { 519 | m_logger->logError("JSON RPC server error: " + msg); 520 | } 521 | 522 | void JsonRpcServer::serviceNotificationReceived(const QString& key, 523 | const QVariant& value) 524 | { 525 | if (key.isEmpty()) 526 | return; 527 | 528 | QJsonDocument response = createNotification(key, value); 529 | if (response.isNull()) 530 | return; 531 | 532 | for (JsonRpcEndpoint* endpoint : getAllClients()) 533 | if (endpoint != nullptr) 534 | endpoint->send(response); 535 | } 536 | 537 | } 538 | 539 | namespace { 540 | 541 | QString logInvoke(const QMetaMethod& meta_method, 542 | const QVariantList& args, 543 | const QVariant& return_value) 544 | { 545 | const auto ns = meta_method.parameterNames(); 546 | auto ps = jcon::variantListToStringList(args); 547 | QStringList args_sl; 548 | std::transform(ns.begin(), ns.end(), ps.begin(), 549 | std::back_inserter(args_sl), 550 | [](auto x, auto y) -> QString { 551 | return static_cast(x) + ": " + y; 552 | } 553 | ); 554 | 555 | auto msg = QString("%1 invoked ") 556 | .arg(static_cast(meta_method.name())); 557 | 558 | if (args_sl.empty()) { 559 | msg += "without arguments"; 560 | } else { 561 | msg += QString("with argument%1: %2") 562 | .arg(args_sl.size() == 1 ? "" : "s") 563 | .arg(args_sl.join(", ")); 564 | } 565 | 566 | if (return_value.isValid()) { 567 | msg += " -> returning: " + jcon::variantToString(return_value); 568 | } 569 | 570 | return msg; 571 | } 572 | 573 | } 574 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_server.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | #include "json_rpc_logger.h" 5 | 6 | #include 7 | 8 | #include 9 | 10 | namespace jcon { 11 | 12 | class JsonRpcEndpoint; 13 | class JsonRpcSocket; 14 | 15 | class JCON_API JsonRpcServer : public QObject 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | using ServiceMap = QMap; 21 | 22 | JsonRpcServer(QObject* parent = nullptr, 23 | std::shared_ptr logger = nullptr); 24 | virtual ~JsonRpcServer(); 25 | 26 | /** 27 | * Register services containing RPC method invocation handlers. 28 | * 29 | * @param[in] services A list of services to register. 30 | */ 31 | void registerServices(const QObjectList& services); 32 | 33 | /** 34 | * Register namespaced services containing RPC method invocation handlers. 35 | * 36 | * @param[in] services A map of (service, namespace) pairs to register. 37 | * @param[in] ns_separator String that is used to separate namespaces from 38 | * method name. 39 | */ 40 | void registerServices(const ServiceMap& services, 41 | const QString& ns_separator = "/"); 42 | 43 | // Allow a server to send unsolicited notifications to client 44 | void enableSendNotification(bool enabled); 45 | 46 | virtual bool listen(int port) = 0; 47 | virtual bool listen(const QHostAddress& addr, int port) = 0; 48 | 49 | virtual void close() = 0; 50 | 51 | /** 52 | * Get the currently calling client endpoint. 53 | */ 54 | static JsonRpcEndpoint* clientEndpoint(); 55 | 56 | protected: 57 | virtual JsonRpcEndpoint* findClient(QObject* socket) = 0; 58 | virtual QVector getAllClients() = 0; 59 | 60 | signals: 61 | void clientConnected(QObject* client_socket); 62 | void clientDisconnected(QObject* client_socket); 63 | 64 | void socketError(QObject* socket, QAbstractSocket::SocketError error); 65 | 66 | public slots: 67 | void jsonRequestReceived(const QJsonObject& request, QObject* socket); 68 | 69 | protected slots: 70 | virtual void newConnection() = 0; 71 | virtual void disconnectClient(QObject* client_socket) = 0; 72 | 73 | protected: 74 | void logInfo(const QString& msg); 75 | void logError(const QString& msg); 76 | std::shared_ptr log() { return m_logger; } 77 | 78 | private slots: 79 | void serviceNotificationReceived(const QString& key, const QVariant& value); 80 | 81 | private: 82 | const QString InvalidRequestId = ""; 83 | 84 | bool dispatch(const QString& method_name, 85 | const QVariant& params, 86 | QVariant& return_value); 87 | 88 | std::pair 89 | namespaceAndMethodName(const QString& full_name); 90 | 91 | bool call(QObject* object, 92 | const QMetaMethod& meta_method, 93 | const QVariantList& args, 94 | QVariant& return_value); 95 | 96 | bool call(QObject* object, 97 | const QMetaMethod& meta_method, 98 | const QVariantMap& args, 99 | QVariant& return_value); 100 | 101 | bool convertArgs(const QMetaMethod& meta_method, 102 | const QVariantList& args, 103 | QVariantList& converted); 104 | 105 | bool convertArgs(const QMetaMethod& meta_method, 106 | const QVariantMap& args, 107 | QVariantList& converted); 108 | 109 | bool doCall(QObject* object, 110 | const QMetaMethod& meta_method, 111 | QVariantList& converted_args, 112 | QVariant& return_value); 113 | 114 | QJsonDocument createResponse(const QString& request_id, 115 | const QVariant& return_value, 116 | const QString& method_name); 117 | QJsonDocument createErrorResponse(const QString& request_id, 118 | int code, 119 | const QString& message); 120 | QJsonDocument createNotification(const QString& key, 121 | const QVariant& value); 122 | 123 | std::shared_ptr m_logger; 124 | ServiceMap m_services; 125 | QString m_ns_separator; 126 | 127 | // allows unsolicited notifications (not part of JSON-RPC standard) 128 | bool m_allowNotification; 129 | 130 | static JsonRpcEndpoint* sm_client_endpoint; 131 | }; 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_socket.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | 5 | #include 6 | #include 7 | 8 | namespace jcon { 9 | 10 | class JCON_API JsonRpcSocket : public QObject 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | JsonRpcSocket() {} 16 | virtual ~JsonRpcSocket() {} 17 | 18 | virtual void connectToHost(const QString& host, int port) = 0; 19 | virtual void connectToUrl(const QUrl& url) = 0; 20 | virtual bool waitForConnected(int msecs = 30000) = 0; 21 | virtual void disconnectFromHost() = 0; 22 | virtual bool isConnected() const = 0; 23 | virtual size_t send(const QByteArray& data) = 0; 24 | virtual QString errorString() const = 0; 25 | virtual QHostAddress localAddress() const = 0; 26 | virtual int localPort() const = 0; 27 | virtual QHostAddress peerAddress() const = 0; 28 | virtual int peerPort() const = 0; 29 | 30 | signals: 31 | void dataReceived(const QByteArray& bytes, QObject* socket); 32 | void socketConnected(QObject* socket); 33 | void socketDisconnected(QObject* socket); 34 | void socketError(QObject* socket, QAbstractSocket::SocketError error); 35 | }; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_success.cpp: -------------------------------------------------------------------------------- 1 | #include "json_rpc_success.h" 2 | 3 | namespace jcon { 4 | 5 | JsonRpcSuccess::JsonRpcSuccess(QVariant result) : m_result(result) 6 | { 7 | } 8 | 9 | JsonRpcSuccess::~JsonRpcSuccess() 10 | { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_success.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | #include "json_rpc_result.h" 5 | 6 | #include 7 | 8 | namespace jcon { 9 | 10 | class JsonRpcSuccess : public JsonRpcResult 11 | { 12 | public: 13 | JsonRpcSuccess(QVariant result); 14 | virtual ~JsonRpcSuccess(); 15 | 16 | bool isSuccess() const override { return true; } 17 | QVariant result() const override { return m_result; } 18 | QString toString() const override { return m_result.toString(); } 19 | 20 | private: 21 | QVariant m_result; 22 | }; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_tcp_client.cpp: -------------------------------------------------------------------------------- 1 | #include "json_rpc_tcp_client.h" 2 | #include "json_rpc_tcp_socket.h" 3 | 4 | #include 5 | 6 | namespace jcon { 7 | 8 | JsonRpcTcpClient::JsonRpcTcpClient(QObject* parent, 9 | std::shared_ptr logger, 10 | int call_timeout_ms) 11 | : JsonRpcClient(std::make_shared(), 12 | parent, logger, call_timeout_ms) 13 | { 14 | } 15 | 16 | JsonRpcTcpClient::~JsonRpcTcpClient() 17 | { 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_tcp_client.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "json_rpc_client.h" 4 | 5 | namespace jcon { 6 | 7 | class JCON_API JsonRpcTcpClient : public JsonRpcClient 8 | { 9 | Q_OBJECT 10 | 11 | public: 12 | JsonRpcTcpClient(QObject* parent = nullptr, 13 | std::shared_ptr logger = nullptr, 14 | int call_timeout_ms = 60000); 15 | 16 | virtual ~JsonRpcTcpClient(); 17 | }; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_tcp_server.cpp: -------------------------------------------------------------------------------- 1 | #include "json_rpc_tcp_server.h" 2 | #include "json_rpc_tcp_socket.h" 3 | #include "jcon_assert.h" 4 | 5 | namespace jcon { 6 | 7 | JsonRpcTcpServer::JsonRpcTcpServer(QObject* parent, 8 | std::shared_ptr logger) 9 | : JsonRpcServer(parent, logger) 10 | , m_server(this) 11 | { 12 | m_server.connect(&m_server, &QTcpServer::newConnection, 13 | this, &JsonRpcTcpServer::newConnection); 14 | } 15 | 16 | JsonRpcTcpServer::~JsonRpcTcpServer() 17 | { 18 | m_server.disconnect(this); 19 | close(); 20 | } 21 | 22 | bool JsonRpcTcpServer::listen(int port) 23 | { 24 | logInfo(QString("listening on port %1").arg(port)); 25 | if (!m_server.listen(QHostAddress::AnyIPv4, port)) { 26 | auto msg = QString("Error listening on port %1").arg(port); 27 | logError(qPrintable(msg)); 28 | return false; 29 | } 30 | return true; 31 | } 32 | 33 | bool JsonRpcTcpServer::listen(const QHostAddress& addr, int port) 34 | { 35 | logInfo(QString("listening on port %1").arg(port)); 36 | if (!m_server.listen(addr, port)) { 37 | auto msg = QString("Error listening on %1:%2") 38 | .arg(addr.toString()).arg(port); 39 | logError(qPrintable(msg)); 40 | return false; 41 | } 42 | return true; 43 | } 44 | 45 | void JsonRpcTcpServer::close() 46 | { 47 | m_server.close(); 48 | } 49 | 50 | JsonRpcEndpoint* JsonRpcTcpServer::findClient(QObject* socket) 51 | { 52 | QTcpSocket* tcp_socket = qobject_cast(socket); 53 | auto it = m_client_endpoints.find(tcp_socket); 54 | return (it != m_client_endpoints.end()) ? it->second.get() : nullptr; 55 | } 56 | 57 | QVector JsonRpcTcpServer::getAllClients() 58 | { 59 | int size = static_cast(m_client_endpoints.size()); 60 | QVector rpc_endpoints(size, nullptr); 61 | 62 | for (auto const& client_endpoint : m_client_endpoints) 63 | rpc_endpoints.append(client_endpoint.second.get()); 64 | 65 | return rpc_endpoints; 66 | } 67 | 68 | void JsonRpcTcpServer::newConnection() 69 | { 70 | JCON_ASSERT(m_server.hasPendingConnections()); 71 | if (m_server.hasPendingConnections()) { 72 | QTcpSocket* tcp_socket = m_server.nextPendingConnection(); 73 | JCON_ASSERT(m_server.nextPendingConnection() == nullptr); 74 | 75 | JCON_ASSERT(tcp_socket); 76 | if (!tcp_socket) { 77 | logError("pending socket was null"); 78 | return; 79 | } 80 | 81 | logInfo("client connected: " + tcp_socket->peerAddress().toString()); 82 | 83 | // TODO: maybe move this to base class? 84 | // { 85 | auto rpc_socket = std::make_shared(tcp_socket); 86 | 87 | auto endpoint = 88 | std::shared_ptr(new JsonRpcEndpoint(rpc_socket, log(), this), 89 | [this](JsonRpcEndpoint* obj) { 90 | if (this->m_server.isListening()) { 91 | obj->deleteLater(); 92 | } else { 93 | delete obj; 94 | } 95 | }); 96 | 97 | connect(endpoint.get(), &JsonRpcEndpoint::socketDisconnected, 98 | this, &JsonRpcTcpServer::disconnectClient); 99 | 100 | connect(endpoint.get(), &JsonRpcEndpoint::socketError, 101 | this, &JsonRpcServer::socketError); 102 | 103 | connect(endpoint.get(), &JsonRpcEndpoint::jsonObjectReceived, 104 | this, &JsonRpcServer::jsonRequestReceived); 105 | 106 | m_client_endpoints[tcp_socket] = endpoint; 107 | 108 | emit(clientConnected(tcp_socket)); 109 | // } 110 | } 111 | } 112 | 113 | void JsonRpcTcpServer::disconnectClient(QObject* client_socket) 114 | { 115 | QTcpSocket* tcp_socket = qobject_cast(client_socket); 116 | JCON_ASSERT(tcp_socket); 117 | if (!tcp_socket) { 118 | logError("client disconnected, but socket is null"); 119 | return; 120 | } 121 | 122 | logInfo("client disconnected: " + tcp_socket->peerAddress().toString()); 123 | auto it = m_client_endpoints.find(tcp_socket); 124 | JCON_ASSERT(it != m_client_endpoints.end()); 125 | if (it == m_client_endpoints.end()) { 126 | logError("unknown client disconnected"); 127 | return; 128 | } 129 | m_client_endpoints.erase(it); 130 | emit(clientDisconnected(client_socket)); 131 | } 132 | 133 | bool jcon::JsonRpcTcpServer::isListening() const { 134 | return m_server.isListening(); 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_tcp_server.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | #include "json_rpc_server.h" 5 | #include "json_rpc_endpoint.h" 6 | #include "json_rpc_socket.h" 7 | 8 | #include 9 | 10 | #include 11 | 12 | namespace jcon { 13 | 14 | class JCON_API JsonRpcTcpServer : public JsonRpcServer 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | JsonRpcTcpServer(QObject* parent = nullptr, 20 | std::shared_ptr logger = nullptr); 21 | virtual ~JsonRpcTcpServer(); 22 | 23 | bool listen(int port) override; 24 | bool listen(const QHostAddress& addr, int port) override; 25 | bool isListening() const; 26 | void close() override; 27 | 28 | protected: 29 | JsonRpcEndpoint* findClient(QObject* socket) override; 30 | QVector getAllClients() override; 31 | 32 | private slots: 33 | /// Called when the underlying QTcpServer gets a new client connection. 34 | void newConnection() override; 35 | 36 | /// Called when the underlying QTcpServer loses a client connection. 37 | void disconnectClient(QObject* client_socket) override; 38 | 39 | private: 40 | QTcpServer m_server; 41 | 42 | /// Clients are uniquely identified by their QTcpSocket*. 43 | std::map> m_client_endpoints; 44 | }; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_tcp_socket.cpp: -------------------------------------------------------------------------------- 1 | #include "json_rpc_tcp_socket.h" 2 | #include "jcon_assert.h" 3 | 4 | #include 5 | 6 | namespace jcon { 7 | 8 | JsonRpcTcpSocket::JsonRpcTcpSocket() 9 | : m_socket(new QTcpSocket(this)) 10 | { 11 | setupSocket(); 12 | } 13 | 14 | JsonRpcTcpSocket::JsonRpcTcpSocket(QTcpSocket* socket) 15 | : m_socket(socket) 16 | { 17 | setupSocket(); 18 | } 19 | 20 | JsonRpcTcpSocket::~JsonRpcTcpSocket() 21 | { 22 | m_socket->disconnect(this); 23 | } 24 | 25 | void JsonRpcTcpSocket::setupSocket() 26 | { 27 | m_socket->setSocketOption(QAbstractSocket::LowDelayOption, "1"); 28 | m_socket->setSocketOption(QAbstractSocket::KeepAliveOption, "1"); 29 | 30 | connect(m_socket, &QTcpSocket::connected, this, [this]() { 31 | emit socketConnected(m_socket); 32 | }); 33 | 34 | connect(m_socket, &QTcpSocket::disconnected, this, [this]() { 35 | emit socketDisconnected(m_socket); 36 | }); 37 | 38 | connect(m_socket, &QTcpSocket::readyRead, 39 | this, &JsonRpcTcpSocket::dataReady); 40 | 41 | #if (QT_VERSION > QT_VERSION_CHECK(5, 15, 0)) 42 | void (QAbstractSocket::*errorFun)(QAbstractSocket::SocketError) = 43 | &QAbstractSocket::errorOccurred; 44 | #else 45 | void (QAbstractSocket::*errorFun)(QAbstractSocket::SocketError) = 46 | &QAbstractSocket::error; 47 | #endif 48 | 49 | connect(m_socket, errorFun, this, 50 | [this](QAbstractSocket::SocketError error) { 51 | emit socketError(m_socket, error); 52 | }); 53 | } 54 | 55 | void JsonRpcTcpSocket::connectToHost(const QString& host, int port) 56 | { 57 | m_socket->connectToHost(host, port, 58 | QIODevice::ReadWrite, 59 | QAbstractSocket::IPv4Protocol); 60 | } 61 | 62 | void JsonRpcTcpSocket::connectToUrl(const QUrl& url) 63 | { 64 | connectToHost(url.host(), url.port()); 65 | } 66 | 67 | bool JsonRpcTcpSocket::waitForConnected(int msecs) 68 | { 69 | return m_socket->waitForConnected(msecs); 70 | } 71 | 72 | void JsonRpcTcpSocket::disconnectFromHost() 73 | { 74 | m_socket->disconnectFromHost(); 75 | m_socket->close(); 76 | } 77 | 78 | bool JsonRpcTcpSocket::isConnected() const 79 | { 80 | return m_socket->state() == QAbstractSocket::ConnectedState; 81 | } 82 | 83 | size_t JsonRpcTcpSocket::send(const QByteArray& data) 84 | { 85 | auto sz = m_socket->write(data); 86 | m_socket->flush(); 87 | return sz; 88 | } 89 | 90 | QString JsonRpcTcpSocket::errorString() const 91 | { 92 | return m_socket->errorString(); 93 | } 94 | 95 | QHostAddress JsonRpcTcpSocket::localAddress() const 96 | { 97 | return m_socket->localAddress(); 98 | } 99 | 100 | int JsonRpcTcpSocket::localPort() const 101 | { 102 | return m_socket->localPort(); 103 | } 104 | 105 | QHostAddress JsonRpcTcpSocket::peerAddress() const 106 | { 107 | return m_socket->peerAddress(); 108 | } 109 | 110 | int JsonRpcTcpSocket::peerPort() const 111 | { 112 | return m_socket->peerPort(); 113 | } 114 | 115 | void JsonRpcTcpSocket::dataReady() 116 | { 117 | JCON_ASSERT(m_socket->bytesAvailable() > 0); 118 | QByteArray bytes = m_socket->read(m_socket->bytesAvailable()); 119 | emit dataReceived(bytes, m_socket); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_tcp_socket.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | #include "json_rpc_socket.h" 5 | 6 | #include 7 | 8 | namespace jcon { 9 | 10 | class JCON_API JsonRpcTcpSocket : public JsonRpcSocket 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | /** 16 | * Default constructor. Create a new QTcpSocket. 17 | */ 18 | JsonRpcTcpSocket(); 19 | 20 | /** 21 | * Constructor taking a previously created socket. This is used by 22 | * JsonRpcServer, since QTcpServer::nextPendingConnection() returns an 23 | * already created socket for the client connection. 24 | * 25 | * @param[in] socket The TCP socket to use. 26 | */ 27 | JsonRpcTcpSocket(QTcpSocket* socket); 28 | 29 | virtual ~JsonRpcTcpSocket(); 30 | 31 | void connectToHost(const QString& host, int port) override; 32 | void connectToUrl(const QUrl& url) override; 33 | bool waitForConnected(int msecs) override; 34 | void disconnectFromHost() override; 35 | bool isConnected() const override; 36 | size_t send(const QByteArray& data) override; 37 | QString errorString() const override; 38 | QHostAddress localAddress() const override; 39 | int localPort() const override; 40 | QHostAddress peerAddress() const override; 41 | int peerPort() const override; 42 | 43 | private slots: 44 | void dataReady(); 45 | 46 | private: 47 | void setupSocket(); 48 | 49 | QTcpSocket* m_socket; 50 | }; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_websocket.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "json_rpc_websocket.h" 4 | #include "jcon_assert.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace jcon { 13 | 14 | JsonRpcWebSocket::JsonRpcWebSocket() 15 | : m_socket(new QWebSocket) 16 | { 17 | setupSocket(); 18 | } 19 | 20 | JsonRpcWebSocket::JsonRpcWebSocket(QWebSocket* socket) 21 | : m_socket(socket) 22 | { 23 | setupSocket(); 24 | } 25 | 26 | JsonRpcWebSocket::~JsonRpcWebSocket() 27 | { 28 | m_socket->disconnect(this); 29 | m_socket->deleteLater(); 30 | } 31 | 32 | void JsonRpcWebSocket::setupSocket() 33 | { 34 | connect(m_socket, &QWebSocket::connected, this, [this]() { 35 | emit socketConnected(m_socket); 36 | }); 37 | 38 | connect(m_socket, &QWebSocket::disconnected, this, [this]() { 39 | emit socketDisconnected(m_socket); 40 | }); 41 | 42 | connect(m_socket, &QWebSocket::textMessageReceived, 43 | this, &JsonRpcWebSocket::dataReady); 44 | 45 | void (QWebSocket::*errorPtr)(QAbstractSocket::SocketError) = 46 | &QWebSocket::errorOccurred; 47 | connect(m_socket, errorPtr, this, 48 | [this](QAbstractSocket::SocketError error) { 49 | emit socketError(m_socket, error); 50 | }); 51 | } 52 | 53 | void JsonRpcWebSocket::connectToHost(const QString& host, int port) 54 | { 55 | QUrl url; 56 | url.setScheme("ws"); 57 | url.setHost(host); 58 | url.setPort(port); 59 | m_socket->open(url); 60 | } 61 | 62 | void JsonRpcWebSocket::connectToUrl(const QUrl& url) 63 | { 64 | m_socket->open(url); 65 | } 66 | 67 | bool JsonRpcWebSocket::waitForConnected(int msecs) 68 | { 69 | QElapsedTimer timer; 70 | bool isConnected = false; 71 | QObject guard; 72 | connect(m_socket, &QWebSocket::connected, 73 | &guard, [&isConnected]() { 74 | isConnected = true; 75 | }); 76 | timer.start(); 77 | while (!isConnected && timer.elapsed() < msecs) { 78 | QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); 79 | } 80 | return isConnected; 81 | } 82 | 83 | void JsonRpcWebSocket::disconnectFromHost() 84 | { 85 | m_socket->close(); 86 | } 87 | 88 | bool JsonRpcWebSocket::isConnected() const 89 | { 90 | return m_socket->state() == QAbstractSocket::ConnectedState; 91 | } 92 | 93 | size_t JsonRpcWebSocket::send(const QByteArray& data) 94 | { 95 | return m_socket->sendTextMessage(data); 96 | } 97 | 98 | QString JsonRpcWebSocket::errorString() const 99 | { 100 | return m_socket->errorString(); 101 | } 102 | 103 | QHostAddress JsonRpcWebSocket::localAddress() const 104 | { 105 | return m_socket->localAddress(); 106 | } 107 | 108 | int JsonRpcWebSocket::localPort() const 109 | { 110 | return m_socket->localPort(); 111 | } 112 | 113 | QHostAddress JsonRpcWebSocket::peerAddress() const 114 | { 115 | return m_socket->peerAddress(); 116 | } 117 | 118 | int JsonRpcWebSocket::peerPort() const 119 | { 120 | return m_socket->peerPort(); 121 | } 122 | 123 | void JsonRpcWebSocket::dataReady(const QString& data) 124 | { 125 | emit dataReceived(data.toUtf8(), m_socket); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_websocket.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | #include "json_rpc_socket.h" 5 | 6 | class QWebSocket; 7 | 8 | namespace jcon { 9 | 10 | class JCON_API JsonRpcWebSocket : public JsonRpcSocket 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | /** 16 | * Default constructor. Create a new QWebSocket. 17 | */ 18 | JsonRpcWebSocket(); 19 | 20 | /** 21 | * Constructor taking a previously created socket. This is used by 22 | * JsonRpcServer, since QWebSocketServer::nextPendingConnection() returns 23 | * an already created socket for the client connection. 24 | * 25 | * @param[in] socket The WebSocket to use. 26 | */ 27 | JsonRpcWebSocket(QWebSocket* socket); 28 | 29 | virtual ~JsonRpcWebSocket(); 30 | 31 | void connectToHost(const QString& host, int port) override; 32 | void connectToUrl(const QUrl& url) override; 33 | bool waitForConnected(int msecs) override; 34 | void disconnectFromHost() override; 35 | bool isConnected() const override; 36 | size_t send(const QByteArray& data) override; 37 | QString errorString() const override; 38 | QHostAddress localAddress() const override; 39 | int localPort() const override; 40 | QHostAddress peerAddress() const override; 41 | int peerPort() const override; 42 | 43 | private slots: 44 | void dataReady(const QString& data); 45 | 46 | private: 47 | void setupSocket(); 48 | 49 | QWebSocket* m_socket; 50 | }; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_websocket_client.cpp: -------------------------------------------------------------------------------- 1 | #include "json_rpc_websocket_client.h" 2 | #include "json_rpc_websocket.h" 3 | 4 | #include 5 | 6 | namespace jcon { 7 | 8 | JsonRpcWebSocketClient::JsonRpcWebSocketClient( 9 | QObject* parent, 10 | std::shared_ptr logger, 11 | int call_timeout_ms) 12 | 13 | : JsonRpcClient(std::make_shared(), 14 | parent, logger, call_timeout_ms) 15 | { 16 | } 17 | 18 | JsonRpcWebSocketClient::~JsonRpcWebSocketClient() 19 | { 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_websocket_client.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "json_rpc_client.h" 4 | 5 | namespace jcon { 6 | 7 | class JCON_API JsonRpcWebSocketClient : public JsonRpcClient 8 | { 9 | Q_OBJECT 10 | 11 | public: 12 | JsonRpcWebSocketClient(QObject* parent = nullptr, 13 | std::shared_ptr logger = nullptr, 14 | int call_timeout_ms = 60000); 15 | 16 | virtual ~JsonRpcWebSocketClient(); 17 | }; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_websocket_server.cpp: -------------------------------------------------------------------------------- 1 | #include "json_rpc_websocket_server.h" 2 | #include "json_rpc_websocket.h" 3 | #include "jcon_assert.h" 4 | 5 | #include 6 | #include 7 | 8 | namespace jcon { 9 | 10 | JsonRpcWebSocketServer::JsonRpcWebSocketServer( 11 | QObject* parent, 12 | std::shared_ptr logger) 13 | : JsonRpcServer(parent, logger) 14 | , m_server(new QWebSocketServer("JSON RPC WebSocket server", 15 | QWebSocketServer::NonSecureMode, 16 | this)) 17 | { 18 | m_server->connect(m_server, &QWebSocketServer::newConnection, 19 | this, &JsonRpcWebSocketServer::newConnection); 20 | } 21 | 22 | JsonRpcWebSocketServer::~JsonRpcWebSocketServer() 23 | { 24 | m_server->disconnect(this); 25 | close(); 26 | delete m_server; 27 | m_server = nullptr; 28 | } 29 | 30 | bool JsonRpcWebSocketServer::listen(int port) 31 | { 32 | logInfo(QString("listening on port %2").arg(port)); 33 | return m_server->listen(QHostAddress::AnyIPv4, port); 34 | } 35 | 36 | 37 | bool JsonRpcWebSocketServer::listen(const QHostAddress& addr, int port) 38 | { 39 | logInfo(QString("listening on port %2").arg(port)); 40 | return m_server->listen(addr, port); 41 | } 42 | 43 | bool JsonRpcWebSocketServer::isListening() const { 44 | return m_server->isListening(); 45 | } 46 | 47 | void JsonRpcWebSocketServer::close() 48 | { 49 | m_server->close(); 50 | } 51 | 52 | JsonRpcEndpoint* JsonRpcWebSocketServer::findClient(QObject* socket) 53 | { 54 | QWebSocket* web_socket = qobject_cast(socket); 55 | auto it = m_client_endpoints.find(web_socket); 56 | return (it != m_client_endpoints.end()) ? it->second.get() : nullptr; 57 | } 58 | 59 | QVector JsonRpcWebSocketServer::getAllClients() 60 | { 61 | int size = static_cast(m_client_endpoints.size()); 62 | QVector rpc_endpoints(size, nullptr); 63 | 64 | for (auto const& client_endpoint : m_client_endpoints) 65 | rpc_endpoints.append(client_endpoint.second.get()); 66 | 67 | return rpc_endpoints; 68 | } 69 | 70 | void JsonRpcWebSocketServer::newConnection() 71 | { 72 | JCON_ASSERT(m_server->hasPendingConnections()); 73 | if (m_server->hasPendingConnections()) { 74 | QWebSocket* web_socket = m_server->nextPendingConnection(); 75 | 76 | JCON_ASSERT(web_socket); 77 | if (!web_socket) { 78 | logError("pending socket was null"); 79 | return; 80 | } 81 | 82 | logInfo("client connected: " + web_socket->peerAddress().toString()); 83 | 84 | // TODO: maybe move this to base class? 85 | // { 86 | auto rpc_socket = std::make_shared(web_socket); 87 | 88 | auto endpoint = 89 | std::shared_ptr(new JsonRpcEndpoint(rpc_socket, log(), this), 90 | [this](JsonRpcEndpoint* obj) { 91 | if (this->m_server != nullptr && this->m_server->isListening()) { 92 | obj->deleteLater(); 93 | } else { 94 | delete obj; 95 | } 96 | }); 97 | 98 | connect(endpoint.get(), &JsonRpcEndpoint::socketDisconnected, 99 | this, &JsonRpcWebSocketServer::disconnectClient); 100 | 101 | connect(endpoint.get(), &JsonRpcEndpoint::jsonObjectReceived, 102 | this, &JsonRpcServer::jsonRequestReceived); 103 | 104 | m_client_endpoints[web_socket] = endpoint; 105 | emit(clientConnected(web_socket)); 106 | // } 107 | } 108 | } 109 | 110 | void JsonRpcWebSocketServer::disconnectClient(QObject* client_socket) 111 | { 112 | QWebSocket* socket = qobject_cast(client_socket); 113 | JCON_ASSERT(socket); 114 | if (!socket) { 115 | logError("client disconnected, but socket is null"); 116 | return; 117 | } 118 | 119 | logInfo("client disconnected: " + socket->peerAddress().toString()); 120 | auto it = m_client_endpoints.find(socket); 121 | JCON_ASSERT(it != m_client_endpoints.end()); 122 | if (it == m_client_endpoints.end()) { 123 | logError("unknown client disconnected"); 124 | return; 125 | } 126 | m_client_endpoints.erase(it); 127 | emit(clientDisconnected(client_socket)); 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/jcon/json_rpc_websocket_server.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | #include "json_rpc_server.h" 5 | #include "json_rpc_endpoint.h" 6 | #include "json_rpc_socket.h" 7 | 8 | #include 9 | 10 | class QWebSocket; 11 | class QWebSocketServer; 12 | 13 | namespace jcon { 14 | 15 | class JCON_API JsonRpcWebSocketServer : public JsonRpcServer 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | JsonRpcWebSocketServer(QObject* parent = nullptr, 21 | std::shared_ptr logger = nullptr); 22 | virtual ~JsonRpcWebSocketServer(); 23 | 24 | bool listen(int port) override; 25 | bool listen(const QHostAddress& addr, int port) override; 26 | bool isListening() const; 27 | void close() override; 28 | 29 | protected: 30 | JsonRpcEndpoint* findClient(QObject* socket) override; 31 | QVector getAllClients() override; 32 | 33 | private slots: 34 | /// Called when the underlying QWebSocketServer gets a new client 35 | /// connection. 36 | void newConnection() override; 37 | 38 | /// Called when the underlying QWebSocketServer loses a client connection. 39 | void disconnectClient(QObject* client_socket) override; 40 | 41 | private: 42 | QWebSocketServer* m_server; 43 | 44 | /// Clients are uniquely identified by their QWebSocket*. 45 | std::map> m_client_endpoints; 46 | }; 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/jcon/string_util.cpp: -------------------------------------------------------------------------------- 1 | #include "string_util.h" 2 | 3 | #include 4 | 5 | namespace jcon { 6 | 7 | QString variantToString(const QVariant& v) 8 | { 9 | if (v.type() == QVariant::List) { 10 | return QString("list (%1 elements)").arg(v.toList().size()); 11 | } 12 | if (v.type() == QVariant::Map) { 13 | return QString("map (%1 elements)").arg(v.toMap().size()); 14 | } 15 | if (v.canConvert()) { 16 | return v.toString(); 17 | } 18 | 19 | return ("N/A"); 20 | } 21 | 22 | QStringList variantListToStringList(const QVariantList& l) 23 | { 24 | QStringList res; 25 | std::transform(l.begin(), l.end(), std::back_inserter(res), 26 | [](const QVariant& v) { 27 | return variantToString(v); 28 | }); 29 | return res; 30 | } 31 | 32 | QStringList variantMapToStringList(const QVariantMap& m) 33 | { 34 | QStringList res; 35 | for (auto k : m.keys()) { 36 | res.push_back(variantToString(m.value(k))); 37 | } 38 | return res; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/jcon/string_util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "jcon.h" 4 | 5 | #include 6 | 7 | namespace jcon { 8 | 9 | /** 10 | * Convert a QVariant to a QString, with non-empty representations of 11 | * QVariantMaps and QVariantLists. Useful for logging. 12 | * 13 | * @param[in] v The QVariant to convert 14 | * 15 | * @return A string representation of the QVariant. 16 | */ 17 | JCON_API QString variantToString(const QVariant& v); 18 | 19 | /** 20 | * Convert a QVariantList to a QStringList 21 | * 22 | * @param[in] l The QVariantList to convert 23 | * 24 | * @return A list with the string representations of each element in the list. 25 | */ 26 | JCON_API QStringList variantListToStringList(const QVariantList& l); 27 | 28 | /** 29 | * Convert a QVariantMap to a QStringList 30 | * 31 | * @param[in] m The QVariantMap to convert 32 | * 33 | * @return A list with the string representations of each element in the list. 34 | */ 35 | JCON_API QStringList variantMapToStringList(const QVariantMap& m); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "example_service.h" 2 | #include "other_service.h" 3 | #include "notification_service.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | enum class SocketType {tcp, websocket}; 18 | 19 | jcon::JsonRpcServer* startServer(QObject* parent, 20 | SocketType socket_type = SocketType::tcp, 21 | bool allow_notifications = false) 22 | { 23 | jcon::JsonRpcServer* rpc_server; 24 | if (socket_type == SocketType::tcp) { 25 | qDebug() << "Starting TCP server"; 26 | rpc_server = new jcon::JsonRpcTcpServer(parent); 27 | } else { 28 | qDebug() << "Starting WebSocket server"; 29 | rpc_server = new jcon::JsonRpcWebSocketServer(parent); 30 | } 31 | 32 | if (allow_notifications) 33 | rpc_server->enableSendNotification(true); 34 | 35 | auto service1 = new ExampleService; 36 | auto service2 = new NotificationService; 37 | 38 | rpc_server->registerServices({ service1, service2 }); 39 | rpc_server->listen(6002); 40 | return rpc_server; 41 | } 42 | 43 | jcon::JsonRpcServer* startNamespacedServer( 44 | QObject* parent, 45 | SocketType socket_type = SocketType::tcp, 46 | bool allow_notifications = false) 47 | { 48 | jcon::JsonRpcServer* rpc_server; 49 | if (socket_type == SocketType::tcp) { 50 | qDebug() << "Starting TCP server"; 51 | rpc_server = new jcon::JsonRpcTcpServer(parent); 52 | } else { 53 | qDebug() << "Starting WebSocket server"; 54 | rpc_server = new jcon::JsonRpcWebSocketServer(parent); 55 | } 56 | 57 | if (allow_notifications) 58 | rpc_server->enableSendNotification(true); 59 | 60 | auto service1 = new ExampleService; 61 | auto service2 = new OtherService; 62 | auto service3 = new NotificationService; 63 | 64 | rpc_server->registerServices( 65 | { 66 | { service1, "ex/myFirstNamespace" }, 67 | { service2, "ex/myOtherNamespace" }, 68 | { service3, "ex/myNotificationNamespace" } 69 | }, "/"); 70 | rpc_server->listen(6002); 71 | return rpc_server; 72 | } 73 | 74 | jcon::JsonRpcClient* startClient(QObject* parent, 75 | SocketType socket_type = SocketType::tcp, 76 | bool allow_notifications = false) 77 | { 78 | jcon::JsonRpcClient* rpc_client; 79 | if (socket_type == SocketType::tcp) { 80 | rpc_client = new jcon::JsonRpcTcpClient(parent); 81 | rpc_client->connectToServer("127.0.0.1", 6002); 82 | } else { 83 | rpc_client = new jcon::JsonRpcWebSocketClient(parent); 84 | // This is just to illustrate the fact that connectToServer also accepts 85 | // a QUrl argument. 86 | rpc_client->connectToServer(QUrl("ws://127.0.0.1:6002")); 87 | } 88 | 89 | if (allow_notifications) 90 | rpc_client->enableReceiveNotification(true); 91 | 92 | return rpc_client; 93 | } 94 | 95 | void invokeMethodAsync(jcon::JsonRpcClient* rpc_client) 96 | { 97 | auto req = rpc_client->callAsync("getRandomInt", 10); 98 | 99 | req->connect(req.get(), &jcon::JsonRpcRequest::result, 100 | [](const QVariant& result) { 101 | qDebug() << "result of asynchronous RPC call:" << result; 102 | }); 103 | 104 | req->connect(req.get(), &jcon::JsonRpcRequest::error, 105 | [](int code, const QString& message) { 106 | qDebug() << "RPC error:" << message 107 | << " (" << code << ")"; 108 | }); 109 | } 110 | 111 | void invokeMethodSync(jcon::JsonRpcClient* rpc_client) 112 | { 113 | auto result = rpc_client->call("getRandomInt", 100); 114 | 115 | if (result->isSuccess()) { 116 | qDebug() << "result of synchronous RPC call:" << result->result(); 117 | } else { 118 | qDebug() << "RPC error:" << result->toString(); 119 | } 120 | } 121 | 122 | void invokeStringMethodAsync(jcon::JsonRpcClient* rpc_client) 123 | { 124 | auto req = rpc_client->callAsync("printMessage", "hello, world"); 125 | 126 | req->connect(req.get(), &jcon::JsonRpcRequest::result, 127 | [](const QVariant& result) { 128 | qDebug() << "result of asynchronous RPC call:" << result; 129 | }); 130 | 131 | req->connect(req.get(), &jcon::JsonRpcRequest::error, 132 | [](int code, const QString& message) { 133 | qDebug() << "RPC error:" << message 134 | << " (" << code << ")"; 135 | }); 136 | } 137 | 138 | void invokeNamedParamsMethodAsync(jcon::JsonRpcClient* rpc_client) 139 | { 140 | auto req = rpc_client->callAsyncNamedParams("namedParams", 141 | QVariantMap{ 142 | {"msg", "hello, world"}, 143 | {"answer", 42} 144 | }); 145 | 146 | req->connect(req.get(), &jcon::JsonRpcRequest::result, 147 | [](const QVariant& result) { 148 | qDebug() << "result of asynchronous RPC call:" << result; 149 | }); 150 | 151 | req->connect(req.get(), &jcon::JsonRpcRequest::error, 152 | [](int code, const QString& message) { 153 | qDebug() << "RPC error:" << message 154 | << " (" << code << ")"; 155 | }); 156 | } 157 | 158 | void invokeStringMethodSync(jcon::JsonRpcClient* rpc_client) 159 | { 160 | auto result = rpc_client->call("printMessage", "hello, world"); 161 | 162 | if (result->isSuccess()) { 163 | qDebug() << "result of synchronous RPC call:" << result->result(); 164 | } else { 165 | qDebug() << "RPC error:" << result->toString(); 166 | } 167 | } 168 | 169 | void invokeNotification(jcon::JsonRpcClient* rpc_client) 170 | { 171 | rpc_client->notification("printNotification", "hello, notification"); 172 | } 173 | 174 | void invokeNamespacedMethods(jcon::JsonRpcClient* rpc_client) 175 | { 176 | auto result = rpc_client->call("ex/myFirstNamespace/getRandomInt", 100); 177 | 178 | if (result->isSuccess()) { 179 | qDebug() << "result of first namespaced synchronous RPC call:" 180 | << result->result(); 181 | } else { 182 | qDebug() << "RPC error:" << result->toString(); 183 | } 184 | 185 | result = rpc_client->call("ex/myOtherNamespace/getRandomInt", 100); 186 | 187 | if (result->isSuccess()) { 188 | qDebug() << "result of second namespaced synchronous RPC call:" 189 | << result->result(); 190 | } else { 191 | qDebug() << "RPC error:" << result->toString(); 192 | } 193 | } 194 | 195 | void waitForOutstandingRequests(jcon::JsonRpcClient* rpc_client) 196 | { 197 | if (rpc_client->outstandingRequestCount() > 0) { 198 | qDebug().noquote() << QString("Waiting for %1 outstanding requests") 199 | .arg(rpc_client->outstandingRequestCount()); 200 | 201 | while (rpc_client->outstandingRequestCount() > 0) { 202 | qDebug() << "Calling QCoreApplication::processEvents()"; 203 | QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); 204 | } 205 | } else { 206 | qDebug() << "No outstanding requests, quitting"; 207 | } 208 | } 209 | 210 | /** 211 | * Example code of running both a server and a client and making some requests 212 | * from the client to the server. 213 | */ 214 | void runServerAndClient(int argc, char* argv[]) 215 | { 216 | QCoreApplication app(argc, argv); 217 | 218 | { 219 | auto server = startServer(nullptr, SocketType::tcp); 220 | auto rpc_client = startClient(&app, SocketType::tcp); 221 | 222 | invokeNotification(rpc_client); 223 | invokeMethodAsync(rpc_client); 224 | invokeMethodSync(rpc_client); 225 | invokeStringMethodSync(rpc_client); 226 | invokeStringMethodAsync(rpc_client); 227 | invokeNamedParamsMethodAsync(rpc_client); 228 | 229 | waitForOutstandingRequests(rpc_client); 230 | delete server; 231 | } 232 | 233 | { 234 | auto server = startNamespacedServer(nullptr); 235 | auto rpc_client = startClient(&app, SocketType::tcp); 236 | invokeNamespacedMethods(rpc_client); 237 | waitForOutstandingRequests(rpc_client); 238 | delete server; 239 | } 240 | 241 | { 242 | auto server = startServer(nullptr, SocketType::tcp, true); 243 | auto rpc_client = startClient(&app, SocketType::tcp, true); 244 | QObject::connect(rpc_client, &jcon::JsonRpcClient::notificationReceived, 245 | &app, [](const QString& key, const QVariant& value){ 246 | qDebug() << "Received notification:" 247 | << "Key:" << key 248 | << "Value:" << value; 249 | }); 250 | app.exec(); 251 | delete server; 252 | } 253 | } 254 | 255 | /** 256 | * Example of starting a server and keeping it running indefinitely. 257 | */ 258 | void runServer(int argc, char* argv[]) 259 | { 260 | QCoreApplication app(argc, argv); 261 | auto server = startServer(nullptr, SocketType::websocket); 262 | app.exec(); 263 | } 264 | 265 | int main(int argc, char* argv[]) 266 | { 267 | runServerAndClient(argc, argv); 268 | // runServer(argc, argv); 269 | return 0; 270 | } 271 | -------------------------------------------------------------------------------- /src/notification_service.cpp: -------------------------------------------------------------------------------- 1 | #include "notification_service.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | NotificationService::NotificationService(QObject *parent) : QObject(parent), 8 | m_timer(new QTimer(this)) 9 | { 10 | m_timer->setInterval(2000); 11 | connect(m_timer, &QTimer::timeout, this, [this](){ 12 | emit sendUnsolicitedNotification("myString", "myValue"); 13 | emit sendUnsolicitedNotification("myInteger", 1); 14 | 15 | QMap map; 16 | map["one"] = 1; 17 | map["three"] = 3; 18 | map["seven"] = 7; 19 | emit sendUnsolicitedNotification("myMap", map); 20 | 21 | emit sendUnsolicitedNotification("myDouble", 54.53); 22 | }); 23 | m_timer->start(); 24 | } 25 | 26 | NotificationService::~NotificationService() = default; 27 | -------------------------------------------------------------------------------- /src/notification_service.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | class QTimer; 5 | 6 | class NotificationService : public QObject 7 | { 8 | Q_OBJECT 9 | 10 | public: 11 | NotificationService(QObject *parent = nullptr); 12 | virtual ~NotificationService(); 13 | 14 | signals: 15 | void sendUnsolicitedNotification(const QString&, const QVariant&); 16 | 17 | private: 18 | QTimer *m_timer; 19 | }; 20 | -------------------------------------------------------------------------------- /src/other_service.cpp: -------------------------------------------------------------------------------- 1 | #include "other_service.h" 2 | 3 | #include 4 | 5 | OtherService::OtherService() = default; 6 | 7 | OtherService::~OtherService() = default; 8 | 9 | int OtherService::getRandomInt(int limit) 10 | { 11 | return -(QRandomGenerator::global()->generate() % limit); 12 | } 13 | -------------------------------------------------------------------------------- /src/other_service.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class OtherService : public QObject 6 | { 7 | Q_OBJECT 8 | 9 | public: 10 | OtherService(); 11 | virtual ~OtherService(); 12 | 13 | Q_INVOKABLE int getRandomInt(int limit); 14 | }; 15 | --------------------------------------------------------------------------------