私はwebfluxスターター(2.0.0.RC1
)を使用してspring-boot spring-boot-starter-webflux
で作業してきました。無限のフラックスを返すシンプルなコントローラーを作成しました。パブリッシャーは、クライアント(サブスクライバー)が存在する場合にのみ機能するようにしたいと思います。私がこのようなコントローラーを持っているとしましょう:
@RestController
public class Demo {
@GetMapping(value = "/")
public Flux<String> getEvents(){
return Flux.create((FluxSink<String> sink) -> {
while(!sink.isCancelled()){
// TODO e.g. fetch data from somewhere
sink.next("DATA");
}
sink.complete();
}).doFinally(signal -> System.out.println("END"));
}
}
さて、そのコードを実行してエンドポイントにアクセスしようとすると http:// localhost:8080 / Chromeで、データを見ることができます。ただし、ブラウザを閉じると、キャンセルイベントが発生していないため、whileループが続行されます。 ブラウザを閉じたらすぐにストリーミングを終了/キャンセルするにはどうすればよいですか?
これから answer 私はそれを引用します:
現在HTTPでは、HTTPプロトコルがこれをサポートしていないため、正確なバックプレッシャ情報はネットワークを介して送信されません。別のワイヤープロトコルを使用すると、これが変わる可能性があります。
背圧はHTTPプロトコルでサポートされていないため、キャンセル要求も行われないことを意味すると思います。
ネットワークトラフィックを分析してもう少し調査すると、ブラウザを閉じるとすぐにブラウザがTCP FIN
を送信することがわかりました。Nettyを設定する方法はありますか?または他の何か)ハーフクローズ接続がパブリッシャーでキャンセルイベントをトリガーし、whileループを停止するようにしますか?
または、自分のサブスクライバーを実装するorg.springframework.http.server.reactive.ServletHttpHandlerAdapter
のような独自のアダプターを作成する必要がありますか?
助けてくれてありがとう。
EDIT:クライアントがない場合、ソケットにデータを書き込もうとするとIOException
が発生します。 スタックトレース でわかるように。
ただし、次のデータチャンクを送信する準備が整うまでに時間がかかる場合があり、そのため、なくなったクライアントを検出するのに同じ時間がかかるため、これでは十分ではありません。 Brian Clozelの回答 で指摘されているように、これはReactorNettyの既知の問題です。 POM.xml
に依存関係を追加して、代わりにTomcatを使用しようとしました。このような:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-Tomcat</artifactId>
</dependency>
Nettyに置き換わり、代わりにTomcatを使用しますが、ブラウザにデータが表示されないため、反応がないようです。ただし、コンソールには警告/情報/例外はありません。 このバージョンの時点でspring-boot-starter-webflux
(2.0.0.RC1
)はTomcatと連携することになっていますか?
これは既知の問題であるため( Brian Clozelの回答 を参照)、最終的に1つのFlux
を使用して実際のデータをフェッチし、別のデータを使用して何らかのping /ハートビートを実装しました。機構。その結果、両方をFlux.merge()
でマージします。
ここに私のソリューションの簡略版を見ることができます:
@RestController
public class Demo {
public interface Notification{}
public static class MyData implements Notification{
…
public boolean isEmpty(){…}
}
@GetMapping(value = "/", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<? extends Notification>> getNotificationStream() {
return Flux.merge(getEventMessageStream(), getHeartbeatStream());
}
private Flux<ServerSentEvent<Notification>> getHeartbeatStream() {
return Flux.interval(Duration.ofSeconds(2))
.map(i -> ServerSentEvent.<Notification>builder().event("ping").build())
.doFinally(signalType ->System.out.println("END"));
}
private Flux<ServerSentEvent<MyData>> getEventMessageStream() {
return Flux.interval(Duration.ofSeconds(30))
.map(i -> {
// TODO e.g. fetch data from somewhere,
// if there is no data return an empty object
return data;
})
.filter(data -> !data.isEmpty())
.map(data -> ServerSentEvent
.builder(data)
.event("message").build());
}
}
すべてをServerSentEvent<? extends Notification>
としてまとめます。 Notification
は単なるマーカーインターフェースです。データイベントとpingイベントを区別するために、event
クラスのServerSentEvent
フィールドを使用します。ハートビートFlux
はイベントを絶えず短い間隔で送信するため、クライアントがなくなったことを検出するのにかかる時間は、せいぜいその間隔の長さです。送信できる実際のデータを取得するまでに時間がかかる場合があり、その結果、クライアントがなくなったことを検出するまでに時間がかかる場合があることを忘れないでください。このように、ping(またはメッセージイベント)を送信できなくなるとすぐに、クライアントがなくなったことを検出します。
マーカーインターフェイスに関する最後の注意点は、通知と呼ばれています。これは実際には必要ありませんが、ある種の安全性を提供します。それがなければ、getNotificationStream()メソッドの戻り値の型としてFlux<ServerSentEvent<?>>
の代わりにFlux<ServerSentEvent<? extends Notification>>
を書くことができます。または、getHeartbeatStream()がFlux<ServerSentEvent<MyData>>
を返すようにすることもできます。ただし、このようにすると、私が望まない任意のオブジェクトを送信できるようになります。結果として、私はインターフェースを追加しました。
なぜこれがこのように動作するのかはわかりませんが、生成演算子の選択によるものと思われます。以下を使用するとうまくいくと思います。
return Flux.interval(Duration.ofMillis(500))
.map(input -> {
return "DATA";
});
Reactorのリファレンスドキュメントによると、おそらくgenerate
とPush
の主な違いにぶつかっているでしょう (generateを使用した非常によく似たアプローチもおそらくうまくいくと思います) 。
私のコメントは、背圧情報(Subscriber
が受け入れる要素の数)に言及していましたが、成功/エラー情報はネットワークを介して伝達されます。
選択したWebサーバー(Reactor Netty、Tomcat、Jettyなど)によっては、クライアント接続を閉じると次のような結果になる場合があります。
つまり、特別なことをする必要はありません。すでにサポートされているはずですが、ここではFlux
の実装が実際の問題である可能性があります。