Code Monkey home page Code Monkey logo

packio's Introduction

Header-only | JSON-RPC | msgpack-RPC | asio | coroutines

This library requires C++17 and is designed as an extension to boost.asio. It will let you build asynchronous servers or client for JSON-RPC or msgpack-RPC.

The project is hosted on GitHub and available on Conan Center. Documentation is available on GitHub Pages.

Overview

#include <iostream>

#include <packio/packio.h>

using packio::allow_extra_arguments;
using packio::arg;
using packio::nl_json_rpc::completion_handler;
using packio::nl_json_rpc::make_client;
using packio::nl_json_rpc::make_server;
using packio::nl_json_rpc::rpc;
using namespace packio::arg_literals;

int main(int, char**)
{
    using namespace packio::arg_literals;

    // Declare a server and a client, sharing the same io_context
    packio::net::io_context io;
    packio::net::ip::tcp::endpoint bind_ep{
        packio::net::ip::make_address("127.0.0.1"), 0};
    auto server = make_server(packio::net::ip::tcp::acceptor{io, bind_ep});
    auto client = make_client(packio::net::ip::tcp::socket{io});

    // Declare a synchronous callback with named arguments
    server->dispatcher()->add(
        "add", {"a", "b"}, [](int a, int b) { return a + b; });
    // Declare an asynchronous callback with named arguments,
    // an argument with a default value and an option to
    // accept and discard extra arguments
    server->dispatcher()->add_async(
        "multiply",
        {allow_extra_arguments, "a", "b"_arg = 2},
        [&io](completion_handler complete, int a, int b) {
            // Call the completion handler later
            packio::net::post(
                io, [a, b, complete = std::move(complete)]() mutable {
                    complete(a * b);
                });
        });
    // Declare a coroutine with unnamed arguments
    server->dispatcher()->add_coro(
        "pow", io, [](int a, int b) -> packio::net::awaitable<int> {
            co_return std::pow(a, b);
        });

    // Connect the client
    client->socket().connect(server->acceptor().local_endpoint());
    // Accept connections
    server->async_serve_forever();
    // Run the io_context
    std::thread thread{[&] { io.run(); }};

    // Make an asynchronous call with named arguments
    // using either `packio::arg` or `packio::arg_literals`
    std::promise<int> add1_result, multiply_result;
    client->async_call(
        "add",
        std::tuple{arg("a") = 42, "b"_arg = 24},
        [&](packio::error_code, const rpc::response_type& r) {
            add1_result.set_value(r.result.get<int>());
        });
    std::cout << "42 + 24 = " << add1_result.get_future().get() << std::endl;

    // Use packio::net::use_future with named arguments and literals
    auto add_future = client->async_call(
        "multiply",
        std::tuple{"a"_arg = 12, "b"_arg = 23},
        packio::net::use_future);
    std::cout << "12 * 23 = " << add_future.get().result.get<int>() << std::endl;

    // Spawn the coroutine and wait for its completion
    std::promise<int> pow_result;
    packio::net::co_spawn(
        io,
        [&]() -> packio::net::awaitable<void> {
            // Call using an awaitable and positional arguments
            auto res = co_await client->async_call(
                "pow", std::tuple{2, 8}, packio::net::use_awaitable);
            pow_result.set_value(res.result.get<int>());
        },
        packio::net::detached);
    std::cout << "2 ** 8 = " << pow_result.get_future().get() << std::endl;

    io.stop();
    thread.join();

    return 0;
}

Requirements

  • C++17 or C++20
  • msgpack >= 3.2.1
  • nlohmann_json >= 3.9.1
  • boost.asio >= 1.70.0 or asio >= 1.13.0

Older versions of msgpack and nlohmann_json are probably compatible but they are not tested on the CI.

Configurations

Standalone or Boost.Asio

By default, packio uses boost::asio. It is also compatible with standalone asio. To use the standalone version, the preprocessor macro PACKIO_STANDALONE_ASIO=1 must be defined.

If you are using the conan package, you can use the option standalone_asio=True.

Depending on your choice, the namespace packio::net will be an alias for either boost::asio or asio.

