web-dev-qa-db-ja.com

Webflux websocketclient、同じセッションで複数のリクエストを送信する方法[クライアントライブラリの設計]

TL; DR;

Spring Webflux WebSocket実装を使用してWebSocketサーバーを設計しようとしています。サーバーには通常のHTTPサーバー操作があります。 create/fetch/update/fetchall。 WebSocketを使用して、クライアントがすべての種類の操作に単一の接続を利用できるように、1つのエンドポイントを公開しようとしました。 webfluxとWebSocketsを使用した正しい設​​計ですか?

ロングバージョン

spring-webfluxのリアクティブWebソケットを使用するプロジェクトを開始しています。サーバーに接続するためにコンシューマーが使用できるリアクティブクライアントライブラリを構築する必要があります。

サーバー上リクエストを取得し、メッセージを読み取り、保存して静的な応答を返します。

public Mono<Void> handle(WebSocketSession webSocketSession) {
    Flux<WebSocketMessage> response = webSocketSession.receive()
            .map(WebSocketMessage::retain)
            .concatMap(webSocketMessage -> Mono.just(webSocketMessage)
                    .map(parseBinaryToEvent) //logic to get domain object
                    .flatMap(e -> service.save(e))
                    .thenReturn(webSocketSession.textMessage(SAVE_SUCCESSFUL))
            );

    return webSocketSession.send(response);
}

クライアント上、誰かがsaveメソッドを呼び出し、serverから応答を返したときに呼び出しを行います。

public Mono<String> save(Event message) {
    new ReactorNettyWebSocketClient().execute(uri, session -> {
      session
              .send(Mono.just(session.binaryMessage(formatEventToMessage)))
              .then(session.receive()
                      .map(WebSocketMessage::getPayloadAsText)
                      .doOnNext(System.out::println).then()); //how to return this to client
    });
    return null;
}

これをどのように設計するかはわかりません。理想的には、

1)client.executeは一度だけ呼び出す必要があり、何とかsessionを保持します。同じセッションを使用して、後続の呼び出しでデータを送信する必要があります。

2)session.receiveで取得したサーバーからの応答を返す方法は?

3)session.receiveの応答が巨大な場合(静的な文字列だけでなく、イベントのリスト)、fetchの場合はどうですか?

いくつか調査を行っていますが、webflux-websocket-clientのドキュメント/実装に関するオンラインの適切なリソースを見つけることができません。先へ進む方法に関する指針。

11
Mritunjay

お願いします! RSocket を使用してください!

これは完全に正しい設計であり、リソースを節約し、すべての可能な操作に対してクライアントごとの接続のみを使用する価値があります。

ただし、ホイールを実装せず、これらすべての種類の通信を提供するプロトコルを使用しないでください。

  • RSocketにはrequest-responseモデルがあり、今日最も一般的なクライアントとサーバー間の対話を行うことができます。
  • RSocketにはrequest-stream通信モデルがあるため、すべてのニーズを満たし、同じ接続を非同期的に再利用してイベントのストリームを返すことができます。 RSocketは、論理ストリームから物理接続へのマッピングとその逆のマッピングをすべて行うので、自分で行うことの苦痛を感じることはありません。
  • RSocketにはfire-and-forgetstream-streamなどのはるかに多くの相互作用モデルがあり、両方の方法でデータのストリーム。

SpringでRSocketを使用する方法

そのためのオプションの1つは、RSocketプロトコルのRSocket-Java実装を使用することです。 RSocket-JavaはProject Reactorの上に構築されているため、Spring WebFluxエコシステムに自然に適合します。

残念ながら、Springエコシステムとの機能統合はありません。幸い、Spring WebFluxとRSocketを統合し、WebSocket RSocketサーバーとWebFlux Httpサーバーを公開するシンプルな RSocket Spring Boot Starter を提供するために数時間を費やしました。

なぜRSocketの方が優れているのですか?

基本的に、RSocketは、同じアプローチを自分で実装する複雑さを隠します。 RSocketを使用すると、カスタムプロトコルやJavaの実装として対話モデルの定義を気にする必要はありません。 RSocketは、特定の論理チャネルにデータを配信します。同じWS接続にメッセージを送信する組み込みのクライアントを提供するため、そのためのカスタム実装を発明する必要はありません。

RSocket-RPC でさらに改善する

RSocketは単なるプロトコルであるため、メッセージ形式は提供されないため、この課題はビジネスロジックに関するものです。ただし、プロトコル形式のバッファをメッセージ形式として提供し、GRPCと同じコード生成手法を再利用するRSocket-RPCプロジェクトがあります。したがって、RSocket-RPCを使用すると、クライアントとサーバーのAPIを簡単に構築でき、トランスポートとプロトコルの抽象化についてまったく不注意です。

