Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
92ba4ca
Resolve 'memset' not declared compiler error on Ubuntu 16
Nov 21, 2016
f4374b3
Merge pull request #1 from uskr/master
semmel Jul 17, 2018
0521e56
Added async-server branch
semmel Jul 19, 2018
f93e543
chore (examples): added makefile
semmel Jul 20, 2018
ea1d161
wip (async server): server writes values of futures as formatted data
semmel Jul 21, 2018
1e1b45f
feat (async server): Can now dispatch to methods which return jsonrp…
semmel Jul 23, 2018
fc21502
fix (async server): Can now also async handle request even if the me…
semmel Jul 23, 2018
101c00a
chore (build): exclude binary artifacts
semmel Jul 24, 2018
460fe8e
chore (build): fixed testclient target
semmel Jul 24, 2018
608c786
chore (build): fixed symbolic boost library links in makefile
semmel Jul 24, 2018
c3a6f3c
docu (API): updated readme explaining asynchronous server
semmel Jul 25, 2018
d11d199
docu (API): fixed indentation
semmel Jul 25, 2018
e7472c1
docu (API): added example of invoking a synchronous method with the a…
semmel Jul 25, 2018
6a17b2c
docu (Readme): small improvements in the documentation
semmel Jul 27, 2018
f15c74b
docu (Readme): fix the example
semmel Jul 27, 2018
6a0f785
hack (JSON format): disabled interpreting strings as date
semmel Aug 22, 2018
4215abc
jsonrpc response errors resulting from exceptions now report the nume…
semmel Sep 26, 2018
0425c5b
The data field in response errors is used for the std::system_error c…
semmel Sep 26, 2018
97d911c
Removed conversion of std::out_of_range exception to JSON-RPC-'Invali…
semmel Sep 28, 2018
cb6a5c2
support for methods returning future<void>
semmel Dec 18, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,4 @@ FakesAssemblies/
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
/nbproject/
159 changes: 112 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,34 @@ Currently, the only dependency is rapidjson (https://github.com/miloyip/rapidjso

Another advantage of removing the dependencies is that now it is easy to compile and use on most platforms that support c++11, without much work.

## Supporting Asynchronous Calls

Exposing methods which do not block the server is possible on the basis of [C++ Futures](http://www.modernescpp.com/index.php/component/content/article/44-blog/multithreading/multithreading-c-17-and-c-20/279-std-future-extensions?Itemid=239) Extension Concurrency TS which are already available via the [Boost Futures v.4implementation](https://www.boost.org/doc/libs/1_67_0/doc/html/thread/synchronization.html#thread.synchronization.futures). The idea is that methods which take longer to complete return a `future` of the result which is later collected and replied with in a continuation method `.then()`. This way the JSON-RPC server does not block it's thread while computing the response to a call.


The future should carry a type convertible to `jsonrpc::Value`. The non-blocking operation of the server can be reached by replacing `Server::HandleRequest` with `Server::asyncHandleRequest` and processing the response' `FormattedData` in the future callback:

```C++
std::string incomingRequest;
ioStream >> incomingRequest; // read from a data source
server.asyncHandleRequest(incomingRequest)
.then([=](auto futureDataPtr){
iostream << futureDataPtr.get()->GetData(); // write to the data sink
});
```

This can be done in general for all incoming requests, because the implementation of `Server::asyncHandleRequest()` can deal with synchronous plain-value-returning methods too. Except for [Lambda methods](#asynchronous-lambdas) no other changes are required by the implementation.

### Asynchronous Lambdas

For now rvalue references of asynchronous lambda are not supported and need to be wrapped with `std::function`. Also these `std::functions`s are not properly disassembled by the template machinery in `Dispatcher::AddMethod` and thus need to register via a custom method `Dispatcher::AddAsyncLambda`.

Asynchronous free static functions and member methods should register to the dispatcher in the same manner as the synchronous versions by `Dispatcher::AddMethod`.

### Additional Dependencies

Until the [TS for Extensions for Concurrency](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4107.html) are implemented by C++2a, asynchronous call handling depends on it's implementation in [Boost Thread](https://www.boost.org/doc/libs/1_67_0/doc/html/thread.html) v. 4. Thus building and linking to `boost_thread` (with `BOOST_THREAD_VERSION=4`) and `boost_system` (a dependency) is required. Linking to POSIX threads or the Windows counterpart is also needed for multi-threaded programs.

## Examples

A simple server that process JSON-RPC requests:
Expand All @@ -26,6 +54,12 @@ public:
return a + b;
}

boost::future<int> AsyncAddInt(int a, int b) const {
return boost::async([](auto a, auto b){
return a + b;
}, a, b);
}

int64_t AddArray(const jsonrpc::Value::Array& a) {
return std::accumulate(a.begin(), a.end(), int64_t(0),
[](const int64_t& a, const jsonrpc::Value& b) { return a + b.AsInteger32(); });
Expand All @@ -49,55 +83,86 @@ void PrintNotification(const std::string& a) {
}

int main() {
Math math;
jsonrpc::Server server;

jsonrpc::JsonFormatHandler jsonFormatHandler;
server.RegisterFormatHandler(jsonFormatHandler);
Math math;
jsonrpc::Server server;

jsonrpc::JsonFormatHandler jsonFormatHandler;
server.RegisterFormatHandler(jsonFormatHandler);

auto& dispatcher = server.GetDispatcher();
// if it is a member method, you must use this 3 parameter version, passing an instance of an object that implements it
dispatcher.AddMethod("add", &Math::Add, math);
dispatcher.AddMethod("async_add_int", &Math::AsyncAddInt, math)
dispatcher.AddMethod("add_array", &Math::AddArray, math);

// if it is just a regular function (non-member or static), you can you the 2 parameter AddMethod
dispatcher.AddMethod("concat", &Concat);
dispatcher.AddMethod("to_struct", &ToStruct);
dispatcher.AddMethod("print_notification", &PrintNotification);

std::function<boost::future<std::string>(std::string)> sReverse = [](std::string in) -> boost::future<std::string> {
std::string res;
return boost::make_ready_future(res.assign(in.rbegin(), in.rend()));
};
dispatcher.AddAsyncLambda("async_reverse", sReverse);

// on a real world, these requests come from your own transport implementation (sockets, http, ipc, named-pipes, etc)
const char addRequest[] = "{\"jsonrpc\":\"2.0\",\"method\":\"add\",\"id\":0,\"params\":[3,2]}";
const char addIntAsyncRequest[] = R"({"jsonrpc":"2.0","method":"async_add_int","id":11,"params":[300,200]})";
const char concatRequest[] = "{\"jsonrpc\":\"2.0\",\"method\":\"concat\",\"id\":1,\"params\":[\"Hello, \",\"World!\"]}";
const char addArrayRequest[] = "{\"jsonrpc\":\"2.0\",\"method\":\"add_array\",\"id\":2,\"params\":[[1000,2147483647]]}";
const char toStructRequest[] = "{\"jsonrpc\":\"2.0\",\"method\":\"to_struct\",\"id\":5,\"params\":[[12,\"foobar\",[12,\"foobar\"]]]}";
const char printNotificationRequest[] = "{\"jsonrpc\":\"2.0\",\"method\":\"print_notification\",\"params\":[\"This is just a notification, no response expected!\"]}";
const char asyncReverseRequest[] = R"({"jsonrpc":"2.0","method":"async_reverse","id":13,"params":["xyz"]})";

std::shared_ptr<jsonrpc::FormattedData> outputFormattedData;
std::cout << "request: " << addRequest << std::endl;
outputFormattedData = server.HandleRequest(addRequest);
std::cout << "response: " << outputFormattedData->GetData() << std::endl;

outputFormattedData.reset();
std::cout << "request: " << concatRequest << std::endl;
outputFormattedData = server.HandleRequest(concatRequest);
std::cout << "response: " << outputFormattedData->GetData() << std::endl;

outputFormattedData.reset();
std::cout << "request: " << addArrayRequest << std::endl;
outputFormattedData = server.HandleRequest(addArrayRequest);
std::cout << "response: " << outputFormattedData->GetData() << std::endl;

outputFormattedData.reset();
std::cout << "request: " << toStructRequest << std::endl;
outputFormattedData = server.HandleRequest(toStructRequest);
std::cout << "response: " << outputFormattedData->GetData() << std::endl;

outputFormatedData.reset();
std::cout << "request: " << printNotificationRequest << std::endl;
outputFormatedData = server.HandleRequest(printNotificationRequest);
std::cout << "response size: " << outputFormatedData->GetSize() << std::endl;

std::cout << "test async wrapper around sync\nrequest: " << addRequest << std::endl;
server.asyncHandleRequest(addRequest)
.then([](boost::shared_future<std::shared_ptr<jsonrpc::FormattedData>> futureDataPtr)
{
std::cout << "response: " << futureDataPtr.get()->GetData() << std::endl; // {"jsonrpc":"2.0","id":0,"result":5}
});

auto& dispatcher = server.GetDispatcher();
// if it is a member method, you must use this 3 parameter version, passing an instance of an object that implements it
dispatcher.AddMethod("add", &Math::Add, math);
dispatcher.AddMethod("add_array", &Math::AddArray, math);

// if it is just a regular function (non-member or static), you can you the 2 parameter AddMethod
dispatcher.AddMethod("concat", &Concat);
dispatcher.AddMethod("to_struct", &ToStruct);
dispatcher.AddMethod("print_notification", &PrintNotification);

// on a real world, these requests come from your own transport implementation (sockets, http, ipc, named-pipes, etc)
const char addRequest[] = "{\"jsonrpc\":\"2.0\",\"method\":\"add\",\"id\":0,\"params\":[3,2]}";
const char concatRequest[] = "{\"jsonrpc\":\"2.0\",\"method\":\"concat\",\"id\":1,\"params\":[\"Hello, \",\"World!\"]}";
const char addArrayRequest[] = "{\"jsonrpc\":\"2.0\",\"method\":\"add_array\",\"id\":2,\"params\":[[1000,2147483647]]}";
const char toStructRequest[] = "{\"jsonrpc\":\"2.0\",\"method\":\"to_struct\",\"id\":5,\"params\":[[12,\"foobar\",[12,\"foobar\"]]]}";
const char printNotificationRequest[] = "{\"jsonrpc\":\"2.0\",\"method\":\"print_notification\",\"params\":[\"This is just a notification, no response expected!\"]}";

std::shared_ptr<jsonrpc::FormattedData> outputFormattedData;
std::cout << "request: " << addRequest << std::endl;
outputFormattedData = server.HandleRequest(addRequest);
std::cout << "response: " << outputFormattedData->GetData() << std::endl;

outputFormattedData.reset();
std::cout << "request: " << concatRequest << std::endl;
outputFormattedData = server.HandleRequest(concatRequest);
std::cout << "response: " << outputFormattedData->GetData() << std::endl;

outputFormattedData.reset();
std::cout << "request: " << addArrayRequest << std::endl;
outputFormattedData = server.HandleRequest(addArrayRequest);
std::cout << "response: " << outputFormattedData->GetData() << std::endl;

outputFormattedData.reset();
std::cout << "request: " << toStructRequest << std::endl;
outputFormattedData = server.HandleRequest(toStructRequest);
std::cout << "response: " << outputFormattedData->GetData() << std::endl;

outputFormatedData.reset();
std::cout << "request: " << printNotificationRequest << std::endl;
outputFormatedData = server.HandleRequest(printNotificationRequest);
std::cout << "response size: " << outputFormatedData->GetSize() << std::endl;
std::cout << "request: " << addIntAsyncRequest << std::endl;
server.asyncHandleRequest(addIntAsyncRequest)
.then([](boost::shared_future<std::shared_ptr<jsonrpc::FormattedData>> futureDataPtr){
std::cout << "response: " << futureDataPtr.get()->GetData() << std::endl; // {"jsonrpc":"2.0","id":11,"result":500}
});

std::cout << "request: " << asyncReverseRequest << std::endl;
server.asyncHandleRequest(asyncReverseRequest)
.then([](auto futureDataPtr){ // can use auto parameter type in C++14
std::cout << "response: " << futureDataPtr.get()->GetData() << std::endl; // {"jsonrpc":"2.0","id":13,"result":"zyx"}
});

// Sleep in the main thread to allow the threaded requests to be processed.
boost::this_thread::sleep_for(boost::chrono::seconds(2));

return 0;
return 0;
}
```

Expand Down
119 changes: 119 additions & 0 deletions examples/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
## -*- Makefile -*-
##
## User: semmel
## Time: Jul 20, 2018 5:00:41 PM
## Makefile created by Oracle Developer Studio.
##
## This file is generated automatically.
##


#### Compiler and tool definitions shared by all build targets #####
CCC = g++
CXX = g++
BASICOPTS = -g -std=gnu++14
CCFLAGS = $(BASICOPTS) "-Wno-parentheses"
CXXFLAGS = $(BASICOPTS) "-Wno-parentheses"
CCADMIN =


# Define the target directories.
TARGETDIR_testserver=GNU-amd64-Linux
TARGETDIR_testserver=GNU-amd64-Linux


all: $(TARGETDIR_testserver)/testserver $(TARGETDIR_testserver)/testclient

## Target: testserver
CPPFLAGS_testserver = \
-I../include \
-I../../../../boost_1_67_0 \
-I../../../../rapidjson/include \
-DBOOST_CHRONO_HEADER_ONLY
OBJS_testserver = \
$(TARGETDIR_testserver)/testserver.o
USERLIBS_testserver =
USERLIBS_testserver = $(SYSLIBS_testserver)
DEPLIBS_testserver =
LDLIBS_testserver = $(USERLIBS_testserver)

LDLIBS := boost_system
LDLIBS += boost_filesystem

BOOST_SHARED_LIBRARY_INSTALL_PATH = /home/semmel/NetBeansProjects/lib-sources/boost_1_67_0/stage/lib

# Link or archive
$(TARGETDIR_testserver)/testserver: $(TARGETDIR_testserver) $(OBJS_testserver) $(DEPLIBS_testserver)
$(LINK.cc) $(CCFLAGS_testserver) $(CPPFLAGS_testserver) -o $@ $(OBJS_testserver) -L../../../../boost_1_67_0/stage/lib -lboost_system -lboost_filesystem -lpthread -lboost_thread -Wl,-rpath,$(BOOST_SHARED_LIBRARY_INSTALL_PATH)


# Compile source files into .o files
$(TARGETDIR_testserver)/testserver.o: $(TARGETDIR_testserver) testserver.cpp
$(COMPILE.cc) $(CCFLAGS_testserver) $(CPPFLAGS_testserver) -o $@ testserver.cpp

$(TARGETDIR_testserver)/base64.o: $(TARGETDIR_testserver) base64.cpp
$(COMPILE.cc) $(CCFLAGS_testserver) $(CPPFLAGS_testserver) -o $@ base64.cpp



## Target: testclient
CPPFLAGS_testclient = \
-I../include \
-I../../../../boost_1_67_0 \
-I../../../../rapidjson/include \
-DBOOST_CHRONO_HEADER_ONLY
OBJS_testclient = \
$(TARGETDIR_testserver)/testclient.o
USERLIBS_testclient = $(SYSLIBS_testclient)
DEPLIBS_testclient =
LDLIBS_testclient = $(USERLIBS_testclient)


# Link or archive
$(TARGETDIR_testserver)/testclient: $(TARGETDIR_testserver) $(OBJS_testclient) $(DEPLIBS_testclient)
$(LINK.cc) $(CCFLAGS_testclient) $(CPPFLAGS_testclient) -o $@ $(OBJS_testclient) $(LDLIBS_testclient) -L../../../../boost_1_67_0/stage/lib -lboost_system -lboost_filesystem -lpthread -lboost_thread -Wl,-rpath,$(BOOST_SHARED_LIBRARY_INSTALL_PATH)


# Compile source files into .o files
$(TARGETDIR_testserver)/base64.o: $(TARGETDIR_testserver) base64.cpp
$(COMPILE.cc) $(CCFLAGS_testclient) $(CPPFLAGS_testclient) -o $@ base64.cpp

$(TARGETDIR_testserver)/testclient.o: $(TARGETDIR_testserver) testclient.cpp
$(COMPILE.cc) $(CCFLAGS_testclient) $(CPPFLAGS_testclient) -o $@ testclient.cpp



#### Clean target deletes all generated files ####
clean:
rm -f \
$(TARGETDIR_testserver)/testserver \
$(TARGETDIR_testserver)/testserver.o \
$(TARGETDIR_testserver)/base64.o \
$(TARGETDIR_testserver)/testclient \
$(TARGETDIR_testserver)/base64.o \
$(TARGETDIR_testserver)/testclient.o \
$(TARGETDIR_testserver)/libboost_system.so.1.67.0 \
$(TARGETDIR_testserver)/libboost_filesystem.so.1.67.0 \
$(TARGETDIR_testserver)/libboost_thread.so.1.67.0
$(CCADMIN)
rm -f -r $(TARGETDIR_testserver)


# Create the target directory (if needed)
$(TARGETDIR_testserver):
mkdir -p $(TARGETDIR_testserver); \
cd $(TARGETDIR_testserver); \
ln -s ../../../../../boost_1_67_0/stage/lib/libboost_system.so.1.67.0 libboost_system.so.1.67.0; \
ln -s ../../../../../boost_1_67_0/stage/lib/libboost_filesystem.so.1.67.0 libboost_filesystem.so.1.67.0; \
ln -s ../../../../../boost_1_67_0/stage/lib/libboost_thread.so.1.67.0 libboost_thread.so.1.67.0

## Target: test_server
runserver:
cd $(TARGETDIR_testserver); \
LD_LIBRARY_PATH=. ; \
./testserver

# Enable dependency checking
.KEEP_STATE:
.KEEP_STATE_FILE:.make.state.GNU-amd64-Linux

Loading