Springのサーバー送信イベント
1. 概要
このチュートリアルでは、Springを使用してServer-Sent-EventsベースのAPIを実装する方法を説明します。
簡単に言うと、Server-Sent-Events(略してSSE)は、Webアプリケーションが単方向のイベントストリームを処理し、サーバーがデータを送信するたびに更新を受信できるようにするHTTP標準です。
Spring 4.2バージョンはすでにサポートしていますが、Spring 5以降はwe now have a more idiomatic and convenient way to handle itです。
2. Spring 5 Webfluxを使用したSSE
これを実現するには、we can make use of implementations such as the Flux class provided by the Reactor library, or potentially the ServerSentEvent entityを使用します。これにより、イベントのメタデータを制御できます。
2.1. Fluxを使用してイベントをストリーミングする
Fluxは、イベントのストリームのリアクティブな表現です。指定されたリクエストまたはレスポンスのメディアタイプに基づいて異なる方法で処理されます。
SSEストリーミングエンドポイントを作成するには、W3C specificationsを追跡し、そのMIMEタイプをtext/event-streamとして指定する必要があります。
@GetMapping(path = "/stream-flux", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux streamFlux() {
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> "Flux - " + LocalTime.now().toString());
}
intervalメソッドは、long値を段階的に出力するFluxを作成します。 次に、これらの値を目的の出力にマップします。
アプリケーションを起動し、エンドポイントを参照して試してみましょう。
サーバーによって秒単位でプッシュされるイベントにブラウザがどのように反応するかを見ていきます。 FluxとReactor Coreの詳細については、this postを確認できます。
2.2. ServerSentEvent要素を利用する
次に、出力StringをServerSentSeventオブジェクトにラップし、これを行うことの利点を調べます。
@GetMapping("/stream-sse")
public Flux> streamEvents() {
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> ServerSentEvent. builder()
.id(String.valueOf(sequence))
.event("periodic-event")
.data("SSE - " + LocalTime.now().toString())
.build());
}
理解できるように、there’re a couple of benefits of using the ServerSentEvent entity:
-
実際のシナリオで必要となるイベントメタデータを処理できます
-
「text/event-stream」メディアタイプ宣言は無視できます
この場合、id、event name、そして最も重要なことに、イベントの実際のdataを指定しました。
また、comments属性とretry値を追加して、イベントを送信するときに使用する再接続時間を指定することもできます。
2.3. WebClientでサーバー送信イベントを使用する
それでは、WebClientでイベントストリームを消費しましょう。:
public void consumeServerSentEvent() {
WebClient client = WebClient.create("http://localhost:8080/sse-server");
ParameterizedTypeReference> type
= new ParameterizedTypeReference>() {};
Flux> eventStream = client.get()
.uri("/stream-sse")
.retrieve()
.bodyToFlux(type);
eventStream.subscribe(
content -> logger.info("Time: {} - event: name[{}], id [{}], content[{}] ",
LocalTime.now(), content.event(), content.id(), content.data()),
error -> logger.error("Error receiving SSE: {}", error),
() -> logger.info("Completed!!!"));
}
subscribeメソッドを使用すると、イベントを正常に受信したとき、エラーが発生したとき、およびストリーミングが完了したときにどのように進むかを示すことができます。
この例では、retrieveメソッドを使用しました。これは、応答本文を取得するためのシンプルで簡単な方法です。
このメソッドは、onStatusステートメントを追加するシナリオを処理しない限り、4xxまたは5xx応答を受信した場合、自動的にWebClientResponseExceptionをスローします。
一方、exchangeメソッドを使用することもできます。このメソッドは、ClientResponseへのアクセスを提供し、失敗した応答でエラー信号を送信しません。
イベントメタデータが必要ない場合は、ServerSentEventラッパーをバイパスできることを考慮に入れる必要があります。
3. Spring MVCでのSSEストリーミング
すでに述べたように、SSE仕様は、SseEmitterクラスが導入されたSpring4.2以降でサポートされていました。
簡単に言うと、ExecutorServiceを定義します。これは、SseEmitterがデータをプッシュする作業を行い、エミッタインスタンスを返し、接続を次のように開いたままにするスレッドです。
@GetMapping("/stream-sse-mvc")
public SseEmitter streamSseMvc() {
SseEmitter emitter = new SseEmitter();
ExecutorService sseMvcExecutor = Executors.newSingleThreadExecutor();
sseMvcExecutor.execute(() -> {
try {
for (int i = 0; true; i++) {
SseEventBuilder event = SseEmitter.event()
.data("SSE MVC - " + LocalTime.now().toString())
.id(String.valueOf(i))
.name("sse event - mvc");
emitter.send(event);
Thread.sleep(1000);
}
} catch (Exception ex) {
emitter.completeWithError(ex);
}
});
return emitter;
}
ユースケースシナリオに適したExecutorServiceを必ず選択してください。
Spring MVCでのSSEの詳細と、this interesting tutorialを読むことで他の例を見ることができます。
4. サーバー送信イベントについて
SSEエンドポイントを実装する方法がわかったので、いくつかの基本的な概念を理解して、もう少し深く掘り下げてみましょう。
SSEは、ほとんどのブラウザで採用されている仕様であり、いつでもイベントを単方向にストリーミングできます。
「イベント」は、仕様で定義された形式に従うUTF-8エンコードされたテキストデータのストリームです。
この形式は、改行で区切られた一連のキーと値の要素(id、retry、dataおよびevent、名前を示す)で構成されます。
コメントもサポートされています。
この仕様は、データペイロード形式を制限するものではありません。単純なStringまたはより複雑なJSONまたはXML構造を使用できます。
最後に考慮しなければならない点は、SSEストリーミングとWebSocketsの使用の違いです。
WebSocketsはサーバーとクライアント間の全二重(双方向)通信を提供しますが、SSEは単方向通信を使用します。
また、WebSocketsはHTTPプロトコルではなく、SSEとは異なり、エラー処理標準を提供していません。
5. 結論
要約すると、この記事では、SSEストリーミングの主要な概念を学びました。これは、間違いなく、次世代システムを作成するための優れたリソースです。
現在、このプロトコルを使用すると、ボンネットの下で何が起こっているのかを理解することができます。
さらに、our Github repositoryにあるいくつかの簡単な例で理論を補完しました。