This scheme perfectly fit the use of JSON Patch , which made it possible not to reinvent the wheel to generate and apply patches. Thus, the JSON library, which has built-in support for JSON Patch, became the ideal basis for our library to sync state.
So, after a couple of weeks of work, a small library was written, which included the following communication primitives:
On the basis of these primitives, Client and Server were implemented, allowing to synchronize the state, as well as assign callbacks to its specific changes.
#include <chrono> #include <map> #include <string> #include <vector> #include "syncer.h" using namespace nlohmann; using namespace std; using namespace std::chrono; using namespace syncer; struct Site { int temperature; int pressure; }; static inline void to_json(json& j, const Site& s) { j = json(); j["temperature"] = s.temperature; j["pressure"] = s.pressure; } static inline void from_json(const json& j, Site& s) { s.temperature = j.at("temperature").get<int>(); s.pressure = j.at("pressure").get<int>(); } struct State { map<string, Site> sites; string forecast; }; static inline void to_json(json& j, const State& s) { j = json(); j["sites"] = s.sites; j["forecast"] = s.forecast; } static inline void from_json(const json& j, State& s) { s.sites = j.at("sites").get<map<string, Site>>(); s.forecast = j.at("forecast").get<string>(); } PatchOpRouter<State> CreateRouter() { PatchOpRouter<State> router; router.AddCallback<int>(R"(/sites/(\w+)/temperature)", PATCH_OP_ANY, [] (const State& old, const smatch& m, PatchOp op, int t) { cout << "Temperature in " << m[1].str() << " has changed: " << old.sites.at(m[1].str()).temperature << " -> " << t << endl; }); router.AddCallback<Site>(R"(/sites/(\w+)$)", PATCH_OP_ADD, [] (const State&, const smatch& m, PatchOp op, const Site& s) { cout << "Site added: " << m[1].str() << " (temperature: " << s.temperature << ", pressure: " << s.pressure << ")" << endl; }); router.AddCallback<Site>(R"(/sites/(\w+)$)", PATCH_OP_REMOVE, [] (const State&, const smatch& m, PatchOp op, const Site&) { cout << "Site removed: " << m[1].str() << endl; }); return router; } int main() { State state; state.sites["forest"] = { 51, 29 }; state.sites["lake"] = { 49, 31 }; state.forecast = "cloudy and rainy"; Server<State> server("tcp://*:5000", "tcp://*:5001", state); Client<State> client("tcp://localhost:5000", "tcp://localhost:5001", CreateRouter()); this_thread::sleep_for(milliseconds(100)); cout << "Forecast: " << client.data().forecast << endl; state.sites.erase("lake"); state.sites["forest"] = { 50, 28 }; state.sites["desert"] = { 55, 30 }; state.forecast = "cloudy and rainy"; server.Update(state); this_thread::sleep_for(milliseconds(100)); return 0; }
The result of this code is the following output:
Site added: forest (temperature: 51, pressure: 29)
Site added: lake (temperature: 49, pressure: 31)
Forecast: cloudy and rainy
Temperature in forest has changed: 51 -> 50
Site removed: lake
Site added: desert (temperature: 55, pressure: 30)
Of course, the chosen approach is far from optimal in terms of performance, since it generously allocates threads for individual sockets, instead of using Epoll. Therefore, it will be poorly suited for systems requiring a large number of simultaneous connections. Hopefully, for most cases this is uncritical.
So, the opportunity to greatly simplify most of the interprocess communication. It will not be so easy to do for legacy-code, since manual checks of changes are strongly mixed with the rest of the functionality, and therefore you have to cut it “alive”. On the other hand, to implement synchronization for the new code became one pleasure.
Source: https://habr.com/ru/post/340654/
All Articles