同じRSocket Spring Boot統合は、RSocket-RPC使用法の example も提供します。

了解しました。確信が持てません。カスタムのWebSocketサーバーを使いたいのですが

したがって、そのためには、自分で地獄を実装する必要があります。私は以前に一度それをやったことがありますが、それはエンタープライズプロジェクトであるため、そのプロジェクトを指すことはできません。それでも、適切なクライアントとサーバーの構築に役立つコードサンプルをいくつか紹介します。

サーバ側

ハンドラーとオープン論理サブスクライバーのマッピング

考慮しなければならない最初の点は、1つの物理接続内のすべての論理ストリームをどこかに格納する必要があることです。

class MyWebSocketRouter implements WebSocketHandler {

  final Map<String, EnumMap<ActionMessage.Type, ChannelHandler>> channelsMapping;


  @Override
  public Mono<Void> handle(WebSocketSession session) {
    final Map<String, Disposable> channelsIdsToDisposableMap = new HashMap<>();
    ...
  }
}

上記のサンプルには2つのマップがあります。 1つ目は、着信メッセージのパラメータなどに基づいてルートを特定できるようにするルートマッピングです。 2つ目は、リクエストストリームのユースケース(私の場合はアクティブなサブスクリプションのマップ)に対して作成されているため、サブスクリプションを作成するメッセージフレームを送信したり、特定のアクションにサブスクライブしてそのサブスクリプションを保持したりすることができます。アクションが実行されると、サブスクリプションが存在する場合はサブスクリプションが解除されます。

メッセージの多重化にプロセッサを使用

すべての論理ストリームからメッセージを返信するには、メッセージを1つのストリームに多重化する必要があります。たとえば、Reactorを使用すると、UnicastProcessorを使用してそれを行うことができます。

@Override
public Mono<Void> handle(WebSocketSession session) {
  final UnicastProcessor<ResponseMessage<?>> funIn = UnicastProcessor.create(Queues.<ResponseMessage<?>>unboundedMultiproducer().get());
  ...

  return Mono
    .subscriberContext()
    .flatMap(context -> Flux.merge(
      session
        .receive()
        ...
        .cast(ActionMessage.class)
        .publishOn(Schedulers.parallel())
        .doOnNext(am -> {
          switch (am.type) {
            case CREATE:
            case UPDATE:
            case CANCEL: {
              ...
            }
            case SUBSCRIBE: {
              Flux<ResponseMessage<?>> flux = Flux
                .from(
                  channelsMapping.get(am.getChannelId())
                                 .get(ActionMessage.Type.SUBSCRIBE)
                                 .handle(am) // returns Publisher<>
                );

              if (flux != null) {
                channelsIdsToDisposableMap.compute(
                  am.getChannelId() + am.getSymbol(), // you can generate a uniq uuid on the client side if needed
                  (cid, disposable) -> {
                    ...

                    return flux
                      .subscriberContext(context)
                      .subscribe(
                        funIn::onNext, // send message to a Processor manually
                        e -> {
                          funIn.onNext(
                            new ResponseMessage<>( // send errors as a messages to Processor here
                              0,
                              e.getMessage(),
                              ...
                              ResponseMessage.Type.ERROR
                            )
                          );
                        }
                      );
                  }
                );
              }

              return;
            }
            case UNSABSCRIBE: {
              Disposable disposable = channelsIdsToDisposableMap.get(am.getChannelId() + am.getSymbol());

              if (disposable != null) {
                disposable.dispose();
              }
            }
          }
        })
        .then(Mono.empty()),

        funIn
            ...
            .map(p -> new WebSocketMessage(WebSocketMessage.Type.TEXT, p))
            .as(session::send)
      ).then()
    );
}

上記のサンプルからわかるように、そこにはたくさんのものが存在します。

  1. メッセージにはルート情報を含める必要があります
  2. メッセージには、関連する一意のストリームIDを含める必要があります。
  3. エラーもメッセージでなければならない場合のメッセージ多重化用の個別のプロセッサ
  4. 各チャネルはどこかに保存する必要があります。この場合、すべてのメッセージがFluxまたは単にMonoを提供できる単純な使用例があります(モノの場合はより簡単に実装できます)サーバー側では、一意のストリームIDを保持する必要はありません)。
  5. このサンプルにはメッセージのエンコード/デコードが含まれていないため、この課題はあなたに任されています。

クライアント側

クライアントもそれほど単純ではありません。

セッションの処理

接続を処理するには、2つのプロセッサを割り当てる必要があるため、さらにそれらを使用してメッセージを多重化および逆多重化できます。

UnicastProcessor<> outgoing = ...
UnicastPorcessor<> incoming = ...
(session) -> {
  return Flux.merge(
     session.receive()
            .subscribeWith(incoming)
            .then(Mono.empty()),
     session.send(outgoing)
  ).then();
}

