jimble

SSE(Server-Sent Events)#

サーバーから一方向にイベントを流します。WebSocket と違って、ただの HTTP です。 プロキシもブラウザもそのまま通ります。

get("/events", context -> {

	try (SseStream sse = context.response().sse()) {

		sse.send("progress", new Data().putData("percent", 10));
		sse.send("done", new Data().putData("ok", true));

	}

});

クライアント側は EventSource です。

const es = new EventSource("/events");
es.addEventListener("progress", e => console.log(JSON.parse(e.data)));
es.addEventListener("done", () => es.close());

進捗を送る#

get("/posts/reindex", context -> {

	List<Data> posts = BlogApp.listPosts();

	try (SseStream sse = context.response().sse()) {

		sse.send("start", new Data().putData("total", posts.size()));

		int done = 0;

		for (Data row : posts) {

			if (!sse.isOpen()) {
				break;
			}

			Data post = row.extractTableData(Post.instance());
			done++;

			sse.send("progress", new Data()
				.putData("done", done)
				.putData("total", posts.size())
				.putData("title", post.getString(Post.title)));

		}

		sse.send("done", new Data().putData("done", done));

	}

});

sse() を呼んだ時点で、次のヘッダが付きます。

Content-Type: text/event-stream
Cache-Control: no-store
X-Accel-Buffering: no

X-Accel-Buffering: no は nginx 対策です。これが無いと nginx が溜め込んでしまい、 「全部終わってから一気に届く」になります。

切断は分かりません#

ここが SSE のいちばん大事なところです。

ブラウザを閉じても、helidon は切断を教えてくれません。 書き込みは成功し続けるか、あるいは永久にブロックします(Java にはソケットの 書き込みタイムアウトがありません)。実際に試したところ、4.8MB / 30秒 書けてしまうことも、 8秒で完全に固まることもありました。

なので jimble の SseStream は、切断を待つのではなく寿命で区切ります

sse {
	max_duration_seconds = 300   # 既定 5分
	max_events           = 0     # 0 = 無制限
	retry_millis         = 3000  # クライアントの再接続間隔
}
// このルートだけ変える
try (SseStream sse = context.response().sse(Duration.ofSeconds(30), 100)) {

sse.isOpen() は「まだ寿命が残っているか」を返します。 「クライアントが繋がっているか」ではありません。 ループの中で必ず見てください。

for (Data row : rows) {

	if (!sse.isOpen()) {
		break;
	}

	sse.send("progress", ...);

}

繋ぎ直し#

EventSource は切れたら自動で繋ぎ直します。retry_millis がその間隔です。

つまり、寿命で切れても勝手に繋ぎ直されます。 無限に続くストリームを作るのではなく、「5分ぶん流して切る、また繋がる」でよいです。

やり直しの位置を伝えたいときは、イベントに id を付けてください。 繋ぎ直しのときに Last-Event-ID ヘッダで返ってきます。

使いどころ#

用途 向いているか
進捗バー、ログの流し込み 向いている
通知の配信 向いている
双方向のやりとり WebSocket
大量の同時接続を長時間 接続数ぶんスレッドを使う。仮想スレッドなので軽いが、寿命は短くする