在准备写一篇关于用Kotlin进行GraphQL订阅的博文时,我首先想看一下如何用Kotlin创建一个基本的WebSockets例子。
在过去,处理WebSockets在Java中是有点尴尬的,很多时候人们用node.js代替,因为在那里感觉更自然。在这篇文章中,我们将用这个领域的一些最新技术再做一次尝试。Kotlin和Spring Boot 2.0(Spring 5)。
因为WebFlux在之前的文章中表现良好,所以在这个例子中也将使用它。功能性反应式编程应该对WebSockets有很好的效果,因为它们基本上是一个双向的、基于推送的数据流。
在这篇文章的例子中,我们的客户端(静态index.html )将打开一个WebSocket连接,创建一个随机的clientId并开始猜测数字(Math.random() * 100 )。客户端将向服务器发送每个猜中的数字和客户端ID。
服务器将收到猜测的数字,检查它们是否是质数,如果是,则将它们广播给所有订阅的客户端。
在客户端,我们将监听这些质数,并显示所有广播的数字,除了我们自己的客户端ID的数字。
这就是它在客户端上的样子。
这个例子对于这样一篇短文来说足够小,但它包含了数据的实时发送和接收,以及转换和多路复用,所以它应该是有趣的。
让我们开始吧。
例子
我们将从客户端代码开始。首先,我们要创建一个随机的clientId ,然后打开一个WebSocket 。
var clientId = parseInt(Math.random()*100000);
document.querySelector('#clientid').innerHTML += clientId;
var ws = new WebSocket("ws://localhost:8080/ws/primes");
ws.onopen = function() { console.log('connection established') };
ws.onclose = function() { console.log('connection closed') };
ws.onerror = function(err) { console.log('error: ', err)};
然后,我们开始猜测数字,并将它们以clientId:number 的格式发送到客户端。
window.setInterval(function() {
var rand = parseInt(Math.random()*100);
document.getElementById('rand').innerHTML = rand;
ws.send(clientId + ":" + rand);
}, 500);
最后我们注册一个onmessage 处理程序,这样我们就可以显示服务器向我们广播的数据。
ws.onmessage = function(message) {
var sender = message.data.split(":")[0];
var value = message.data.split(":")[1];
if (sender !== clientId.toString()) {
document.getElementById('primes').innerHTML += '<div>' + sender + " found a prime: <b>" + value + '</b></div>';
}
};
这就是客户端的工作。这将适用于任何具有上述ID字段的HTML。
现在在服务器上,我们使用从Spring Initializrfor Kotlin和Spring Boot生成的模板,使用Reactive Web。
首先,我们定义一个事件的数据模型。
data class Event(val sender: Int, val value: Int)
然后我们继续定义WebSocketHandler 。我们的目标是,从客户端receive 数据,将其解析为一个Event ,过滤掉非质数,并将质数发送到所有客户端。
我确信有几种(可能是更优雅的)方法来实现这个目标,但在这个例子中,我选择了使用一个共享的TopicProcessor,它使我们能够从多个线程异步地传递消息。
@Component
class PrimeNumbersHandler : WebSocketHandler {
private val processor = TopicProcessor.share<Event>("shared", 1024)
如果有一个客户端连接,我们handle ,如下所示。
override fun handle(session: WebSocketSession): Mono<Void> {
return session.send(
processor
.map { ev -> session.textMessage("${ev.sender}:${ev.value}") }
)
上面的片段意味着,只要在共享的processor ,有一个新的值,我们就把它下发到客户端,映射成一个格式为clientId:number 的字符串。
我们还需要定义一个处理程序来接收来自客户端的数据。
.and(
session.receive()
.map { ev ->
val parts = ev.payloadAsText.split(":")
Event(sender = parts[0].toInt(), value = parts[1].toInt())
}
.filter { ev -> isPrime(ev.value) }
.doOnNext { ev -> processor.onNext(ev) }
)
我们接收数据,将其映射为一个Event ,过滤掉非整数,然后将该值发送到共享processor 。
我使用了这个帖子中的isPrime 函数,但它与这个例子并不特别相关。
唯一要做的是将处理程序连接到我们的Webflux应用程序。为此,我们创建了一个WebSocketHandlerAdapter 和一个handlerMapping ,其中/ws/primes 路线指向我们的PrimeNumbersHandler 。
@Configuration
@EnableWebFlux
@ComponentScan(value = ["org.zupzup.kotlinwebfluxdemo"])
class WebSocketConfig(
val primeNumbersHandler: PrimeNumbersHandler
) {
@Bean
fun websocketHandlerAdapter() = WebSocketHandlerAdapter()
@Bean
fun handlerMapping() : HandlerMapping {
val handlerMapping = SimpleUrlHandlerMapping()
handlerMapping.urlMap = mapOf(
"/ws/primes" to primeNumbersHandler
)
handlerMapping.order = 1
return handlerMapping
}
}
就这样了!
如果你运行这个文件并在多个浏览器标签中打开index.html ,你会看到所有正确猜到的首字母数字都被广播到每个客户端。
你可以在这里找到完整的示例代码。
总结
Spring 5处理WebSockets的方式非常简洁和直观,我在启动和运行方面没有任何问题。
关于Webflux,或者说reactor ,我遇到了一些问题,因为我过去只用RxJS做了一点FRP,反应器的术语对我来说还是有点陌生。
另外,我很难找到一种将传入值传递给传出值的通量的习惯性方法。虽然reactor的文档很好,但我没有找到很多例子,特别是与Rx系列的伟大例子相比。
我希望这种情况能得到改善,以降低入门门槛。不过总的来说,我喜欢我能够快速而简洁地解决这个例子,即使没有很多以前的知识。
所以请继续关注未来更多的Kotlin Webflux行动。)