Introduction à RSocket

Introduction à RSocket

1. introduction

Dans ce didacticiel, nous allons jeter un premier regard surRSocket et sur la manière dont il permet la communication client-serveur.

2. Qu'est ce que RSocket?

RSocket is a binary, point-to-point communication protocol destiné à être utilisé dans des applications distribuées. En ce sens, il offre une alternative aux autres protocoles tels que HTTP.

Une comparaison complète entre RSocket et d'autres protocoles dépasse le cadre de cet article. Au lieu de cela, nous nous concentrerons sur une caractéristique clé de RSocket: ses modèles d'interaction.

RSocket provides four interaction models. Dans cet esprit, nous allons explorer chacun d'eux avec un exemple.

3. Dépendances Maven

RSocket n'a besoin que de deux dépendances directes pour nos exemples:


    io.rsocket
    rsocket-core
    0.11.13


    io.rsocket
    rsocket-transport-netty
    0.11.13

Les dépendancesrsocket-core etrsocket-transport-netty sont disponibles sur Maven Central.

An important note is that the RSocket library makes frequent use of reactive streams. Les classesFlux etMono sont utilisées tout au long de cet article, donc une compréhension de base de celles-ci sera utile.

4. Configuration du serveur

Commençons par créer la classeServer:

public class Server {
    private final Disposable server;

    public Server() {
        this.server = RSocketFactory.receive()
          .acceptor((setupPayload, reactiveSocket) -> Mono.just(new RSocketImpl()))
          .transport(TcpServerTransport.create("localhost", TCP_PORT))
          .start()
          .subscribe();
    }

    public void dispose() {
        this.server.dispose();
    }

    private class RSocketImpl extends AbstractRSocket {}
}

Here we use the RSocketFactory to set up and listen to a TCP socket. Nous transmettons nosRSocketImpl personnalisés pour gérer les demandes des clients. Nous ajouterons des méthodes auxRSocketImpl au fur et à mesure.

Ensuite, pour démarrer le serveur, il suffit de l’instancier:

Server server = new Server();

A single server instance can handle multiple connections. En conséquence, une seule instance de serveur prend en charge tous nos exemples.

Lorsque nous avons terminé, la méthodedispose arrête le serveur et libère le port TCP.

4. Modèles d'interaction

4.1. Request/Response

RSocket fournit un modèle de demande / réponse - chaque demande reçoit une seule réponse.

Pour ce modèle, nous allons créer un service simple qui renvoie un message au client.

Commençons par ajouter une méthode à notre extension deAbstractRSocket, RSocketImpl:

@Override
public Mono requestResponse(Payload payload) {
    try {
        return Mono.just(payload); // reflect the payload back to the sender
    } catch (Exception x) {
        return Mono.error(x);
    }
}

The requestResponse method returns a single result for each request, comme nous pouvons le voir par le type de réponseMono<Payload>.

Payload is the class that contains message content and metadata. Il est utilisé par tous les modèles d'interaction. Le contenu de la charge utile est binaire, mais il existe des méthodes pratiques qui prennent en charge le contenu basé surString.

Ensuite, nous pouvons créer notre classe de clients:

public class ReqResClient {

    private final RSocket socket;

    public ReqResClient() {
        this.socket = RSocketFactory.connect()
          .transport(TcpClientTransport.create("localhost", TCP_PORT))
          .start()
          .block();
    }

    public String callBlocking(String string) {
        return socket
          .requestResponse(DefaultPayload.create(string))
          .map(Payload::getDataUtf8)
          .block();
    }

    public void dispose() {
        this.socket.dispose();
    }
}

Le client utilise la méthodeRSocketFactory.connect() pour initier une connexion socket avec le serveur. We use the requestResponse method on the socket to send a payload to the server.

Notre charge utile contient lesString transmis au client. La réponseWhen the Mono<Payload> arrive, nous pouvons utiliser la méthodegetDataUtf8() pour accéder au contenuString de la réponse.

Enfin, nous pouvons exécuter le test d'intégration pour voir la demande / réponse en action. Nous enverrons unString au serveur et vérifierons que le mêmeString est renvoyé:

@Test
public void whenSendingAString_thenRevceiveTheSameString() {
    ReqResClient client = new ReqResClient();
    String string = "Hello RSocket";

    assertEquals(string, client.callBlocking(string));

    client.dispose();
}

4.2. Feu et oublie

Avec le modèle feu et oublie,the client will receive no response from the server.

Dans cet exemple, le client enverra des mesures simulées au serveur toutes les 50 ms. Le serveur publiera les mesures.

Ajoutons un gestionnaire d'incendie et d'oubli à notre serveur dans la classeRSocketImpl:

@Override
public Mono fireAndForget(Payload payload) {
    try {
        dataPublisher.publish(payload); // forward the payload
        return Mono.empty();
    } catch (Exception x) {
        return Mono.error(x);
    }
}

Ce gestionnaire ressemble beaucoup au gestionnaire de demandes / réponses. Cependant,fireAndForget returns Mono<Void> au lieu deMono<Payload>.

LedataPublisher est une instance deorg.reactivestreams.Publisher. Ainsi, il met la charge utile à la disposition des abonnés. Nous utiliserons cela dans l'exemple de requête / flux.

Ensuite, nous allons créer le client Fire-and-forget:

public class FireNForgetClient {
    private final RSocket socket;
    private final List data;

    public FireNForgetClient() {
        this.socket = RSocketFactory.connect()
          .transport(TcpClientTransport.create("localhost", TCP_PORT))
          .start()
          .block();
    }

    /** Send binary velocity (float) every 50ms */
    public void sendData() {
        data = Collections.unmodifiableList(generateData());
        Flux.interval(Duration.ofMillis(50))
          .take(data.size())
          .map(this::createFloatPayload)
          .flatMap(socket::fireAndForget)
          .blockLast();
    }

    // ...
}

La configuration de la prise est identique à celle d’avant.

La méthodesendData() utilise un fluxFlux pour envoyer plusieurs messages. For each message, we invoke socket::fireAndForget.

We need to subscribe to the Mono<Void> response for each message. Si nous oublions de nous abonner,socket::fireAndForget ne s'exécutera pas.

L'opérateurflatMap s'assure que les réponsesVoid sont transmises à l'abonné, tandis que l'opérateurblockLast agit en tant qu'abonné.

Nous allons attendre la section suivante pour exécuter le test d'incendie et d'oubli. À ce stade, nous allons créer un client de demande / flux pour recevoir les données transmises par le client d'incendie et d'oubli.

4.3. Request/Stream

Dans le modèle de requête / flux,a single request may receive multiple responses. Pour voir cela en action, nous pouvons nous appuyer sur l'exemple du feu et oublier. Pour ce faire, demandons un flux pour récupérer les mesures que nous avons envoyées dans la section précédente.

Comme précédemment, commençons par ajouter un nouvel écouteur auxRSocketImpl sur le serveur:

@Override
public Flux requestStream(Payload payload) {
    return Flux.from(dataPublisher);
}

The requestStream handler returns a Flux<Payload> stream. Comme nous l'avons rappelé dans la section précédente, le gestionnaire defireAndForget a publié les données entrantes vers lesdataPublisher. Maintenant, nous allons créer un fluxFlux en utilisant les mêmesdataPublisher que la source d'événement . Ainsi, les données de mesure seront transmises de manière asynchrone de notre client fire-and-oublier à notre client request / stream.

Créons ensuite le client de demande / flux:

public class ReqStreamClient {

    private final RSocket socket;

    public ReqStreamClient() {
        this.socket = RSocketFactory.connect()
          .transport(TcpClientTransport.create("localhost", TCP_PORT))
          .start()
          .block();
    }

    public Flux getDataStream() {
        return socket
          .requestStream(DefaultPayload.create(DATA_STREAM_NAME))
          .map(Payload::getData)
          .map(buf -> buf.getFloat())
          .onErrorReturn(null);
    }

    public void dispose() {
        this.socket.dispose();
    }
}

Nous nous connectons au serveur de la même manière que nos clients précédents.

EngetDataStream()we use socket.requestStream() to receive a Flux<Payload> stream from the server. De ce flux, nous extrayons les valeursFloat des données binaires. Enfin, le flux est renvoyé à l'appelant, ce qui lui permet de s'y abonner et de traiter les résultats.