RPC components

You can define the following preprocessor macros to either 0 or 1 to force-disable or force-enable components of packio:

  • PACKIO_HAS_MSGPACK
  • PACKIO_HAS_NLOHMANN_JSON
  • PACKIO_HAS_BOOST_JSON

If you're using the conan package, use the associated options instead, conan will define these macros accordingly.

If you're not using the conan package, packio will try to auto-detect whether these components are available on your system. Define the macros to the appropriate value if you encounter any issue.

Boost before 1.75

If you're using the conan package with a boost version older than 1.75, you need to manually disable Boost.Json with the options boost_json=False. Boost.Json version 1.75 contains some bugs when using C-strings as arguments so I'd recommend at using at least version 1.76.

Tested compilers

  • gcc-9
  • gcc-10
  • gcc-11
  • gcc-12
  • clang-11
  • clang-12
  • clang-13
  • clang-14
  • Apple clang-13
  • Visual Studio 2019 Version 16
  • Visual Studio 2022 Version 17

Older compilers may be compatible but are not tested.

Install with conan

conan install packio/x.x.x

Coroutines

packio is compatible with C++20 coroutines:

  • calls can use the packio::asio::use_awaitable completion token
  • coroutines can be registered in the server

Coroutines are tested for the following compilers:

  • gcc-11
  • gcc-12
  • clang-14
  • Apple clang-12

Samples

You will find some samples in test_package/samples/ to help you get a hand on packio.

Bonus

Let's compute fibonacci's numbers recursively over websockets with coroutines on a single thread ... in 65 lines of code.

#include <iostream>

#include <packio/extra/websocket.h>
#include <packio/packio.h>

using packio::msgpack_rpc::make_client;
using packio::msgpack_rpc::make_server;
using packio::net::ip::make_address;

using awaitable_tcp_stream = decltype(packio::net::use_awaitable_t<>::as_default_on(
    std::declval<boost::beast::tcp_stream>()));
using websocket = packio::extra::
    websocket_adapter<boost::beast::websocket::stream<awaitable_tcp_stream>, true>;
using ws_acceptor =
    packio::extra::websocket_acceptor_adapter<packio::net::ip::tcp::acceptor, websocket>;

int main(int argc, char** argv)
{
    if (argc < 2) {
        std::cerr << "I require one argument" << std::endl;
        return 1;
    }
    const int n = std::atoi(argv[1]);

    packio::net::io_context io;
    packio::net::ip::tcp::endpoint bind_ep{make_address("127.0.0.1"), 0};

    auto server = make_server(ws_acceptor{io, bind_ep});
    auto client = make_client(websocket{io});

    server->dispatcher()->add_coro(
        "fibonacci", io, [&](int n) -> packio::net::awaitable<int> {
            if (n <= 1) {
                co_return n;
            }

            auto r1 = co_await client->async_call("fibonacci", std::tuple{n - 1});
            auto r2 = co_await client->async_call("fibonacci", std::tuple{n - 2});

            co_return r1.result.as<int>() + r2.result.as<int>();
        });

    int result = 0;
    packio::net::co_spawn(
        io,
        [&]() -> packio::net::awaitable<void> {
            auto ep = server->acceptor().local_endpoint();
            co_await client->socket().next_layer().async_connect(ep);
            co_await client->socket().async_handshake(
                "127.0.0.1:" + std::to_string(ep.port()), "/");
            auto ret = co_await client->async_call("fibonacci", std::tuple{n});
            result = ret.result.template as<int>();
            io.stop();
        },
        packio::net::detached);

    server->async_serve_forever();
    io.run();

    std::cout << "F{" << n << "} = " << result << std::endl;

    return 0;
}

packio's People

Contributors

muellni avatar qchateau avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

packio's Issues

Omit data field in error response

Is there any way to get the response to omit the data field? For the most part, it's sort of redundant information for me. If I use set_error() with no arguments, it gets set to unknown error which is extra redundant since that's already what's in the message field of the response.

RPC client as class member variable

