Sending and receiving messages

In this article, we will describe how a client can send and receive messages when connected to a scene.

A client must be connected to a scene to be able to send and receive data to/from it or to/from other peers connected to it.

Messaging API behave the same way in client code as in server code. Therefore, in the following document, we will use the term “peer” to reference both the client and server.

There are two ways for clients and servers to send messages and requests to each other: Fire & forget calls (FFC) and remote procedure calls (RPC).

FFC (Fire & Forget Calls)

Fire & forget messaging is the simplest kind of communication occuring between 2 peers.

FFC messages have the following properties (without writing additionnal code):

  • The caller can’t know when, or if the message arrived successfully, depending on the selected reliability for the call.

  • The callee can’t send a response back to the caller.

  • They suffer from a very low bandwidth overhead (no overhead for unreliable FFC, acknowledgment and potential retries for reliable).

To send an FFC to another peer, use the method send() on a scene or peer object.

Sending

Send a message from a C++ client to a scene’s host (currently, the scene host is always the Stormancer server):

using namespace Stormancer;
Scene_ptr scene;

// Send the int values 10, then 15, to the scene's host (currently, this is always the Stormancer server) on the route "my_int_route"
scene->send(PeerFilter::matchSceneHost(), "my_int_route", 10);
scene->send(PeerFilter::matchSceneHost(), "my_int_route", 15);

Since we did not specify the reliability of the messages, they use the default reliability, which is RELIABLE_ORDERED. This guarantees that your messages will always reach their destination, in the order in which they were sent. In our example, the server will always receive first 10, then 15, on the route "my_int_route".

Note however, that the ordering between different routes is unspecified:

scene->send(PeerFilter::matchSceneHost(), "my_int_route", 10);
scene->send(PeerFilter::matchSceneHost(), "my_float_route", 10.5);
scene->send(PeerFilter::matchSceneHost(), "my_int_route", 15);

In the above example, the value 10.5 on "my_float_route" is NOT guaranteed to be received after the value 10 on "my_int_route" and before the value 15. Of course the ordering guarantee for the values 10 and 15 on "my_int_route" still holds.

Send a message from a scene on the server to a client connected to the scene:

ISceneHost scene;
IScenePeerClient peer;

// Routes are bidirectional: messages can be sent both from the client to the server, and vice versa, on the same route.
scene.Send(new MatchPeerFilter(peer), "my_int_route", 20);
scene.Send(new MatchPeerFilter(peer), "my_int_route", 30);

The same reliability and ordering concerns apply to messages sent by the server. They have the same defaults: in the example above, where we didn’t specify a reliability mode, the messages are guaranteed to be received by the client in the order in which they were sent.

Receiving

On the client, handlers for FFC messages can be set using the Scene::addRoute() method. One call to addRoute() must be made for each route, when connecting to a scene, inside the scene initializer.

using namespace Stormancer;
std::shared_ptr<IClient> client;

client->connectToPublicScene("my_scene",
        // This is the scene initializer, where the route handlers must be registered with addRoute().
        [](std::shared_ptr<Scene> scene)
{
        // Register a handler for the route "my_int_route", which expects an integer value.
        scene->addRoute("my_int_route", [](int value)
        {
                std::cout << "Received the value " << value << std::endl;
        });
}).wait();

On the server, route handlers should be put inside a Controller class. Controller classes inherit from ControllerBase. To create a handler for a route, add a method to a controller class, and annotate it with the Api attribute, like so:

class MySceneController : ControllerBase
{
        [Api(ApiType = ApiType.FireForget, ApiAccess = ApiAccess.Public, Route = "my_int_route")]
        public void MyIntRouteHandler(int value)
        {
                // Do something with value
        }
}

Sending structured data

In addition to basic types like int and float, Stormancer also supports sending structured types

FFC Reliability options

Fire & Forget messages can be sent with different reliability options. Each of them have different advantages and drawbacks. Which to choose depends on the type of message you want to send.

For instance,

RELIABLE_ORDERED,

This message is reliable and will arrive in the order you sent it. Messages will be delayed while waiting for out of order messages. Same overhead as UNRELIABLE_SEQUENCED. Sequenced and ordered messages sent on the same channel will arrive in the order sent. This is the default message reliability when sending an FFC.

RELIABLE_SEQUENCED,

This message is reliable and will arrive in the sequence you sent it. Out or order messages will be dropped. Same overhead as UNRELIABLE_SEQUENCED. Sequenced and ordered messages sent on the same channel will arrive in the order sent.

RELIABLE

The message is sent reliably, but not necessarily in any order. Same overhead as UNRELIABLE.

UNRELIABLE

Same as regular UDP, except that it will also discard duplicate datagrams. RakNet (the underlying protocol used by Stormancer) adds (6 to 17) + 21 bits of overhead, 16 of which is used to detect duplicate packets and 6 to 17 of which is used for message length.

UNRELIABLE_SEQUENCED,

Regular UDP with a sequence counter. Out of order messages will be discarded. Sequenced and ordered messages sent on the same channel will arrive in the order sent.

These options are available to both client and server.

RPC (Remote Procedure Calls)

RPCs are built on top of FFCs to provide Request/Response semantics. They have the following properties:

  • For the caller, the RPC completes asynchroously once it completes on the callee

  • The callee can return one or several values before completion of the RPC.

  • The callee can throw exceptions. If the exception is of type ClientException, its message gets send back to the caller. Otherwise, it’s hidden from the caller, and a log with exception details is generated.

  • RPCs are always reliable and complete with an exception in case of connectivity issue

  • RPCs are cancellable. If the caller cancels an RPC, the callee receives a notification through the CancellationToken available in the RPC request context.

  • RPC don’t timeout by default. Use the timeout method to generate a cancellation token that cancels itself after a delay to implement timeout.

  • Up to 65535 RPCs are possible concurrent for a given caller-callee couple.

RPCs are implemented in the RpcService component available on all scenes

Sending an RPC

To send and RPC to a remote host, clients and servers applications must import the RpcService class from the scene dependency scope.

Handling RPCs and FFCs

On the server, the API plugin enables easier creation of both FFC and RPC endpoints by declaring Controllers whose methods will be automatically registered as routes.

class HelloController: ControllerBase
{
    [Api(ApiAccess.Public, ApiType.Rpc)]
    public string World(string name)
    {
        return $"Hello {name}!";
    }

    [Api(ApiAccess.Public, ApiType.FireForget)]
    public void Ffc(string name)
    {
        //handle Fire & forget message
    }
}

This controller adds the routes Hello.World and Hello.Ffc to the scene it is attached to. The API plugin uses the Scene.AddRoute method to add controller actions.