如何使用Spring WebFlux和WebSockets确保Reactive Stream完成

时间:2018-04-03 13:47:21

标签: websocket kotlin reactive-programming spring-webflux project-reactor

我在Kotlin为Spring WebFlux编写了一个测试客户端和服务器。客户端向服务器发送号码(例如4)并返回那么多号码(例如0,1,2和3)。这是服务器实现:

class NumbersWebSocketHandler : WebSocketHandler {
    override fun handle(session: WebSocketSession): Mono<Void> {
        var index = 0
        var count = 1
        val publisher = Flux.generate<Int> { sink ->
            if (index < count) {
                sink.next(index)
                index++
            } else {
                sink.complete()
            }
        }.map(Int::toString)
            .map(session::textMessage)
            .delayElements(Duration.ofMillis(500))

        return session.receive()
            .map(WebSocketMessage::getPayloadAsText)
            .doOnNext {
                println("About to send $it numbers")
                count = it.toInt()
            }
            .then()
            .and(session.send(publisher))
            .then()
    }
}

这是客户:

fun main(args: Array<String>) {
    val uri = URI("ws://localhost:8080/numbers")
    val client = ReactorNettyWebSocketClient()

    println("How many numbers would you like?")
    val input = Flux.just(readLine())

    client.execute(uri) { session ->
        session.send(input.map(session::textMessage))
            .then(
                session.receive()
                    .map(WebSocketMessage::getPayloadAsText)
                    .map { it.toInt() }
                    .reduce { a,b ->
                        println("Reduce called with $a and $b")
                        a + b
                    }
                    .doOnNext(::println)
                    .then()
            )
            .then()
    }.block()
}

客户端成功接收数字,并调用reduce,如下所示:

  

用0和1

减少调用      

使用1和2减少调用

     

使用3和3减少调用

然而,永远不会达到对 doOnNext 的调用 - 大概是因为客户端不知道最后一个项目已被发送。我的问题是,我需要在客户端或服务器上添加哪些代码才能打印总数?

更新:关闭服务器端的会话无济于事。我试过了:

.delayElements(Duration.ofMillis(500))
.doOnComplete { session.close() }

还有:

.delayElements(Duration.ofMillis(500))
.doFinally { session.close() }

但是对客户的行为都没有任何影响。也没有在调用'send'后明确关闭会话:

.and(session.send(publisher))
.then()
.and { session.close() }
.then()

2 个答案:

答案 0 :(得分:1)

Mono<Void>返回的WebSocketHandler指示处理完成的时间,进而指示WebSocket连接应保持打开状态的时间。问题在于,返回的Mono<Void>的两面将永远无法完成。

客户端使用reduce来等待来自服务器端的输入结束,但是服务器使用receive() + doOnNext + then()来等待永远接收消息。因此客户端和服务器都互相等待。在客户端添加take有助于打破僵局:

client.execute(uri, session -> session
        .send(input.map(session::textMessage))
        .then(session.receive()
                .map(WebSocketMessage::getPayloadAsText)
                .map(Integer::valueOf)
                .take(3)
                .reduce((a,b) -> {
                    logger.debug("Reduce called with " + a + " and " + b);
                    return a + b;
                })
                .doOnNext(logger::debug)
        )
        .then()
).block();

第二个问题是服务器的组成不正确。发送与通过.and接收同时触发。相反,发送流应首先依赖于接收计数:

session.receive()
        .map(WebSocketMessage::getPayloadAsText)
        .flatMap(s -> {
            logger.debug("About to send " + s + " numbers");
            count.set(Integer.parseInt(s));
            return session.send(publisher);
        })
        .then()

答案 1 :(得分:0)

marble diagram for reduce可以看出,它希望上游发布者发送onComplete事件来发送事件本身。我不确定,但我认为仅在连接正常终止时才会发出。这就是doOnNext永远不会被执行的原因。

而不是reduce我认为您应该使用subscribe()代替,并定义您自己的Subscriber实例,您可以保留状态。

编辑:您无法在此处使用subscribe方法。如果在服务器上关闭连接,它应该像这样工作:

.map(Int::toString)
        .map(session::textMessage)
        .delayElements(Duration.ofMillis(500))
        .then(session::close)