How do i define the rpc client as a class member variable? I cannot use auto. I have to setup a rpc connection to serveral devices and i thought the best to do this is to instantiate a device object from a device class, each holding the rpc client as a member variable.

How to deal with optional parameters

When the dispatcher encounters a missing parameter (using the nlohmann::json implementation), it throws the request away as the request fails to parse in rpc::convert_named_args, due to the use of the json::at method which throws due to the parameter key not being present.

We have a use-case where the other end omits optional parameters (instead of setting them to null), which we cannot change. Currently, I have hacked a workaround in, which is to attempt to return a default constructed object if the parameter is not found:

    template <typename T, typename NamesContainer, std::size_t... Idxs>
    static T convert_named_args(
        const nlohmann::json& args,
        const NamesContainer& names,
        std::index_sequence<Idxs...>)
    {
        return T{(args.template value<std::tuple_element_t<Idxs, T>>(names.at(Idxs), {}))...};
    }

It's not particularly elegant, and requires a suitably friendly type with a sensible default constructor to be passed into dispatcher::add (e.g. nlohmann::json, which can be explicitly tested for is_null() in the dispatcher callback).

Do you think this change is appropriate for general inclusion in a PR? I'm not sure how well it would work for the Boost JSON or MessagePack variants. Alternatively, do you have a better solution?

packio in cmake project

I am trying to use packio in my cmake project but I get a few compiler errors:

error 1

