因為有監控服務是否斷掉的需求, 要做一個 Dashboard 來即時呈現問題點,
但用輪詢怕會自己把自己搞死, 所以就看了一下 Server-sent events,
也不是什麼新東西了, 主流瀏覽器也都支援了, 大概沒什麼或坑吧 XD
就拿來試玩一下順便紀錄.
好的, 首先先看看你專案是用 web 還是 webflux?
如果是 web 看這邊 Async Requests - HTTP Streaming - SSE
如果是 webFlux 的看這邊 Reactive Core - Codecs - HTTP Streaming
我這邊是用 webflux 練習, 就沒有練習 SseEmitter 的用法囉
Dependency
pom.xml1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Code
我準備兩個 VO 物件給頁面呈現使用, 這 VO 很簡單就是裝載現在時間跟服務的名稱.
ClusterHealthVo.java1
2
3
4
5
6
7
public class ClusterHealthVo {
private List<ApplicationVo> clusterHealth;
private Instant nowTime;
}
ApplicationVo.java1
2
3
4
5
6
public class ApplicationVo {
private String name;
}
我用了一個模擬的服務列表
ApplicationService.java1
2
3
4
5
6
7
8
9
10
11
12
public class ApplicationService {
private static List<ApplicationVo> applicationVoList = new ArrayList<>();
// 新增服務
public void addApplication(ApplicationVo applicationVo){
applicationVoList.add(applicationVo);
}
// 取得服務清單
public List<ApplicationVo> getApplicationVoList(){
return applicationVoList;
}
}
再加上一個定時器模擬服務增加的動作
ApplicationLoader.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 4j
public class ApplicationLoader {
private final ApplicationService applicationService;
private final AtomicInteger atomicInteger = new AtomicInteger();
.class) (ApplicationReadyEvent
public void applicationReady() {
}
5000) (fixedRate =
public void schedule() {
applicationService.addApplication(new ApplicationVo("Sam_" + atomicInteger.incrementAndGet()));
}
}
使用到 Scheduled 別忘了啟動
DemoApplication.java1
2
3
4
5
6
7
8
9
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
都好了之後這邊就是後端 SSE 的實做啦
SseController.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"/sse") (
public class SseController {
private final ApplicationService applicationService;
"/stream-flux", produces = MediaType.TEXT_EVENT_STREAM_VALUE) (path =
public Flux<ClusterHealthVo> streamFlux() {
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> new ClusterHealthVo(applicationService.getApplicationVoList(), Instant.now()));
}
"/stream-sse") (
public Flux<ServerSentEvent<ClusterHealthVo>> streamEvents() {
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> ServerSentEvent.<ClusterHealthVo>builder()
.id(String.valueOf(sequence))
.event("health-check-event")
.data(new ClusterHealthVo(applicationService.getApplicationVoList(), Instant.now()))
.build());
}
}
其實功能是一樣的, 差別是有沒有自訂 event 名稱
如果我們分別用瀏覽器直接打開 API 的網址會看到他一直從後端輸出資料
http://localhost/sse/stream-flux1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21data:{"clusterHealth":[{"name":"Sam_1"}],"nowTime":"2019-07-05T08:49:36.890Z"}
data:{"clusterHealth":[{"name":"Sam_1"}],"nowTime":"2019-07-05T08:49:37.890Z"}
data:{"clusterHealth":[{"name":"Sam_1"}],"nowTime":"2019-07-05T08:49:38.890Z"}
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"}],"nowTime":"2019-07-05T08:49:39.890Z"}
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"}],"nowTime":"2019-07-05T08:49:40.890Z"}
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"}],"nowTime":"2019-07-05T08:49:41.889Z"}
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"}],"nowTime":"2019-07-05T08:49:42.889Z"}
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"}],"nowTime":"2019-07-05T08:49:43.890Z"}
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"},{"name":"Sam_3"}],"nowTime":"2019-07-05T08:49:44.889Z"}
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"},{"name":"Sam_3"}],"nowTime":"2019-07-05T08:49:45.889Z"}
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"},{"name":"Sam_3"}],"nowTime":"2019-07-05T08:49:46.889Z"}
使用 ServerSentEvent 則是會輸出這樣的資料
http://localhost/sse/stream-sse1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43id:0
event:health-check-event
data:{"clusterHealth":[{"name":"Sam_1"}],"nowTime":"2019-07-05T08:51:20.442Z"}
id:1
event:health-check-event
data:{"clusterHealth":[{"name":"Sam_1"}],"nowTime":"2019-07-05T08:51:21.441Z"}
id:2
event:health-check-event
data:{"clusterHealth":[{"name":"Sam_1"}],"nowTime":"2019-07-05T08:51:22.441Z"}
id:3
event:health-check-event
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"}],"nowTime":"2019-07-05T08:51:23.441Z"}
id:4
event:health-check-event
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"}],"nowTime":"2019-07-05T08:51:24.441Z"}
id:5
event:health-check-event
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"}],"nowTime":"2019-07-05T08:51:25.441Z"}
id:6
event:health-check-event
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"}],"nowTime":"2019-07-05T08:51:26.443Z"}
id:7
event:health-check-event
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"}],"nowTime":"2019-07-05T08:51:27.442Z"}
id:8
event:health-check-event
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"},{"name":"Sam_3"}],"nowTime":"2019-07-05T08:51:28.441Z"}
id:9
event:health-check-event
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"},{"name":"Sam_3"}],"nowTime":"2019-07-05T08:51:29.441Z"}
id:10
event:health-check-event
data:{"clusterHealth":[{"name":"Sam_1"},{"name":"Sam_2"},{"name":"Sam_3"}],"nowTime":"2019-07-05T08:51:30.442Z"}
來看看前端網頁怎麼些收資料
demo-flux.html1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Server-Sent Events Demo</title>
</head>
<body>
<h1>Server-Sent Events Demo - Flux<ClusterHealthVo></h1>
<div id="vue-display">
<table>
<tr v-for="item in clusterHealth">
<td>{{ item.name }}</td>
</tr>
</table>
<div>刷新時間</div>
<div>{{ nowTime }}</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<script>
Vue.config.devtools = true;
Vue.config.debug = true;
window.addEventListener('load', function (ev) {
var es = new EventSource('/sse/stream-flux')
es.addEventListener('message', function (msg) {
console.log(msg.data);
var reObj = JSON.parse(msg.data);
vueMainContent.clusterHealth = reObj.clusterHealth;
vueMainContent.nowTime = reObj.nowTime;
});
es.addEventListener("open", function (e) {
console.log('連接成功');
});
es.addEventListener("error", function (e) {
console.log('網路異常 最後連線時間<br/>' + vueMainContent.nowTime);
vueMainContent.clusterHealth = [];
});
});
var vueMainContent = new Vue({
el: '#vue-display',
data: {
clusterHealth: [],
nowTime: ''
}
});
</script>
</body>
</html>
如果是接 Flux<ServerSentEvent
demo-sse.html1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Server-Sent Events Demo</title>
</head>
<body>
<h1>Server-Sent Events Demo - Flux<ServerSentEvent<ClusterHealthVo>></h1>
<div id="vue-display">
<table>
<tr v-for="item in clusterHealth">
<td>{{ item.name }}</td>
</tr>
</table>
<div>刷新時間</div>
<div>{{ nowTime }}</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<script>
Vue.config.devtools = true;
Vue.config.debug = true;
window.addEventListener('load', function (ev) {
var es = new EventSource('/sse/stream-sse')
es.addEventListener('message', function (msg) {
// 有自訂就不會走 message 這個
console.log("message");
});
es.addEventListener("open", function (e) {
console.log('連接成功');
});
es.addEventListener("error", function (e) {
console.log('網路異常 最後連線時間<br/>' + vueMainContent.nowTime);
});
es.addEventListener('health-check-event', function(msg) {
console.log('is health-check-event!!');
var reObj = JSON.parse(msg.data);
vueMainContent.clusterHealth = reObj.clusterHealth;
vueMainContent.nowTime = reObj.nowTime;
}, false);
});
var vueMainContent = new Vue({
el: '#vue-display',
data: {
clusterHealth: [],
nowTime: ''
}
});
</script>
</body>
</html>
差別是不會統一走 message 接收進來1
2
3
4es.addEventListener('message', function (msg) {
// 有自訂就不會走 message 這個
console.log("message");
});
References
Server-Sent Events in Spring - baeldung
Async Requests - HTTP Streaming - SSE - Spring Framework Documentation
Reactive Core - Codecs - HTTP Streaming - Spring Framework Documentation
Server-Sent Events でチャットアプリを作ってみた (Spring Boot 2.x系 x Spring MVC x Akka Actor)
Server-Sent Events with Spring
Spring 5 新增全新的reactive web框架:webflux
使用server side event 實作聊天功能
使用 Spring 5 的 Webflux 开发 Reactive 应用
SAM的程式筆記 由朱尚禮製作,以創用CC 姓名標示-非商業性-相同方式分享 4.0 國際 授權條款釋出。