Ktor HttpClient挂在runBlocking中

时间:2019-05-28 17:42:17

标签: kotlin kotlin-coroutines ktor

我正在使用Ktor的HttpClient(Ktor版本为1.2.1)在服务器端Kotlin中验证App Store配方。到目前为止,这是我的代码:

class AppStoreClient(
        val url: String,
        val password: String,
        val excludeOldTransactions: Boolean = true
) {
    private val objectMapper = ObjectMapperFactory.defaultObjectMapper()
    private val client = HttpClient(Apache /* tried with CIO as well */) {
        install(JsonFeature) {
            serializer = JacksonSerializer()
        }
    }

    suspend fun validate(receipt: String): VerifyReceiptResponse {
        val post = client.post<String> {
            url(this@AppStoreClient.url)
            contentType(ContentType.Application.Json)
            accept(ContentType.Application.Json)
            body = VerifyReceiptRequest(
                    receipt,
                    password,
                    excludeOldTransactions
            )
        }

        // client.close()

        // Apple does not send Content-Type header ¯\_(ツ)_/¯
        // So Ktor's deserialization is not working here and
        // I have to manually deserialize the response.
        return objectMapper.readValue(post)
    }
}

这里我正在测试:

fun main() = runBlocking {
    val client = AppStoreClient("https://sandbox.itunes.apple.com/verifyReceipt", "<password>")

    println(client.validate("<recipe1>"))
    // println(client.validate("<recipe2>"))
    // println(client.validate("<recipe3>"))
}

我在输出中获得了所有响应(一个或三个),但是我的应用程序只是挂起,并且从未退出main方法。似乎runBlocking仍在等待某些东西,例如client.close。确实,如果我在第一个请求之后关闭了客户端,则应用程序成功结束,但这将迫使我在每个单独的验证请求上创建客户端。客户端的管道配置似乎很耗时,AppStoreClient旨在成为一个长期存在的对象,因此我认为客户端可以共享其生命周期(甚至可能注入了依赖项)。

io.ktor.client.HttpClient是一个可以长期用于多个请求的对象吗?还是应该为每个请求创建一个新对象?

如果是,我在做什么错,所以runBlocking挂起了?


P.S。该代码适用于Ktor 1.1.1!是一个错误吗?


P.P.S。此代码也将挂起:

fun main() {
    val client = AppStoreClient("...", "...")

    runBlocking {
        println(client.validate("..."))
        println(client.validate("..."))
        println(client.validate("..."))
    }

    runBlocking {
        println(client.validate("..."))
        println(client.validate("..."))
        println(client.validate("..."))
    }
}

所以我可能会认真考虑关闭客户。

1 个答案:

答案 0 :(得分:1)

  

io.ktor.client.HttpClient是一个可以长期用于多个请求的对象吗?还是应该为每个请求创建一个新对象?

是的,建议使用单个HttpClient,因为某些资源(例如ApacheHttpClient的线程池)是在后台分配的,并且没有每次创建新客户端的理由。

  

如果是,我在做什么错,所以runBlocking挂起了?

您遇到的关闭客户而不是协程本身的问题,请考虑以下示例,该示例也会“挂起”:

fun main() {
    val client = HttpAsyncClients.createDefault().also {
        it.start()
    }
}

所以在我的实践中,关闭开发人员的客户责任,例如:

fun main() {
    val client = HttpAsyncClients.createDefault().also {
        it.start()
    }

    client.close() // we're good now

}

或者在更复杂的应用程序中使用Runtime.addShutodownHook

  

P.S。该代码适用于Ktor 1.1.1!是一个错误吗?

我认为这是一个真实的问题,1.1.1做什么,1.2.1不做(反之亦然)


UPD

根据Ktor客户端documentation,您应手动关闭客户端:

suspend fun sequentialRequests() {
    val client = HttpClient()

    // Get the content of an URL.
    val firstBytes = client.get<ByteArray>("https://127.0.0.1:8080/a")

    // Once the previous request is done, get the content of an URL.
    val secondBytes = client.get<ByteArray>("https://127.0.0.1:8080/b")

    client.close()
}