Ktor HttpClient 在 runBlocking 中挂起

Posted

技术标签:

【中文标题】Ktor HttpClient 在 runBlocking 中挂起【英文标题】:Ktor HttpClient is hang in runBlocking 【发布时间】:2019-10-14 07:05:39 【问题描述】:

我正在使用 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 挂起?


附:该代码适用于 Ktor 1.1.1!是bug吗?


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】:

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

附:该代码适用于 Ktor 1.1.1!是bug吗?

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


UPD

根据Ktor Client 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()

【讨论】:

以上是关于Ktor HttpClient 在 runBlocking 中挂起的主要内容,如果未能解决你的问题,请参考以下文章

Ktor 的 HttpClient 使用的正确模式

如何使用针对 linuxX64 的 ktor-client-core 修复“未解决的参考:HttpClient”

在 Kotlin 多平台项目中使用 Ktor HttpClient 将文件作为二进制文件

处理 HttpClient Ktor 中的异常

在 ktor httpClient(js) JS 引擎中忽略自签名证书的配置

使用 Ktor HttpClient(CIO) websocket 发送内容时如何捕获 Broken pipe 异常?