引言
如果现在有一个场景A服务会不断产生新的数据,如何让数据实时提供给Web呢?轮询?Websocket?
由于最近在推Webflux的使用,更好的适配kotlin的协程,加上我司有很多大屏和实时数据监测的业务,对于很多模块都有二三十个API在同时请求,使用轮询是很麻烦的,所以最近弄了一套更简单的数据响应思路。
传统的实时数据响应,一般都是前端轮询和Websocket来进行实时数据获取,前者时效性不够,API请求过多的时候会很混乱,后者由于我们只要响应实时数据,并不需要双向通信(比如可视化大屏,一些数据监测业务),用长连接有种杀鸡用牛刀的感觉了。
由于Webflux是基于Reactive设计是天然的发布订阅方式,所以我们可以使用SSE来实现数据的持续响应,并且配合Kotlin的Flow,涉及读取第三方数据转发的场景下,可以更好的对模块解耦。
对比
轮询就不进行对比了,过于古法,并且时效性大打折扣,减小轮询时间也会造成资源消耗浪费耗能增高
WebSocket
对于Websocket来说,他的使用是复杂的,他可能需要引入第三方的依赖要配置,需要升级协议为HTTP Upgrade,需要处理复杂的心跳监测问题,我们如果只是需要单向的获取数据,对于WS来说,可能时一个复杂成本较高的方案。
当然对于吞吐量和数据量过大的场景,还有需要双向交换数据的场景来说,Websocket依然是不二之选,此文只探讨关于单向数据实时响应的,所以对于Websocket来说,他可能并不是最好的选择。
SSE
SSE也是基于Socket建立一个长链接,但是不需要你做复杂的心跳检测,并且他是单向的,对比WS来说,他更加的轻量简单,他是基于HTTP协议所以不用太担心兼容问题。
在Spring5后引入了Reactive,并且提供了Webflux,天然支持了SSE的协议,对于使用来说更加的方便,加上Kotlin的Flow设计也是发布订阅的模式,和Reactive设计天然的契合,加上官方也早已适配了Kotlin相关的特性和语言,所以我们可以使用Flow来进行响应式流的替换
需求分析
对于一开始的需求,我们可以创建一个热流来不断的缓存服务产生的数据,然后对于请求来说我们可以返回这个流,让请求的客户端作为流的订阅者,当有数据emit的时候,流就会自动的发布新emit的数据给相关的订阅者,达到实时数据响应的结果。
项目搭建
依赖引入
- 依赖版本
[versions]
kotlin = "2.0.0"
springBootPlugin = "3.3.5"
dependencyManagementPlugin = "1.1.6"
- 依赖
[libraries]
spring-boot-webflux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux" }
kotlinx-coroutines-reactor = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-reactor" }
- 插件
[plugins]
kotlin-language = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
springboot = { id = "org.springframework.boot", version.ref = "springBootPlugin" }
dependencyManagement = { id = "io.spring.dependency-management", version.ref = "dependencyManagementPlugin" }
启动类的创建
@SpringBootApplication
class App
fun main(args: Array<String>){
runApplication<App>(*args)
}
创建一个全局Flow
由于我的代码是直接从项目里面复制的代码,所以看着可能有点没必要对于getFlow和dataEmit来说,因为我这个类是用于管理多个Flow的重心,所以只是一个参考,知道有一个SharedFlow来转发数据就行
这里不用冷流是因为我们说过,我们的服务是在不断产生数据的,而不是订阅调用的时候才去产生数据
@Component
class DeviceDataFlowCenter {
private val deviceDataFlow = MutableSharedFlow<String>()
suspend fun dataEmit(data: String) {
deviceDataFlow.emit(data)
}
suspend fun getFlow(): Flow<String> {
return deviceDataFlow
}
}
数据生产任务创建
模拟一个任务在启动后不断的生产数据然后提交给我们的热流
@Service
class DeviceDataReadTask(
private val deviceDataFlowCenter: DeviceDataFlowCenter,
) {
@EventListener(ApplicationReadyEvent::class)
fun launchReadTask(){
CoroutineScope(Dispatchers.Default).launch {
dataSend1()
}
}
suspend fun dataSend1() = withContext(Dispatchers.IO) {
var count = 0
while (isActive) {
delay(1000)
deviceDataFlowCenter.dataEmit("1.数据${++count}")
}
}
}
编写Controller去返回流
编写一个控制器当请求test/test路径的时候返回我们的热流,提供给客户端来不断的订阅数据
@RestController
@RequestMapping("test")
@CrossOrigin
class AppController(
private val deviceDataFlowCenter: DeviceDataFlowCenter
) {
@GetMapping("test")
suspend fun test(): Flow<String> = withContext(Dispatchers.IO){
deviceDataFlowCenter.getFlow()
}
}
编写前端测试用例
通过WebAPI提供的EventSource,前端可以非常轻松愉快且简单的来获取我们的SSE数据的不断的响应
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE请求读取</title>
</head>
<body>
<h1>SSE请求读取</h1>
<div id="messages"></div>
<script>
const eventSource = new EventSource("http://localhost:8111/test/test");
eventSource.onmessage = function (event) {
console.log("Received SSE: ", event.data);
const messagesDiv = document.getElementById("messages");
const newMessage = document.createElement("div");
newMessage.textContent = event.data;
messagesDiv.appendChild(newMessage);
};
eventSource.onopen = function () {
console.log("Connection opened");
};
eventSource.onerror = function (error) {
console.error("Error with SSE connection:", error);
eventSource.close();
};
</script>
</body>
</html>
结尾
至此,我们使用了Webflux+Kotlin的Flow来改变了传统的实时数据的响应,并且更加的高效和简单,而且还用到了Kotlin本身的特性。当然这也可能不是非常完美的,只是对于单向数据响应的场景中,个人认为这是非常不错的一种方案,仅供参考和学习。