すべての論理ストリームをどこかに保持する

作成されたすべてのストリームは、それがMonoであるかFluxであるかに関係なく、どこに格納する必要があります。これにより、どのストリームメッセージに関連するかを区別できるようになります。

Map<String, MonoSink> monoSinksMap = ...;
Map<String, FluxSink> fluxSinksMap = ...;

monoSinkとFluxSinkには同じ親インターフェースがないため、2つのマップを保持する必要があります。

メッセージルーティング

上記のサンプルでは、​​クライアント側の最初の部分を検討しました。次に、メッセージルーティングメカニズムを構築する必要があります。

...
.subscribeWith(incoming)
.doOnNext(message -> {
    if (monoSinkMap.containsKey(message.getStreamId())) {
        MonoSink sink = monoSinkMap.get(message.getStreamId());
        monoSinkMap.remove(message.getStreamId());
        if (message.getType() == SUCCESS) {
            sink.success(message.getData());
        }
        else {
            sink.error(message.getCause());
        }
    } else if (fluxSinkMap.containsKey(message.getStreamId())) {
        FluxSink sink = fluxSinkMap.get(message.getStreamId());
        if (message.getType() == NEXT) {
            sink.next(message.getData());
        }
        else if (message.getType() == COMPLETE) {
            fluxSinkMap.remove(message.getStreamId());
            sink.next(message.getData());
            sink.complete();
        }
        else {
            fluxSinkMap.remove(message.getStreamId());
            sink.error(message.getCause());
        }
    }
})

上記のコードサンプルは、着信メッセージをルーティングする方法を示しています。

多重リクエスト

最後の部分はメッセージの多重化です。そのために、可能な送信者クラスの実装をカバーします。

class Sender {
    UnicastProcessor<> outgoing = ...
    UnicastPorcessor<> incoming = ...

    Map<String, MonoSink> monoSinksMap = ...;
    Map<String, FluxSink> fluxSinksMap = ...;

    public Sender () {

//ここにwebsocket接続を作成し、前述のコードを挿入します}

    Mono<R> sendForMono(T data) {
        //generate message with unique 
        return Mono.<R>create(sink -> {
            monoSinksMap.put(streamId, sink);
            outgoing.onNext(message); // send message to server only when subscribed to Mono
        });
    }

     Flux<R> sendForFlux(T data) {
         return Flux.<R>create(sink -> {
            fluxSinksMap.put(streamId, sink);
            outgoing.onNext(message); // send message to server only when subscribed to Flux
        });
     }
}

カスタム実装の要約

  1. ハードコア
  2. バックプレッシャーのサポートが実装されていないため、別の課題になる可能性があります
  3. 足元で簡単に自分を撃つ

テイクアウェイ

  1. RSocketを使用してください。自分でプロトコルを作成しないでください。ハードです。
  2. Pivotalの人たちからのRSocketの詳細について- https://www.youtube.com/watch?v=WVnAbv65uC
  3. 私の講演の1つからRSocketについてさらに学ぶには https://www.youtube.com/watch?v=XKMyj6arY2A
  4. プロテウスと呼ばれるRSocketの上に構築された注目のフレームワークがあります-あなたはそれに興味があるかもしれません- https://www.netifi.com/
  5. RSocketプロトコルのコアデベロッパーからプロテウスの詳細を学ぶには https://www.google.com/url?sa=t&source=web&rct=j&url=https://m.youtube.com/watch%3Fv% 3D_rqQtkIeNIQ&ved = 2ahUKEwjpyLTpsLzfAhXDDiwKHUUUA8gQt9IBMAR6BAgNEB8&usg = AOvVaw0B_VdOj42gjr0YrzLLUX1E
10
Oleh Dokuka

これがあなたの問題かどうかわからない??静的フラックス応答(これはクローズ可能なストリームです)を送信していることがわかりました。たとえば、そのセッションにメッセージを送信するには、オープンストリームが必要です。たとえば、プロセッサを作成できます。

public class SocketMessageComponent {
private DirectProcessor<String> emitterProcessor;
private Flux<String> subscriber;

public SocketMessageComponent() {
    emitterProcessor = DirectProcessor.create();
    subscriber = emitterProcessor.share();
}

public Flux<String> getSubscriber() {
    return subscriber;
}

public void sendMessage(String mesage) {
    emitterProcessor.onNext(mesage);
}

}

そしてあなたは送ることができます

 public Mono<Void> handle(WebSocketSession webSocketSession) {
    this.webSocketSession = webSocketSession;
    return webSocketSession.send(socketMessageComponent.getSubscriber()
            .map(webSocketSession::textMessage))
            .and(webSocketSession.receive()
                    .map(WebSocketMessage::getPayloadAsText).log());
}
3
Ricard Kollcaku