Maintenant, testons. We’ll verify the round trip from fire-and-forget to request/stream.

Nous pouvons affirmer que chaque valeur est reçue dans le même ordre qu’elle a été envoyée. Ensuite, nous pouvons affirmer que nous recevons le même nombre de valeurs envoyées:

@Test
public void whenSendingStream_thenReceiveTheSameStream() {
    FireNForgetClient fnfClient = new FireNForgetClient();
    ReqStreamClient streamClient = new ReqStreamClient();

    List data = fnfClient.getData();
    List dataReceived = new ArrayList<>();

    Disposable subscription = streamClient.getDataStream()
      .index()
      .subscribe(
        tuple -> {
            assertEquals("Wrong value", data.get(tuple.getT1().intValue()), tuple.getT2());
            dataReceived.add(tuple.getT2());
        },
        err -> LOG.error(err.getMessage())
      );

    fnfClient.sendData();

    // ... dispose client & subscription

    assertEquals("Wrong data count received", data.size(), dataReceived.size());
}

4.4. Canal

The channel model provides bidirectional communication. Dans ce modèle, les flux de messages circulent de manière asynchrone dans les deux sens.

Créons une simulation de jeu simple pour tester cela. Dans ce jeu, chaque côté du canal deviendra un joueur. Pendant le jeu, ces joueurs enverront des messages de l'autre côté à intervalles aléatoires. Le côté opposé réagira aux messages.

Tout d'abord, nous allons créer le gestionnaire sur le serveur. Comme avant, on ajoute auxRSocketImpl:

@Override
public Flux requestChannel(Publisher payloads) {
    Flux.from(payloads)
      .subscribe(gameController::processPayload);
    return Flux.from(gameController);
}

The requestChannel handler has Payload streams for both input and output. Le paramètre d'entréePublisher<Payload> est un flux de charges utiles reçues du client. À leur arrivée, ces charges utiles sont transmises à la fonctiongameController::processPayload.

En réponse, nous renvoyons un fluxFlux différent au client. Ce flux est créé à partir de nosgameController, qui est également unPublisher.

Voici un résumé de la classeGameController:

public class GameController implements Publisher {

    @Override
    public void subscribe(Subscriber subscriber) {
        // send Payload messages to the subscriber at random intervals
    }

    public void processPayload(Payload payload) {
        // react to messages from the other player
    }
}

Lorsque leGameController reçoit un abonné, il commence à envoyer des messages à cet abonné.

Ensuite, créons le client:

public class ChannelClient {

    private final RSocket socket;
    private final GameController gameController;

    public ChannelClient() {
        this.socket = RSocketFactory.connect()
          .transport(TcpClientTransport.create("localhost", TCP_PORT))
          .start()
          .block();

        this.gameController = new GameController("Client Player");
    }

    public void playGame() {
        socket.requestChannel(Flux.from(gameController))
          .doOnNext(gameController::processPayload)
          .blockLast();
    }

    public void dispose() {
        this.socket.dispose();
    }
}

Comme nous l'avons vu dans nos exemples précédents, le client se connecte au serveur de la même manière que les autres clients.

Le client crée sa propre instance desGameController.

We use socket.requestChannel() to send our Payload stream to the server. Le serveur répond avec son propre flux de données.

En tant que charges utiles reçues du serveur, nous les transmettons à notre gestionnairegameController::processPayload.

Dans notre simulation de jeu, le client et le serveur sont des images inversées l’une de l’autre. Autrement dit,each side is sending a stream of Payload and receiving a stream of Payload from the other end.

Les flux s'exécutent indépendamment, sans synchronisation.

Enfin, exécutons la simulation dans un test:

@Test
public void whenRunningChannelGame_thenLogTheResults() {
    ChannelClient client = new ChannelClient();
    client.playGame();
    client.dispose();
}

5. Conclusion

Dans cet article d'introduction, nous avons exploré les modèles d'interaction fournis par RSocket. Le code source complet des exemples peut être trouvé dans nosGithub repository.

Assurez-vous de consulter lesRSocket website pour une discussion plus approfondie. En particulier, les documentsFAQ etMotivations fournissent un bon fond.