RSocketの紹介

RSocketの概要

1. 前書き

このチュートリアルでは、RSocketと、それがクライアント/サーバー通信を可能にする方法を最初に見ていきます。

2. RSocketとは何ですか?

分散アプリケーションでの使用を目的としたRSocket is a binary, point-to-point communication protocol。 その意味で、HTTPのような他のプロトコルの代替手段を提供します。

RSocketと他のプロトコルの完全な比較は、この記事の範囲外です。 代わりに、RSocketの主要な機能である相互作用モデルに焦点を当てます。

RSocket provides four interaction models.それを念頭に置いて、例を挙げてそれぞれについて説明します。

3. Mavenの依存関係

RSocketの例では、2つの直接的な依存関係のみが必要です。


    io.rsocket
    rsocket-core
    0.11.13


    io.rsocket
    rsocket-transport-netty
    0.11.13

rsocket-coreおよびrsocket-transport-nettyの依存関係は、MavenCentralで利用できます。

An important note is that the RSocket library makes frequent use of reactive streamsFluxクラスとMonoクラスはこの記事全体で使用されているため、これらの基本的な理解が役立ちます。

4. サーバーのセットアップ

まず、Serverクラスを作成しましょう。

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.クライアントからの要求を処理するためにカスタムRSocketImplを渡します。 進むにつれて、RSocketImplにメソッドを追加します。

次に、サーバーを起動するには、インスタンスを作成するだけです。

Server server = new Server();

A single server instance can handle multiple connections。 その結果、1つのサーバーインスタンスのみがすべてのサンプルをサポートします。

終了すると、disposeメソッドはサーバーを停止し、TCPポートを解放します。

4. 相互作用モデル

4.1. Request/Response

RSocketは、リクエスト/レスポンスモデルを提供します。各リクエストは単一のレスポンスを受け取ります。

このモデルでは、クライアントにメッセージを返す簡単なサービスを作成します。

AbstractRSocket, 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);
    }
}

Mono<Payload>応答タイプからわかるように、The requestResponse method returns a single result for each request

Payload is the class that contains message content and metadata。 すべてのインタラクションモデルで使用されます。 ペイロードのコンテンツはバイナリですが、Stringベースのコンテンツをサポートする便利なメソッドがあります。

次に、クライアントクラスを作成できます。

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

クライアントはRSocketFactory.connect()メソッドを使用して、サーバーとのソケット接続を開始します。 We use the requestResponse method on the socket to send a payload to the server

ペイロードには、クライアントに渡されたStringが含まれています。 When the Mono<Payload>応答が到着しました。getDataUtf8() メソッドを使用して、応答のStringコンテンツにアクセスできます。

最後に、統合テストを実行して、実際のリクエスト/レスポンスを確認できます。 Stringをサーバーに送信し、同じStringが返されることを確認します。

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

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

    client.dispose();
}

4.2. 火と忘れ

ファイアアンドフォーゲットモデルでは、the client will receive no response from the server

この例では、クライアントは50ミリ秒間隔でシミュレートされた測定値をサーバーに送信します。 サーバーは測定値を公開します。

サーバーのRSocketImplクラスにファイアアンドフォーゲットハンドラーを追加しましょう。

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

このハンドラーは、要求/応答ハンドラーと非常によく似ています。 ただし、Mono<Payload>ではなくfireAndForget returns Mono<Void>

dataPublisherorg.reactivestreams.Publisherのインスタンスです。 したがって、ペイロードをサブスクライバーが利用できるようにします。 リクエスト/ストリームの例でそれを利用します。

次に、ファイアアンドフォーゲットクライアントを作成します。

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

    // ...
}

ソケットのセットアップは以前とまったく同じです。

sendData()メソッドは、Fluxストリームを使用して複数のメッセージを送信します。 For each message, we invoke socket::fireAndForget

We need to subscribe to the Mono<Void> response for each message。 サブスクライブを忘れると、socket::fireAndForgetは実行されません。

flatMap演算子は、Void応答がサブスクライバーに渡されることを確認し、blockLastオペレーターはサブスクライバーとして機能します。

次のセクションまでファイアアンドフォーゲットテストを実行するのを待ちます。 その時点で、ファイアアンドフォーゲットクライアントによってプッシュされたデータを受信するためのリクエスト/ストリームクライアントを作成します。

4.3. Request/Stream

リクエスト/ストリームモデルでは、a single request may receive multiple responses。 これを実際に見るために、火と忘れの例に基づいて構築することができます。 そのために、前のセクションで送信した測定値を取得するためのストリームをリクエストしましょう。

前と同じように、サーバー上のRSocketImplに新しいリスナーを追加することから始めましょう。

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

The requestStream handler returns a Flux<Payload> stream。 前のセクションで思い出したように、fireAndForgetハンドラーは着信データをdataPublisher.に公開しました。次に、イベントソースと同じdataPublisherを使用してFluxストリームを作成します。 。 これを行うことにより、測定データは、fire-and-forgetクライアントからrequest / streamクライアントに非同期的に流れます。

次に、リクエスト/ストリームクライアントを作成しましょう。

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

以前のクライアントと同じ方法でサーバーに接続します。

getDataStream()we use socket.requestStream() to receive a Flux<Payload> stream from the server。 そのストリームから、バイナリデータからFloat値を抽出します。 最後に、ストリームが呼び出し元に返され、呼び出し元がそれをサブスクライブして結果を処理できるようにします。

それでは、テストしてみましょう。 We’ll verify the round trip from fire-and-forget to request/stream.

各値は、送信されたときと同じ順序で受信されると断言できます。 次に、送信されたのと同じ数の値を受け取ったことをアサートできます。

@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. チャネル

The channel model provides bidirectional communication。 このモデルでは、メッセージストリームは双方向に非同期で流れます。

これをテストするための簡単なゲームシミュレーションを作成しましょう。 このゲームでは、チャンネルの両側がプレイヤーになります。 ゲームが実行されると、これらのプレーヤーはランダムな時間間隔で反対側にメッセージを送信します。 反対側はメッセージに反応します。

まず、サーバー上にハンドラーを作成します。 前と同じように、RSocketImplに次のように追加します。

@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 outputPublisher<Payload>入力パラメータは、クライアントから受信したペイロードのストリームです。 それらが到着すると、これらのペイロードはgameController::processPayload関数に渡されます。

それに応じて、別のFluxストリームをクライアントに返します。 このストリームは、PublisherでもあるgameControllerから作成されます。

GameControllerクラスの概要は次のとおりです。

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

GameControllerがサブスクライバーを受信すると、そのサブスクライバーへのメッセージの送信を開始します。

次に、クライアントを作成しましょう。

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

前の例で見たように、クライアントは他のクライアントと同じ方法でサーバーに接続します。

クライアントは、GameControllerの独自のインスタンスを作成します。

We use socket.requestChannel() to send our Payload stream to the server。 サーバーは、独自のペイロードストリームで応答します。

サーバーから受信したペイロードとして、それらをgameController::processPayloadハンドラーに渡します。

ゲームシミュレーションでは、クライアントとサーバーは互いに鏡像です。 つまり、each side is sending a stream of Payload and receiving a stream of Payload from the other endです。

ストリームは、同期せずに独立して実行されます。

最後に、テストでシミュレーションを実行してみましょう。

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

5. 結論

この紹介記事では、RSocketが提供するインタラクションモデルについて説明しました。 例の完全なソースコードは、Github repositoryにあります。

詳細については、必ずRSocket websiteを確認してください。 特に、FAQおよびMotivationsドキュメントは優れた背景を提供します。