.conan/data/packio/2.2.0/_/_/package/0c0791061c9ce0b41dcaec85e4a8aeeb1bfa1a31/include/packio/json_rpc/hash.h:66:8: error: redefinition of ‘struct std::hash<boost::json::value>’
   66 | struct hash<boost::json::value> {
...
.conan/data/boost/1.80.0/_/_/package/cb420f7a9d4f5344407adad933bfd4a05ab58ddd/include/boost/json/value.hpp:4030:8: note: previous definition of ‘struct std::hash<boost::json::value>’
 4030 | struct hash< ::boost::json::value > {

error 2

error: ‘using element_type = class packio::dispatcher<packio::nl_json_rpc::rpc, packio::default_map, std::mutex>’ {aka ‘class packio::dispatcher<packio::nl_json_rpc::rpc, packio::default_map, std::mutex>’} has no member named ‘add_coro’
  108 |     server->dispatcher()->add_coro(

error 3

error: ‘awaitable’ in namespace ‘packio::net’ does not name a template type
  109 |             "pow", io, [](int a, int b) -> packio::net::awaitable<int> {

It seems that boost asio is not correctly linked somehow?

My cmake code for including packio:

include("${CMAKE_MODULE_PATH}/conan.cmake")
conan_cmake_configure(REQUIRES packio/2.2.0
        GENERATORS cmake_find_package)
conan_cmake_autodetect(settings)
message("external: settings = ${settings}")
conan_cmake_install(PATH_OR_REFERENCE .
        BUILD missing
        REMOTE conancenter
        SETTINGS ${settings})

Any help would be very much appreciated.

UPDATE:
I am getting this error even when running test_package:

packio$ conan test test_package/ packio/2.2.0
.conan/data/packio/2.2.0/_/_/package/0c0791061c9ce0b41dcaec85e4a8aeeb1bfa1a31/include/packio/json_rpc/hash.h:66:8: error: redefinition of ‘struct std::hash<boost::json::value>’
   66 | struct hash<boost::json::value> {
...
.conan/data/boost/1.80.0/_/_/package/cb420f7a9d4f5344407adad933bfd4a05ab58ddd/include/boost/json/value.hpp:4030:8: note: previous definition of ‘struct std::hash<boost::json::value>’
 4030 | struct hash< ::boost::json::value > {

I have no experience with conan so please forgive me if I am missing something obvious.

Separated client and server

Based on my previous request Closed Issue #65 i have a further question. I'm using the nl_json_rpc client and I tried to separate the client and server in two different programs (executable). From the client i can connect to the server socket (in the code below i get "The connection has been established!")

packio::net::io_context io_context;
packio::net::ip::tcp::socket socket{io_context};
packio::net::ip::tcp::resolver resolver{io_context};
auto endpoints = resolver.resolve("localhost", std::to_string(6543));

packio::error_code error;
auto client = make_client(std::move(socket));
client->socket().connect(endpoints->endpoint(), error);

if(not error)
{
    std::cout << "The connection has been established!";
}
else
{
    std::cerr << "Something went wrong :(";
}

But for some reason the following RPC async_call does not work anymore, e.g. it seems it waits forever for the add_result.get_future().get()

std::promise<int> add_result, multiply_result;
client->async_call(
    "add",
    std::tuple{arg("a") = 42, arg("b") = 24},
    [&](packio::error_code, const rpc::response_type& r) {
        add_result.set_value(r.result.get<int>());
    });
std::cout << "42 + 24 = " << add_result.get_future().get() << std::endl;

May i ask you if you know what the problem is or you can add an example with separated client & server?

Accessing session from handler?

Hi!
I'm working with your library, and I'm trying to figure out how to get the session (or socket) from within a handler.
We'd like to send back notifications on the same websocket; the server may have several users connected.

The only example code I saw related to this, was capturing a session_ptr when adding the handler (from basic_test_server_crash.cpp)

    std::shared_ptr<session_type> session_ptr;
    this->server_->async_serve([&](auto ec, auto session) {
        ASSERT_FALSE(ec);
        session->start();
        session_ptr = session;
    });
    this->server_->dispatcher()->add(
        "close", [&]() { session_ptr->socket().close(); });

But, I'm not sure something like that would work with more than one connection.
I could capture the server itself, but I don't know how to go about finding the socket in the handler.

Thank you for any hints here!

Kyle

Missing optional header

The optional header isn't included in traits.h. It causes some errors with MSVC:

E:\Users\Yoann\.conan\data\packio\1.3.0\_\_\package\5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9\include\packio\traits.h(88): error C2065: 'optional': undeclared identifier
  E:\Users\Yoann\.conan\data\packio\1.3.0\_\_\package\5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9\include\packio\traits.h(89): note: see reference to class template instantiation 'packio::traits::AsCallHandler<T,Result>' being compiled
E:\Users\Yoann\.conan\data\packio\1.3.0\_\_\package\5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9\include\packio\traits.h(88): error C3544: '_Args': parameter pack expects a type template argument
E:\Users\Yoann\.conan\data\packio\1.3.0\_\_\package\5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9\include\packio\traits.h(88): error C2993: 'unknown-type': is not a valid type for non-type template parameter 'condition'
E:\Users\Yoann\.conan\data\packio\1.3.0\_\_\package\5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9\include\packio\traits.h(88): error C2955: 'packio::traits::Trait': use of class template requires template argument list
  E:\Users\Yoann\.conan\data\packio\1.3.0\_\_\package\5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9\include\packio\traits.h(62): note: see declaration of 'packio::traits::Trait'
E:\Users\Yoann\.conan\data\packio\1.3.0\_\_\package\5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9\include\packio\traits.h(88): error C2143: syntax error: missing ',' before '>'
E:\Users\Yoann\.conan\data\packio\1.3.0\_\_\package\5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9\include\packio\client.h(166): error C2061: syntax error: identifier 'optional'
  E:\Users\Yoann\.conan\data\packio\1.3.0\_\_\package\5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9\include\packio\client.h(483): note: see reference to class template instantiation 'packio::client<Socket,Map>' being compiled
E:\Users\Yoann\.conan\data\packio\1.3.0\_\_\package\5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9\include\packio\client.h(182): error C2061: syntax error: identifier 'optional'
E:\Users\Yoann\.conan\data\packio\1.3.0\_\_\package\5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9\include\packio\client.h(414): error C2061: syntax error: identifier 'optional'
  E:\Users\Yoann\.conan\data\packio\1.3.0\_\_\package\5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9\include\packio\client.h(472): note: see reference to class template instantiation 'packio::client<Socket,Map>::initiate_async_call<Buffer>' being compiled

Coroutines

Client calls should be compatible, verify that
See how we can use coroutines as server procedures

Progress support with WebSocket adapter

I have WebSocket server over JSON-RPC (MR #60), that can do long (from 1 to 30 minutes) the API call and can generate progress (%) with extra data (remaining time, sec).
Can I do progress somehow?

Feature proposal - unsolicited notifications

I am currently using packio as a JSON RPC client in a commercial project. The server is a custom JSON RPC implementation that sends unsolicited notifications. This behaviour is outside the JSON RPC specification, but we have no control over the server. Other libraries have implemented this extension - see https://github.com/joncol/jcon-cpp as an example.

I have implemented the client-side support for this, extending the client class using the existing dispatcher class used by the server. We have been using this successfully over the last couple of months. What we have not done:

  • Server support for sending such notifications
  • Testing for Boost JSON and MessagePack variants (our project uses JSON for Modern C++)
  • Packio tests

Before I do any further work - would you be interested in a PR for this feature?

Lack of basic exception guarantee in manual_strand

Here, if handler invocation throws, you'll leave the handler in the queue. If a higher layer (in user code) attempts to recover from the exception, you may end up calling the same handler again (possibly in a different async operation on the same I/O object).

Compiler support

We should run CI for macOS and windows, also, list supported compilers and boost versions

conan package install error: 'ConanOutput' object has no attribute 'warn'

Error details:

ERROR: packio/2.5.0: Error in configure() method, line 89
        self.output.warn("packio requires C++17. Your compiler is unknown. Assuming it supports C++17.")
        AttributeError: 'ConanOutput' object has no attribute 'warn'

cause analysis:
warn() in https://github.com/conan-io/conan-center-index/blob/master/recipes/packio/all/conanfile.py#L89C18-L89C24 doesn't exist in conan 2.0 api https://docs.conan.io/2/reference/conanfile/running_and_output.html

Differentiate between cancel and closing

Hello,

first of all a big thanks for this library!

I'm currently working on a HTTPS stream adapter to use packio to talk to OpenWRT's ubus system bus. ubus exposes its API via jsonrpc2.0 over HTTP. Since I don't want to send login credentials over the plain wire, HTTPS is used.

This works quite well, except for some issues that stem from the HTTP/HTTPS session management. The webserver (which is uhttpd) which is running on the OpenWRT device does not enable HTTP keepalive by default, so it immediately kills the session (down to the TCP layers) once a request/response cycle is through. But even with HTTP keepalive enabled, it only waits for a few seconds before doing the same.

In my adapters async_read_some() this results in a boost::asio::ssl::error::stream_truncated. Because why should one gracefully shutdown the SSL layer, if you can just shutdown the TCP layer? sigh

Anyway, the problem I'm trying to solve is that once this happens, and the error code is propagated up into packio code, the packio code is calling its client::close(). This then calls my handler with net::error::operation_aborted. Which is the same error code that client::cancel() uses. So there is currently no way to differentiate between cancellation and the situation where the remote host (the OpenWRT) closes the session.

I currently hotfix this locally by letting client::close() return net::error::not_connected, so that I can detect this in my handler and re-open my HTTPS session.

I'm not sure if this really the best approach, so I wanted to ask for feedback here.

Thanks in advance!

With best wishes,
Tobias

C++20 should use coroutines by default

ATM, C++20 and coroutines usage is differenciated in tests because compilers do not properly support coroutines. In a (close?) future, C++20 and coroutines usage should go hand in hand.

Compiler status:

  • gcc: need gcc10.3 or gcc11 because of a bug in coroutines which has already been fixed upstream
  • clang: need clang-11 for non experimental support
  • msvc: need version 16.8 (released but not yet available in Github Actions)
  • apple-clang: probably in the next version ?

Connections handling by separate threads

Is there a way to handle each connection by a separate thread?

Background: clients keep the connections alive (connection pool) for long period of the time to send many requests to one connection (one after another). Processing of a request, including reading it and writing it to the socket, is quite heavy (consumes a lot of CPU).

How can I start a new thread for each connection, right after accept() ? The new thread suppose to be created once for entire connection, not for each request.

Thank you in advance.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.