Kotlin Flow+WebFlux构建更简洁的Web实时数据响应

795 阅读5分钟

引言

如果现在有一个场景A服务会不断产生新的数据,如何让数据实时提供给Web呢?轮询?Websocket?

由于最近在推Webflux的使用,更好的适配kotlin的协程,加上我司有很多大屏和实时数据监测的业务,对于很多模块都有二三十个API在同时请求,使用轮询是很麻烦的,所以最近弄了一套更简单的数据响应思路。

传统的实时数据响应,一般都是前端轮询和Websocket来进行实时数据获取,前者时效性不够,API请求过多的时候会很混乱,后者由于我们只要响应实时数据,并不需要双向通信(比如可视化大屏,一些数据监测业务),用长连接有种杀鸡用牛刀的感觉了。

由于Webflux是基于Reactive设计是天然的发布订阅方式,所以我们可以使用SSE来实现数据的持续响应,并且配合Kotlin的Flow,涉及读取第三方数据转发的场景下,可以更好的对模块解耦。

对比

轮询就不进行对比了,过于古法,并且时效性大打折扣,减小轮询时间也会造成资源消耗浪费耗能增高

WebSocket

对于Websocket来说,他的使用是复杂的,他可能需要引入第三方的依赖要配置,需要升级协议为HTTP Upgrade,需要处理复杂的心跳监测问题,我们如果只是需要单向的获取数据,对于WS来说,可能时一个复杂成本较高的方案。

当然对于吞吐量和数据量过大的场景,还有需要双向交换数据的场景来说,Websocket依然是不二之选,此文只探讨关于单向数据实时响应的,所以对于Websocket来说,他可能并不是最好的选择。

QQ_1731737357052.png

SSE

SSE也是基于Socket建立一个长链接,但是不需要你做复杂的心跳检测,并且他是单向的,对比WS来说,他更加的轻量简单,他是基于HTTP协议所以不用太担心兼容问题。

在Spring5后引入了Reactive,并且提供了Webflux,天然支持了SSE的协议,对于使用来说更加的方便,加上Kotlin的Flow设计也是发布订阅的模式,和Reactive设计天然的契合,加上官方也早已适配了Kotlin相关的特性和语言,所以我们可以使用Flow来进行响应式流的替换

文档参考Coroutines :: Spring Framework

QQ_1731737761032.png

需求分析

对于一开始的需求,我们可以创建一个热流来不断的缓存服务产生的数据,然后对于请求来说我们可以返回这个流,让请求的客户端作为流的订阅者,当有数据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数据的不断的响应

相关文档 EventSource - Web API | MDN

QQ_1731740870923.png

<!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本身的特性。当然这也可能不是非常完美的,只是对于单向数据响应的场景中,个人认为这是非常不错的一种方案,仅供参考和学习。