因為有監控服務是否斷掉的需求, 要做一個 Dashboard 來即時呈現問題點,
但用輪詢怕會自己把自己搞死, 所以就看了一下 Server-sent events,
也不是什麼新東西了, 主流瀏覽器也都支援了, 大概沒什麼或坑吧 XD
就拿來試玩一下順便紀錄.
好的, 首先先看看你專案是用 web 還是 webflux?
如果是 web 看這邊 Async Requests - HTTP Streaming - SSE
如果是 webFlux 的看這邊 Reactive Core - Codecs - HTTP Streaming
我這邊是用 webflux 練習, 就沒有練習 SseEmitter 的用法囉
Dependency
pom.xml
1 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
| <?xml version="1.0" encoding="UTF-8"?> <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/> </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.java
1 2 3 4 5 6 7
| @Data @NoArgsConstructor @AllArgsConstructor public class ClusterHealthVo { private List<ApplicationVo> clusterHealth; private Instant nowTime; }
|
ApplicationVo.java
1 2 3 4 5 6
| @Data @NoArgsConstructor @AllArgsConstructor public class ApplicationVo { private String name; }
|
我用了一個模擬的服務列表
ApplicationService.java
1 2 3 4 5 6 7 8 9 10 11 12
| @Service 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.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Slf4j @Component @RequiredArgsConstructor public class ApplicationLoader { private final ApplicationService applicationService; private final AtomicInteger atomicInteger = new AtomicInteger();
@EventListener(ApplicationReadyEvent.class) public void applicationReady() { }
@Scheduled(fixedRate = 5000) public void schedule() { applicationService.addApplication(new ApplicationVo("Sam_" + atomicInteger.incrementAndGet())); } }
|
使用到 Scheduled 別忘了啟動
DemoApplication.java
1 2 3 4 5 6 7 8 9
| @EnableScheduling @SpringBootApplication public class DemoApplication {
public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }
}
|
都好了之後這邊就是後端 SSE 的實做啦
SseController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @RestController @RequestMapping("/sse") @RequiredArgsConstructor public class SseController { private final ApplicationService applicationService;
@GetMapping(path = "/stream-flux", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<ClusterHealthVo> streamFlux() { return Flux.interval(Duration.ofSeconds(1)) .map(sequence -> new ClusterHealthVo(applicationService.getApplicationVoList(), Instant.now())); }
@GetMapping("/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-flux
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| data:{"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-sse
1 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
| id: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.html
1 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
| <!DOCTYPE html> <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> 的話要對應一下 event 的名稱
demo-sse.html
1 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
| <!DOCTYPE html> <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) { 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 4
| es.addEventListener('message', function (msg) { 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 國際 授權條款釋出。