Einführung in RSocket

Einführung in RSocket

1. Einführung

In diesem Tutorial werfen wir einen ersten Blick aufRSocket und wie es die Client-Server-Kommunikation ermöglicht.

2. Was ist RSocket?

RSocket is a binary, point-to-point communication protocol zur Verwendung in verteilten Anwendungen. In diesem Sinne bietet es eine Alternative zu anderen Protokollen wie HTTP.

Ein vollständiger Vergleich zwischen RSocket und anderen Protokollen würde den Rahmen dieses Artikels sprengen. Stattdessen konzentrieren wir uns auf ein Hauptmerkmal von RSocket: seine Interaktionsmodelle.

RSocket provides four interaction models. In diesem Sinne werden wir jedes anhand eines Beispiels untersuchen.

3. Maven-Abhängigkeiten

RSocket benötigt für unsere Beispiele nur zwei direkte Abhängigkeiten:


    io.rsocket
    rsocket-core
    0.11.13


    io.rsocket
    rsocket-transport-netty
    0.11.13

Die Abhängigkeitenrsocket-core undrsocket-transport-netty sind in Maven Central verfügbar.

An important note is that the RSocket library makes frequent use of reactive streams. Die KlassenFlux undMono werden in diesem Artikel verwendet, sodass ein grundlegendes Verständnis dieser Klassen hilfreich ist.

4. Server-Setup

Erstellen wir zunächst die KlasseServer:

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. Wir übergeben unsere benutzerdefiniertenRSocketImpl, um Anfragen von Kunden zu bearbeiten. Wir werden denRSocketImpl im Laufe der Zeit Methoden hinzufügen.

Als nächstes müssen wir den Server instanziieren, um ihn zu starten:

Server server = new Server();

A single server instance can handle multiple connections. Infolgedessen unterstützt nur eine Serverinstanz alle unsere Beispiele.

Wenn wir fertig sind, stoppt die Methodedisposeden Server und gibt den TCP-Port frei.

4. Interaktionsmodelle

4.1. Request/Response

RSocket bietet ein Anforderungs- / Antwortmodell - jede Anforderung erhält eine einzelne Antwort.

Für dieses Modell erstellen wir einen einfachen Dienst, der eine Nachricht an den Client zurückgibt.

Beginnen wir mit dem Hinzufügen einer Methode zu unserer Erweiterung vonAbstractRSocket, 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, wie wir am AntworttypMono<Payload> sehen können.

Payload is the class that contains message content and metadata. Es wird von allen Interaktionsmodellen verwendet. Der Inhalt der Nutzdaten ist binär, es gibt jedoch praktische Methoden, dieString-basierten Inhalt unterstützen.

Als nächstes können wir unsere Client-Klasse erstellen:

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();
    }
}

Der Client verwendet die MethodeRSocketFactory.connect(), um eine Socket-Verbindung mit dem Server herzustellen. We use the requestResponse method on the socket to send a payload to the server.

Unsere Nutzdaten enthalten dieString, die an den Client übergeben werden. When the Mono<Payload> Antwort kommt an. Wir können diegetDataUtf8() -Smethod verwenden, um auf denString-Inhalt der Antwort zuzugreifen.

Schließlich können wir den Integrationstest ausführen, um die Anforderung / Antwort in Aktion zu sehen. Wir senden einString an den Server und überprüfen, ob dasselbeString zurückgegeben wird:

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

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

    client.dispose();
}

4.2. Feuer und vergessen

Mit dem Fire-and-Forget-Modellthe client will receive no response from the server.

In diesem Beispiel sendet der Client simulierte Messungen in Abständen von 50 ms an den Server. Der Server veröffentlicht die Messungen.

Fügen wir unserem Server in der KlasseRSocketImpleinen Fire-and-Forget-Handler hinzu:

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

Dieser Handler sieht dem Request / Response-Handler sehr ähnlich. fireAndForget returns Mono<Void> anstelle vonMono<Payload>.

dataPublisher ist eine Instanz vonorg.reactivestreams.Publisher. Somit wird die Nutzlast den Abonnenten zur Verfügung gestellt. Wir werden dies im Anforderungs- / Stream-Beispiel verwenden.

Als Nächstes erstellen wir den Fire-and-Forget-Client:

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();
    }

    // ...
}

Das Socket-Setup ist genau das gleiche wie zuvor.

Die MethodesendData()verwendet einen Stream vonFlux, um mehrere Nachrichten zu senden. For each message, we invoke socket::fireAndForget.

We need to subscribe to the Mono<Void> response for each message. Wenn wir vergessen, uns anzumelden, wirdsocket::fireAndForget nicht ausgeführt.

