如何在 Kotlin 中从服务器异步下载?
How to download from a server asynchronously in Kotlin?
我正在尝试从服务器逐行下载数据异步。其实我用的是Kotlin Coroutines,但是当我在async lambda block中连接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}")
}
那是因为您目前正在使用 runBlocking
。
您需要在协程范围内启动它以使其异步,另外您可以将操作切换为 IO
。
suspend fun download(urlString : String): Result = withContext(Dispatchers.IO){
...
}
并从您的特定协程范围调用它,例如:
MainScope().launch {
val result = download(baseUrl)
}
这里有几个问题:
由于 runBlocking
,您当前的整个 download()
函数无论如何都会阻塞当前线程。如果您在整个请求期间阻塞当前线程,则该方法将不是异步的。如果您阻塞当前线程等待它,那么在其中启动单个 async
协程是没有意义的。
您收到的警告是因为您在协程中使用阻塞 API,通常不鼓励这样做,因为它会阻塞协程所在的线程 运行 上,这不是协程的设计方式。
当前代码正在读取正文中的所有行以从中构建一个 String
,这使得逐行阅读有点毫无意义。
对于问题 #1,您可以通过将函数声明为 suspend
函数而不是使用 runBlocking
来使您的函数异步,并且您可以摆脱 async
。不过,这确实转移了在 download()
的调用站点处理异步的负担。您需要在协程中调用它(使用像 async
这样的协程构建器),并且您需要一个协程作用域。
对于问题 #2,通常的解决方法是只在 withContext(Disptachers.IO) { ... }
块中执行阻塞调用,因此它们是在线程池中完成的,该线程池将根据 IO 操作的需要创建新线程。如果您可以改用异步 API,则不必求助于它,因为您只需将这些 API 映射到协程模型即可。
在这种情况下,HttpUrlConnection
是一个不方便的低级 API,除了被阻塞。如果你是 JRE 11+ 上的 运行,你可以使用内置 JDK11 HttpClient
的异步 API,并将其适配为协程。
暂时忽略问题#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 that stream the response line by line instead, such as 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()
}
此方法在调用之前挂起,但可能 return 在读取响应之前挂起。收集 returned 流是读取从服务器流式传输的响应正文行的方法。
另一种选择是使用 Ktor HTTP client, and read from the response body little by little. See how to read the response, or more specifically the response streaming documentation。
不过,我不得不承认文档非常简洁。所以一般都得自己摸索细节。
我正在尝试从服务器逐行下载数据异步。其实我用的是Kotlin Coroutines,但是当我在async lambda block中连接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}")
}
那是因为您目前正在使用 runBlocking
。
您需要在协程范围内启动它以使其异步,另外您可以将操作切换为 IO
。
suspend fun download(urlString : String): Result = withContext(Dispatchers.IO){
...
}
并从您的特定协程范围调用它,例如:
MainScope().launch {
val result = download(baseUrl)
}
这里有几个问题:
由于
runBlocking
,您当前的整个download()
函数无论如何都会阻塞当前线程。如果您在整个请求期间阻塞当前线程,则该方法将不是异步的。如果您阻塞当前线程等待它,那么在其中启动单个async
协程是没有意义的。您收到的警告是因为您在协程中使用阻塞 API,通常不鼓励这样做,因为它会阻塞协程所在的线程 运行 上,这不是协程的设计方式。
当前代码正在读取正文中的所有行以从中构建一个
String
,这使得逐行阅读有点毫无意义。
对于问题 #1,您可以通过将函数声明为 suspend
函数而不是使用 runBlocking
来使您的函数异步,并且您可以摆脱 async
。不过,这确实转移了在 download()
的调用站点处理异步的负担。您需要在协程中调用它(使用像 async
这样的协程构建器),并且您需要一个协程作用域。
对于问题 #2,通常的解决方法是只在 withContext(Disptachers.IO) { ... }
块中执行阻塞调用,因此它们是在线程池中完成的,该线程池将根据 IO 操作的需要创建新线程。如果您可以改用异步 API,则不必求助于它,因为您只需将这些 API 映射到协程模型即可。
在这种情况下,HttpUrlConnection
是一个不方便的低级 API,除了被阻塞。如果你是 JRE 11+ 上的 运行,你可以使用内置 JDK11 HttpClient
的异步 API,并将其适配为协程。
暂时忽略问题#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 that stream the response line by line instead, such as 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()
}
此方法在调用之前挂起,但可能 return 在读取响应之前挂起。收集 returned 流是读取从服务器流式传输的响应正文行的方法。
另一种选择是使用 Ktor HTTP client, and read from the response body little by little. See how to read the response, or more specifically the response streaming documentation。 不过,我不得不承认文档非常简洁。所以一般都得自己摸索细节。