【问题标题】:How to download from a server asynchronously in Kotlin? [closed]如何在 Kotlin 中从服务器异步下载? [关闭]
【发布时间】:2021-06-06 12:15:21
【问题描述】:

我正在尝试异步逐行从服务器下载数据。其实我用的是Kotlin Coroutines,但是当我在async lambda块中连接httpurlconnection时,它显示了一个警告,该方法将在那里被阻塞,所以我无法异步执行请求。

这是代码:

fun download(urlString : String) = runBlocking{
    val url = URL(urlString)
    val httpurlconnection = url.openConnection() as HttpURLConnection
    httpurlconnection.setRequestMethod("GET")
    val response = async {
        httpurlconnection.connect()
        val scanner = java.util.Scanner(httpurlconnection.inputstream)
        val sb = StringBuilder()
        while(scanner.hasNextLine()){
            val string = scanner.nextLine()
            println(string)
            sb.append(string)
        }
        sb.toString()
    }
    println("length of response = ${response.length}")
}

【问题讨论】:

  • 如果你要构建一个完整的字符串,那么逐行阅读正文有什么意义?为什么要立即将整个流读入字符串?
  • 另外,你到底想在这里做什么异步?在您当前的代码中,download() 处于阻塞状态:在完全下载正文之前它不会返回。您可能应该在这里多解释一下您要实现的目标。您是否希望 download() 完全异步,因为调用者可以在下载发生时继续前进?
  • 我已经编辑了问题以删除查找库的请求,因为我认为这是 XY 问题的结果,核心问题是其他人将来可能会遇到的问题,并且有一个很好的解决它的答案。在修改后的状态下,我认为应该重新提出问题。

标签: kotlin asynchronous kotlin-coroutines


【解决方案1】:

那是因为您当前使用的是runBlocking

您需要在协程范围内启动它以使其异步,另外您可以将操作切换到IO

suspend fun download(urlString : String): Result = withContext(Dispatchers.IO){
    ...
}

并从您的特定协程范围调用它,例如:

MainScope().launch {
    val result = download(baseUrl)
}

【讨论】:

    【解决方案2】:

    这里有几个问题:

    1. 由于runBlocking,您当前的整个download() 函数无论如何都会阻塞当前线程。如果在整个请求期间阻塞当前线程,该方法将不是异步的。如果您阻止当前线程等待它,那么在其中启动单个 async 协程是没有意义的。

    2. 您收到的警告是因为您在协程中使用了阻塞 API,通常不鼓励这样做,因为它会阻塞协程正在运行的线程,而不是协程的设计使用方式。

    3. 当前代码正在读取正文中的所有行以从中构建一个 String,这使得逐行读取有点毫无意义。

    对于问题 #1,您可以通过将函数声明为 suspend 函数而不是使用 runBlocking 来使函数异步,并且您可以摆脱 async。不过,这确实转移了在download() 的调用站点处理异步的负担。您需要在协程中调用它(使用像 async 这样的协程构建器),并且为此需要一个协程范围。

    对于问题 #2,通常的解决方法是在 withContext(Disptachers.IO) { ... } 块中执行阻塞调用,因此它们在线程池中完成,该线程池将根据 IO 操作的需要创建新线程。如果您可以改用异步 API,则不必求助,因为您只需将这些 API 映射到协程模型即可。

    在这种情况下,HttpUrlConnection 是一个不方便的低级 API,除了阻塞。如果您在 JRE 11+ 上运行,则可以改用 JDK11 内置的异步 API HttpClient,并将其适配为协程。

    暂时忽略问题 #3,因为您当前的代码无论如何都在读取整个正文,一个简单的等价物如下:

    import kotlinx.coroutines.future.await
    import java.net.URI
    import java.net.http.*
    
    suspend fun download(urlString : String): String {
        val client = HttpClient.newHttpClient()
        val request = HttpRequest.newBuilder().uri(URI.create(urlString)).build()
        val futureResponse = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
        // await() adapts the async Java future API to the coroutine world.
        // It suspends until the response is received (in a non-blocking way)
        // and until the full body is read and converted to a String
        return futureResponse.await().body()
    }
    

    此方法暂停,直到收到响应。您可以使用协程来管理调用此函数的方式。

    回到第 3 期,如果您真的想处理行来,您可以使用 BodyHandlers 之一来逐行流式传输响应,例如 BodyHandlers.ofLines()

    import kotlinx.coroutines.flow.Flow
    import kotlinx.coroutines.future.await
    import kotlinx.coroutines.stream.consumeAsFlow
    import java.net.URI
    import java.net.http.*
    
    suspend fun downloadLines(urlString : String): Flow<String> {
        val client = HttpClient.newHttpClient()
        val request = HttpRequest.newBuilder().uri(URI.create(urlString)).build()
        val futureResponse = client.sendAsync(request, HttpResponse.BodyHandlers.ofLines())
        // await() here only waits for the reponse to arrive,
        // it doesn't wait until the full response body is read.
        // Instead, the Stream<String> returned by body() provides
        // the body lines as they come
        return futureResponse.await().body().consumeAsFlow()
    }
    

    此方法在调用之前暂停,但可能在读取响应之前返回。收集返回的流是在响应从服务器流式传输时读取响应正文行的方式。

    另一种选择是一点一点地使用Ktor HTTP clientread from the response body。看看如何阅读回复,或者更具体地说是response streaming documentation。 不过,我不得不承认文档非常简洁。所以通常你必须自己弄清楚细节。

    【讨论】:

    • 您提到最好使用已经异步的 API 并将其映射到协程,并且您的推理听起来像是在暗示这样的 API 不是阻塞线程,这是不可能的。他们将使用自己的线程池而不是 Dispatchers.IO。是否还有其他理由避免使用 Dispatchers.IO?
    • @Tenfour04 一些异步库可以使用本机非阻塞机制(例如 NuProcess),或者使用非阻塞轮询,它们不必阻塞线程。话虽如此,我相信他们中的大多数确实在后台使用了自己的线程(实际上是 JDK11 的 HttpClient 的情况),所以我在这个答案中的含义很糟糕,感谢您指出这一点。我将更改这部分答案。
    • 感谢您的更新。我不知道 NuProcess。
    猜你喜欢
    • 1970-01-01
    • 2015-10-08
    • 1970-01-01
    • 2021-12-18
    • 2014-07-09
    • 2014-02-05
    • 1970-01-01
    • 2012-03-14
    • 1970-01-01
    相关资源
    最近更新 更多