Der OperatorflatMap stellt sicher, dass die Antworten vonVoidan den Teilnehmer weitergeleitet werden, während der OperatorblockLastals Teilnehmer fungiert.

Wir werden bis zum nächsten Abschnitt warten, um den Feuer-und-Vergessen-Test durchzuführen. Zu diesem Zeitpunkt erstellen wir einen Anforderungs- / Stream-Client, um die Daten zu empfangen, die vom Fire-and-Forget-Client übertragen wurden.

4.3. Request/Stream

Im Anforderungs- / Stream-Modella single request may receive multiple responses. Um dies in Aktion zu sehen, können wir auf dem Feuer-und-Vergessen-Beispiel aufbauen. Fordern Sie dazu einen Stream an, um die im vorherigen Abschnitt gesendeten Messungen abzurufen.

Beginnen wir wie zuvor damit, denRSocketImpl auf dem Server einen neuen Listener hinzuzufügen:

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

The requestStream handler returns a Flux<Payload> stream. Wie wir uns aus dem vorherigen Abschnitt erinnern, hat der Handler vonfireAndForgeteingehende Daten fürdataPublisher. veröffentlicht. Jetzt erstellen wir einen Stream vonFlux mit denselbendataPublisher wie die Ereignisquelle . Auf diese Weise fließen die Messdaten asynchron von unserem Fire-and-Forget-Client zu unserem Request / Stream-Client.

Erstellen wir als Nächstes den Anforderungs- / Stream-Client:

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();
    }
}

Wir verbinden uns mit dem Server auf die gleiche Weise wie unsere vorherigen Kunden.

IngetDataStream()we use socket.requestStream() to receive a Flux<Payload> stream from the server. Aus diesem Stream extrahieren wir dieFloat-Werte aus den Binärdaten. Schließlich wird der Stream an den Anrufer zurückgegeben, sodass der Anrufer ihn abonnieren und die Ergebnisse verarbeiten kann.

Jetzt testen wir. We’ll verify the round trip from fire-and-forget to request/stream.

Wir können behaupten, dass jeder Wert in der gleichen Reihenfolge empfangen wird, in der er gesendet wurde. Dann können wir behaupten, dass wir die gleiche Anzahl von Werten erhalten, die gesendet wurden:

@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. Kanal

The channel model provides bidirectional communication. In diesem Modell fließen Nachrichtenströme asynchron in beide Richtungen.

Erstellen wir eine einfache Spielsimulation, um dies zu testen. In diesem Spiel wird jede Seite des Kanals zum Spieler. Während das Spiel läuft, senden diese Spieler in zufälligen Zeitintervallen Nachrichten an die andere Seite. Die Gegenseite reagiert auf die Nachrichten.

Zunächst erstellen wir den Handler auf dem Server. Wie zuvor addieren wir zuRSocketImpl:

@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. Der EingabeparameterPublisher<Payload>ist ein Strom von Nutzdaten, die vom Client empfangen werden. Bei ihrer Ankunft werden diese Nutzdaten an die FunktiongameController::processPayloadübergeben.

Als Antwort geben wir einen anderenFlux-Stream an den Client zurück. Dieser Stream wird aus unserengameController erstellt, die auchPublisher sind.

Hier ist eine Zusammenfassung der KlasseGameController:

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
    }
}

WennGameController einen Teilnehmer empfängt, beginnt es, Nachrichten an diesen Teilnehmer zu senden.

Als Nächstes erstellen wir den 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();
    }
}

Wie wir in unseren vorherigen Beispielen gesehen haben, stellt der Client auf dieselbe Weise wie die anderen Clients eine Verbindung zum Server her.

Der Client erstellt eine eigene Instanz derGameController.

We use socket.requestChannel() to send our Payload stream to the server. Der Server antwortet mit einem eigenen Payload-Stream.

Als vom Server empfangene Nutzdaten übergeben wir sie an den Handler vongameController::processPayload.

In unserer Spielsimulation sind Client und Server Spiegelbilder voneinander. Das heißt,each side is sending a stream of Payload and receiving a stream of Payload from the other end.

Die Streams werden unabhängig voneinander ohne Synchronisierung ausgeführt.

Lassen Sie uns abschließend die Simulation in einem Test ausführen:

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

5. Fazit

In diesem Einführungsartikel haben wir die von RSocket bereitgestellten Interaktionsmodelle untersucht. Den vollständigen Quellcode der Beispiele finden Sie in unserenGithub repository.

Schauen Sie sich unbedingt dieRSocket websitean, um eine eingehendere Diskussion zu erhalten. Insbesondere die DokumenteFAQ undMotivations bieten einen guten Hintergrund.