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のドキュメント/実装に関するオンラインの適切なリソースを見つけることができません。先へ進む方法に関する指針。
これは完全に正しい設計であり、リソースを節約し、すべての可能な操作に対してクライアントごとの接続のみを使用する価値があります。
ただし、ホイールを実装せず、これらすべての種類の通信を提供するプロトコルを使用しないでください。
そのためのオプションの1つは、RSocketプロトコルのRSocket-Java実装を使用することです。 RSocket-JavaはProject Reactorの上に構築されているため、Spring WebFluxエコシステムに自然に適合します。
残念ながら、Springエコシステムとの機能統合はありません。幸い、Spring WebFluxとRSocketを統合し、WebSocket RSocketサーバーとWebFlux Httpサーバーを公開するシンプルな RSocket Spring Boot Starter を提供するために数時間を費やしました。
基本的に、RSocketは、同じアプローチを自分で実装する複雑さを隠します。 RSocketを使用すると、カスタムプロトコルやJavaの実装として対話モデルの定義を気にする必要はありません。 RSocketは、特定の論理チャネルにデータを配信します。同じWS接続にメッセージを送信する組み込みのクライアントを提供するため、そのためのカスタム実装を発明する必要はありません。
RSocketは単なるプロトコルであるため、メッセージ形式は提供されないため、この課題はビジネスロジックに関するものです。ただし、プロトコル形式のバッファをメッセージ形式として提供し、GRPCと同じコード生成手法を再利用するRSocket-RPCプロジェクトがあります。したがって、RSocket-RPCを使用すると、クライアントとサーバーのAPIを簡単に構築でき、トランスポートとプロトコルの抽象化についてまったく不注意です。
同じRSocket Spring Boot統合は、RSocket-RPC使用法の example も提供します。
したがって、そのためには、自分で地獄を実装する必要があります。私は以前に一度それをやったことがありますが、それはエンタープライズプロジェクトであるため、そのプロジェクトを指すことはできません。それでも、適切なクライアントとサーバーの構築に役立つコードサンプルをいくつか紹介します。
考慮しなければならない最初の点は、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()
);
}
上記のサンプルからわかるように、そこにはたくさんのものが存在します。
Flux
または単にMono
を提供できる単純な使用例があります(モノの場合はより簡単に実装できます)サーバー側では、一意のストリームIDを保持する必要はありません)。クライアントもそれほど単純ではありません。
接続を処理するには、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
});
}
}
これがあなたの問題かどうかわからない??静的フラックス応答(これはクローズ可能なストリームです)を送信していることがわかりました。たとえば、そのセッションにメッセージを送信するには、オープンストリームが必要です。たとえば、プロセッサを作成できます。
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());
}