RSocket Kotlin入门
在Kotlin和Ktor的帮助下,建立一个RSocket客户端和服务器。
RSocket是一个为反应式应用程序设计的传输协议。关于RSocket的更多信息可以在其网站上找到,让我重点写一下RSocket 和Kotlin 如何结合。
RSocket有几个用各种语言编写的库,实现了RSocket协议。对于Kotlin来说,这是Ktor(Kotlin客户端和网络服务器库)的一个扩展,名为rsocket-kotlin。我们将通过这篇文章来了解这个扩展。
RSocket + Ktor库大量使用了Kotlin的循环程序和流程。如果你以前没有使用过这些,我建议先看看这些。编写这篇文章的内容是我第一次使用流程的经历,虽然它们类似于我习惯的构造,但在正确的时间调用正确的方法仍然被证明是一种挑战。
在这篇文章中,我们将构建两个服务;一个是通过HTTP处理入站请求,一个是只暴露RSocket端点的后端服务。这使我能够展示工作中的RSocket客户端和服务器,以及摆弄一些Ktor功能以使其更加有趣。在后面的章节中,入站服务将被称为 "客户端",后端服务被称为 "服务器"。
依赖关系
在写这篇文章的时候,你会想使用以下的依赖性。
XML
implementation("io.rsocket.kotlin:rsocket-core:0.15.4")
implementation("io.rsocket.kotlin:rsocket-ktor-client:0.15.4")
implementation("io.rsocket.kotlin:rsocket-ktor-server:0.15.4")
implementation("io.ktor:ktor-server-netty:2.0.1")
implementation("io.ktor:ktor-client-cio:2.0.1")
正如你所看到的,RSocket的依赖性都是0.x.x ,因此,API会随着时间的推移而波动。希望在我写完这篇文章后,它们能保持稍微稳定,这样我就不必更新它了......
入站和后端服务的构建块
在下面代表入站和后端服务的两个部分中,我们将看看端点可以建立的基础代码。
入站服务(RSocket客户端+HTTP服务器)
Kotlin
fun main() {
val client = HttpClient {
install(WebSockets)
install(RSocketSupport)
}
embeddedServer(Netty, port = 8000) {
install(io.ktor.server.websocket.WebSockets)
routing {
// Add HTTP endpoints
}
}.start(wait = true)
}
这里我们有一个HTTPClient ,它将被用来发出RSocket请求。我知道它看起来与RSocket没有关系;现在,它还没有。它是由Ktor提供的一个普通的旧HTTPClient ;但是,正如你稍后看到的,RSocket客户端库提供了扩展函数,利用HTTPClient 来发出请求。
客户端安装WebSockets 和RSocketSupport ,以通过WebSockets和RSockets的插件分别增加功能。这里必须指出,WebSockets 必须在RSocketSupport 之前安装;否则,你将收到以下错误:
Exception in thread "main" java.lang.IllegalStateException: RSocket require WebSockets to work. You must install WebSockets plugin first.
at io.rsocket.kotlin.ktor.client.RSocketSupport$Plugin.install(RSocketSupport.kt:49)
at io.rsocket.kotlin.ktor.client.RSocketSupport$Plugin.install(RSocketSupport.kt:40)
at io.ktor.client.HttpClientConfig$install$3.invoke(HttpClientConfig.kt:78)
at io.ktor.client.HttpClientConfig$install$3.invoke(HttpClientConfig.kt:73)
at io.ktor.client.HttpClientConfig.install(HttpClientConfig.kt:96)
at io.ktor.client.HttpClient.<init>(HttpClient.kt:165)
at io.ktor.client.HttpClient.<init>(HttpClient.kt:80)
at io.ktor.client.HttpClientKt.HttpClient(HttpClient.kt:42)
at io.ktor.client.HttpClientJvmKt.HttpClient(HttpClientJvm.kt:21)
at dev.lankydan.inbound.InboundKt.main(Inbound.kt:38)
at dev.lankydan.inbound.InboundKt.main(Inbound.kt)
值得庆幸的是,这个异常是很好的、明确的,告诉你如何纠正这个问题。
然后,我们有非RSocket部分,在这里创建了一个HTTP服务器,代表了系统的入口。WebSockets ,这与RSocket本身没有关系,但在这篇文章的例子中使用,使它们更有趣。
后端服务(RSocket服务器)
Kotlin
fun main() {
embeddedServer(Netty, port = 9000) {
install(WebSockets)
install(RSocketSupport)
routing {
// Add RSocket endpoints
}
}.start(wait = true)
}
到了后端服务,它只提供RSocket端点,入站服务与之进行通信。这意味着不需要创建一个HTTPClient 。这一次,由embeddedServer 提供的HTTP服务器安装了WebSockets 和RSocketSupport 。
RSocket请求类型
在下面的章节中,我们将看一下每个RSocket请求类型的例子,其中包括:
- Fire-and-forget。
- 请求响应。
- 请求流。
- 请求通道。
以下各节中的例子将详细介绍每种RSocket请求类型的客户端和服务器端实现。
发射和遗忘
Fire and forget请求向一个端点发送一些数据,然后继续前进,不期望有响应被送回来。这使得客户端和服务器端的实现都得到了优化。
来自RSocket文档:
Fire-and-forget是对请求/响应的一种优化,在不需要响应时很有用。它允许显著的性能优化,不仅是通过跳过响应来节省网络使用量,而且在客户端和服务器处理时间方面也是如此,因为不需要记账来等待和关联响应或取消请求。
客户端
Kotlin
fun Routing.fireAndForget(client: HttpClient) {
get("fireAndForget") {
val rSocket: RSocket = client.rSocket(path = "fireAndForget", port = 9000)
rSocket.fireAndForget(buildPayload { data("Hello") })
log.info("Completed fire and forget request")
call.respondText { "Completed" }
}
}
要想发送一个fire-and-forget请求,可以通过调用rSocket ,在之前制作的HTTPClient ,创建一个RSocket 。这是RSocket库提供的一个扩展函数,用于利用Ktor的HTTPClient 。当创建一个RSocket ,定义端点的路径(默认为localhost )并指定一个端口。然后使用buildPayload 调用RSocket.fireAndForget 来构建要发送的数据。buildPayload 使用lambda和PayloadBuilder 来协助构建一个有效载荷。
在调用fireAndForget 之后,就没有什么可做的了,因为你要忘记响应。
服务器端
Kotlin
fun Routing.fireAndForget() {
rSocket("fireAndForget") {
RSocketRequestHandler {
fireAndForget { request: Payload ->
val text = request.data.readText()
log.info("Received request (fire and forget): '$text' ")
}
}
}
}
在服务器端,rSocket 定义了端点的路径,然后RSocketRequestHandler 和fireAndForget 一起指定了要处理的请求类型。这种模式在下面的章节中重复出现,只有对fireAndForget 的调用被替换成不同的请求类型。
每个请求处理程序都被提供了一些参数,使其能够为请求提供服务。在这个fireAndForget 的例子中,只提供了请求的Payload ,因为这是它所需要的全部。
请求响应
在上一节的知识基础上,requestResponse 和fireAndForget 请求之间没有太大的区别。因此,本节将是简短而温馨的。
客户端
Kotlin
fun Routing.requestResponse(client: HttpClient) {
get("requestResponse") {
val rSocket: RSocket = client.rSocket(path = "requestResponse", port = 9000)
val response: Payload = rSocket.requestResponse(buildPayload { data("Hello") })
val text = response.data.readText()
log.info("Received response from backend: '$text'")
call.respondText { text }
}
}
这与fireAndForget 的例子几乎一样,唯一的区别是服务器回复的响应Payload 的存在。
服务器端
Kotlin
fun Routing.requestResponse() {
rSocket("requestResponse") {
RSocketRequestHandler {
requestResponse { request: Payload ->
val text = request.data.readText()
log.info("Received request (request/response): '$text' ")
delay(200)
buildPayload { data("Received: '$text' - Returning: 'some data'") }
}
}
}
}
这个请求处理程序看起来也很熟悉。这里我们调用requestResponse ,然后从函数中返回一个Payload 。这个Payload 在响应中被发回给客户端。
请注意,传入
requestResponse的函数是一个悬浮函数,允许它调用类似delay的方法。每个请求处理方法也共享这种行为。
请求流
请求流是一个数据流,从RSocket端点向一个方向流动,流向客户端。这种数据流可以是有限的,也可以是不确定的;然而,任何一种都可以取消,以终止连接双方的数据流。
客户端
Kotlin
fun Routing.requestStream(client: HttpClient) {
webSocket("requestStream") {
val rSocket: RSocket = client.rSocket(path = "requestStream", port = 9000)
val stream: Flow<Payload> = rSocket.requestStream(buildPayload { data("Hello") })
// Receives data via a WebSocket
incoming.receiveAsFlow().onEach { frame ->
log.info("Received frame: $frame")
if (frame is Frame.Text && frame.readText() == "stop") {
log.info("Stop requested, cancelling socket")
this@webSocket.close(CloseReason(CloseReason.Codes.NORMAL, "Client called 'stop'"))
}
}.launchIn(this)
// Handles data sent back over the RSocket connection
stream.onCompletion {
log.info("Connection terminated")
}.collect { payload: Payload ->
val data = payload.data.readText()
log.info("Received payload: '$data'")
delay(500)
send("Received payload: '$data'")
}
}
}
上面的例子看起来比较复杂。然而,这部分是由于前几节中没有的额外WebSocket代码造成的。
现在让我们来看看这段代码:
requestStream 调用并传递一个 ,代表RSocket端点收到的初始 ,返回一个 的 s,供客户端处理。在这个例子中,它记录了收到的有效载荷,并通过WebSockets将它们发送给HTTP端点的原始调用者。它通过调用 ,这是对 API的Payload Payload Flow Payload collect Flow 终端操作,触发了 的执行(更多信息可以在Flow Flow文档中找到)。
这里在collect 内调用的delay 很有意思。RSocket为你管理背压,这意味着如果收到的有效载荷比它们能够被处理的速度快,RSocket终端将停止流化有效载荷,直到前一批处理完毕。如果你想自己看看,你可以运行这些例子中的代码。
在这一点上,我们不知道请求流是否是有限的,但无论它是运行到完成还是被取消,onCompletion 函数都将执行。
我们还没有经过的代码是通过WebSocket处理传入的数据。incoming 是一个ReceiveChannel ,它就做这个。通过将其与receiveAsFlow 链接,以简化对接收数据的操作,并启动Flow ,使其作为一个单独的Job 运行,可以让它在另一个线程上运行,一旦WebSocket结束就会终止(感谢Coroutines)。如果忘记调用launchIn ,将导致传入的数据被忽略,因为Flow 从来没有开始处理。你也不想在这里调用collect ,因为它是阻塞的,会阻止方法中后来调用的RSocket代码运行。相信我,我因为这个错误损失了很多时间。
这个例子的最后一个有趣部分是对this@websocket.close 的调用,它终止了WebSocket和RSocketFlow ,因为它存在于webSocket 函数的CoroutineScope 。Flow 的这种终止被发送到 RSocket 端点,从而结束那里运行的流。
服务器端
Kotlin
fun Routing.requestSteam() {
rSocket("requestStream") {
RSocketRequestHandler {
requestStream { request: Payload ->
val prefix = request.data.readText()
log.info("Received request (stream): $prefix")
flow {
emitDataContinuously(prefix)
}.onCompletion { throwable ->
if (throwable is CancellationException) {
log.info("Connection terminated")
}
}
}
}
}
}
suspend fun FlowCollector<Payload>.emitDataContinuously(prefix: String) {
var i = 0
while (true) {
val data = "data: ${if (prefix.isBlank()) "" else "($prefix) "}$i"
log.info("Emitting $data")
emitOrClose(buildPayload { data(data) })
i += 1
delay(200)
}
}
上面的服务器端代码将数据连续发射到连接到客户端的流中。
与其他请求处理程序一样,收到一个初始Payload ,以确定流的整体行为。
然后创建一个Flow ,代表流(或数据流)给调用者。注意,requestStream'的调用签名是。
Kotlin
public fun requestStream(block: suspend (RSocket.(payload: Payload) -> Flow<Payload>))
意味着需要创建一个Flow ,而以任何其他方式与响应流进行交互是不可能的。
数据是通过使用一个while循环连续发射数据的,该循环每次都会调用emitOrClose ,将数据发送到响应流。与emit 相比,emitOrclose 是一种更优雅的发送数据的方式,因为它明确地处理取消,所以它可以关闭任何正在发送的Payloads,以防止任何内存泄漏。
请求通道
一个请求通道是一个双向的数据流。请求流只从服务器向客户端发送数据,而请求通道允许双方发送和接收数据;然后可以使用这种机制来模拟更有趣的行为,因为任何一方都可以持续影响另一方。
客户端
Kotlin
fun Routing.requestChannel(client: HttpClient) {
webSocket("requestChannel") {
val rSocket: RSocket = client.rSocket(path = "requestChannel", port = 9000)
// Receives data via a WebSocket and transforms it
val payloads: Flow<Payload> = incoming.receiveAsFlow().transform { frame ->
if (frame is Frame.Text) {
val text = frame.readText()
log.info("Received text: $text")
if (text == "stop") {
log.info("Stop requested, cancelling socket")
this@webSocket.close(CloseReason(CloseReason.Codes.NORMAL, "Client called 'stop'"))
} else {
emitOrClose(buildPayload { data(text) })
}
}
}
val stream: Flow<Payload> = rSocket.requestChannel(buildPayload { data("Hello") }, payloads)
// Handles data sent back over the RSocket connection
stream.onCompletion {
log.info("Connection terminated")
}.collect { payload: Payload ->
val data = payload.data.readText()
log.info("Received payload: '$data'")
delay(500)
send("Received payload: '$data'")
}
}
}
其中一些代码与请求流的实现重叠,区别在于传入requestChannel 方法的Flow 。
输入的Flow 代表传输到请求端点的数据流,而返回的Flow 则是接收端点发送的数据。
该示例利用了通过incoming 方法(与请求流片段中的方法相同)通过WebSocket接收的数据。传入的数据被转换为Flow ,然后transform,将数据转换为Payload,通过通道发送。尽管在某种程度上,这是 "传入 "数据,但从创建的RSocket通道的角度来看,它被转换并被视为传出流。这表明你可以使用Kotlin的Flow,写出灵活的代码,将各种概念融为一体,而RSocket API正是利用了这一点。
服务器端
Kotlin
private fun Routing.requestChannel() {
rSocket("requestChannel") {
RSocketRequestHandler {
requestChannel { request: Payload, payloads: Flow<Payload> ->
var prefix = request.data.readText()
log.info("Received request (channel): '$prefix'")
payloads.onEach { payload ->
prefix = payload.data.readText()
log.info("Received extra payload, changed emitted values to include prefix: '$prefix'")
}.launchIn(this)
flow {
emitDataContinuously(prefix)
}.onCompletion { throwable ->
if (throwable is CancellationException) {
log.info("Connection terminated")
}
}
}
}
}
}
suspend fun FlowCollector<Payload>.emitDataContinuously(prefix: String) {
var i = 0
while (true) {
val data = "data: ${if (prefix.isBlank()) "" else "($prefix) "}$i"
log.info("Emitting $data")
emitOrClose(buildPayload { data(data) })
i += 1
delay(200)
}
}
正如我们在客户端部分看到的,一个Flow 被传递到requestChannel 方法中,以表示从客户端到服务器的数据流。因此,端点需要一种方法来接收这些数据;requestChannel's函数的参数payloads 就是这样做的。
忘记调用
launchIn,将导致传入的数据被忽略,因为Flow从来没有开始处理。你也不想在这里调用collect,因为它是阻塞的,会阻止方法中后来调用的RSocket代码运行。相信我,我因为这个错误损失了很多时间。
请不要像我这样动摇,请不要动摇。
除此以外,请求通道的实现与请求流的实现是一样的。
总结
这篇文章介绍了如何用Kotlin和Ktor使用RSocket。重点是实现,而不是 "为什么是RSocket",我会让你自己去研究(如果这是你想要的)。我承认,一开始我很难正确利用这些API。然而,一旦我有了有效的实现,它们的相似性就变得很明显了,这就是为什么我不断重复 "这和上一节是一样的"。主要的困难是使用Flow,如果你以前没有使用过,像我一样,你也可能会犯所有我犯的错误。如果你把自己的例子或实际工作建立在我在这里写的代码上,你至少应该绕过一些问题,更顺利地进展到工作实现。