Client

In this example we’re going to create a pars application by deriving from an app::single class that allows us to create an application that uses just one component, namely comp::client in this case.

If you’re interested in building the library and examples as well as running the examples, take a look at the building page.

Main

The main function is straightforward. We just instantiate ::pars_example::apps::client, and then execute it passing argc and argv. Should any exception arise, we will print the exception message to standard output:

 1int main(int argc, char** argv)
 2{
 3  auto app = ::pars_example::apps::client{};
 4
 5  try
 6  {
 7    return app.exec(argc, argv);
 8  }
 9  catch (std::exception& e)
10  {
11    std::cout << fmt::format("Error: {}", e.what()) << "\n";
12
13    return EXIT_FAILURE;
14  }
15}

Application

We start by declaring such application:

1class client : public app::single<comp::client>

Then we override the only functions that we are required to implement in order to have a runnable single application (ie: the startup function):

1  void startup(int argc, char** argv) override

Startup

We are going to leverage the pars::app::state_machine to handle the application state, which can be in one of the cases defined in resource::client_state.

1  app::state_machine<client_state> state = {client_state::creating};

As a first thing hence, we’re going to ask for a transition from the actual expected state to the next state. This will check that the current application state is resource::client_state::creating (and will throws if it’s not) and creates a pars::app::state_tx object that initiate the transition on the pars::app::state_machine.

Once we’re done, we’re going to commit this state transition in order for the state to change to resource::client_state::initializing. Should any exception be raised after the creation of this pars::app::state_tx, the initiated transition will be rolled back and the app state machine will remain in the initial state:

1    auto ts = state.tx(client_state::creating, client_state::initializing);

Next we’re going to read the inputs:

 1    switch (argc)
 2    {
 3    case 6:
 4      connect_p.service_cmode = net::cmode_from_string(argv[1]);
 5
 6      connect_p.service_addr = argv[2];
 7
 8      work_id = std::stoull(argv[3]);
 9
10      if (std::string_view(argv[4]).compare("fast") == 0)
11        fast_fib = true;
12      else if (std::string_view(argv[4]).compare("slow") == 0)
13        fast_fib = false;
14      else
15        usage();
16
17      n = std::stoull(argv[5]);
18
19      break;
20
21    default:
22      usage();
23    }

Now we register the handler functions (ie: the ones that react to various kinds of events) for the events that we’re intrested in:

 1    hfs().on<fired, init>(&self::initialize, this);
 2
 3    hfs().on<fired, exception>(&self::terminate, this);
 4
 5    comp().req().on<fired, pipe_created>(&self::send_work, this);
 6
 7    comp().req().on<sent, fib_requested>(&self::recv_answer, this);
 8
 9    comp().req().on<received, fib_computed>(&self::terminate, this);
10
11    comp().req().on<fired, network_error>(&self::terminate, this);
12
13    comp().req().on<fired, pipe_removed>(&self::terminate, this);

Finally as anticipated, we just commit the state transition:

1    ts.commit();
2
3    pars::info(SL, "Application Started!");

Handler Functions

We’re going now to briefly comment all the handler functions that we registered during startup (see previous section).

This application is made out of a client component, which has a single nng_req socket. These handler functions are going to connect to the server, send the request and print the output or any error that could arise.

On fired<init> we’re going to initialize the client component and connect to the specified address using the given mode:

 1  void initialize(hf_arg<fired, init> fired)
 2  {
 3    auto ts = state.tx(client_state::initializing, client_state::started);
 4
 5    comp().init({.req_opts = {.recv_timeout = req_recv_timeout.count(),
 6                              .send_timeout = req_send_timeout.count()}});
 7
 8    comp().connect(connect_p);
 9
10    ts.commit();
11
12    pars::info(SL, "Fired {}, Application Initialized!", fired.event());
13  }

On fired<pipe_created> (ie: connection established), we’re going to start a send operation on the req socket:

 1  void send_work(hf_arg<fired, pipe_created> fired)
 2  {
 3    auto ts = state.tx(client_state::started, client_state::sending_work);
 4
 5    auto [ev, md] = fired.as_tuple();
 6
 7    auto out_ev = fib_requested{work_id, n, fast_fib};
 8
 9    // use the default context on the sock to send the event
10    comp().req().sock().send(out_ev, md.pipe());
11
12    ts.commit();
13
14    pars::info(SL, "Fired {}, Sent {}!", ev, out_ev);
15  }

On sent<event::fib_requested> we start a receive operation:

 1  void recv_answer(hf_arg<sent, fib_requested> sent)
 2  {
 3    auto ts =
 4      state.tx(client_state::sending_work, client_state::waiting_work_done);
 5
 6    // recv on the default context of the sock
 7    comp().req().sock().recv();
 8
 9    ts.commit();
10
11    pars::info(SL, "Sent {}, Receiving!", sent.event());
12  }

On received<event::fib_computed> we terminate the component and print the result:

 1  void terminate(hf_arg<received, fib_computed> recv)
 2  {
 3    auto ts =
 4      state.tx(client_state::waiting_work_done, client_state::terminating);
 5
 6    auto& ev = recv.event();
 7
 8    graceful_terminate();
 9
10    ts.commit();
11
12    pars::info(SL, "Received {}, Application Terminated!", ev);
13
14    std::cout << "WORK(" << ev.work_id << ") FIB(" << n << ") = " << ev.fib_n
15              << "\n";
16  }

On fired<exception> (ie: an exception was thrown during an execution of some handler function), we just terminate and print the exception message:

 1  void terminate(hf_arg<fired, exception> fired)
 2  {
 3    auto ts = state.tx(client_state::terminated);
 4
 5    auto& ev = fired.event();
 6
 7    graceful_terminate();
 8
 9    ts.commit();
10
11    pars::info(SL, "Fired {} while \"{}\", Application Terminated!", ev,
12               state.current());
13
14    std::cout << fmt::format("ERROR: {}", ev) << std::endl;
15  }

On fired<network_error> (ie: we got some error while executing a send or recv operation), we just terminate and print the error message:

 1  void terminate(hf_arg<fired, network_error> fired)
 2  {
 3    auto ts = state.tx(client_state::terminated);
 4
 5    auto& ev = fired.event();
 6
 7    graceful_terminate();
 8
 9    ts.commit();
10
11    pars::info(SL, "Fired {} while \"{}\", Application Terminated!", ev,
12               state.current());
13
14    std::cout << fmt::format("ERROR: {}", ev.error) << std::endl;
15  }

On fired<pipe_removed> (ie: connection gone), we just terminate and inform the client was disconnected:

 1  void terminate(hf_arg<fired, pipe_removed> fired)
 2  {
 3    auto ts = state.tx(client_state::terminated);
 4
 5    graceful_terminate();
 6
 7    ts.commit();
 8
 9    pars::info(SL, "Fired {} while \"{}\", Application Terminated!",
10               fired.event(), state.current());
11
12    std::cout << fmt::format("Client Disconnected!") << std::endl;
13  }