HTTP 客户端请求

预计阅读时间: 8 分钟

简单的请求

基本用法简单:只需实例化一个 HttpClient 实例、 指定一个引擎——例如: ApacheOkHttpAndroidIosJsJettyCIO 或者 Mock, 并使用众多可用的便利方法之一发出请求。

引擎可以省略,对于 JVM,Ktor 会使用 ServiceLoader 从所包含的构件中选择一个可用的引擎;对于其他平台 Ktor 会选用默认实现。

首先需要实例化客户端:

val client = HttpClient()

然后,执行读取完整 StringGET 请求:

val htmlContent = client.get<String>("https://en.wikipedia.org/wiki/Main_Page")

而如果对原始数据感兴趣,可以读取 ByteArray

val bytes: ByteArray = client.get<ByteArray>("http://127.0.0.1:8080/")

可以对请求进行大量定制,并且流式传输请求与响应有效载荷:

val channel: ByteReadChannel = client.get<ByteReadChannel>("http://127.0.0.1:8080/")

使用完客户端之后,应该关闭它以彻底停止底层引擎。

client.close()

如果只使用客户端发出一个请求,考虑使用 use。一旦传入的块执行完,客户端会自动关闭。

val status = HttpClient().use { client ->
    client.get<HttpStatusCode>("http://127.0.0.1:8080/check")
}

定制请求

我们不能只进行 get 请求,Ktor 允许使用任何 HTTP 动词构件复杂请求,并且灵活地以多种方式处理响应。

call 方法

HttpClient 的 call 方法返回一个 HttpClientCall,可以用它执行简单的未指定类型的请求。

可以使用 response: HttpResponse 读取其内容。 更详细信息请参见使用 HttpResponse 接收内容部分。

val call = client.call("http://127.0.0.1:8080/") {
    method = HttpMethod.Get
}
println(call.response.receive<String>())

request 方法

除了 call 之外,还有一个 request 方法用于执行类型化的请求, 它接收指定类型如字符串、 HttpResponse 或者任何其他类。 必须在构建请求时指定 URL 与方法。

val call = client.request<String> {
    url("http://127.0.0.1:8080/")
    method = HttpMethod.Get
}

getpostputdeletepatchhead 以及 options 方法

request 类似,还有几个可以使用最常见的 HTTP 动词(GETPOSTPUTDELETEPATCHHEAD 以及 OPTIONS)执行请求的扩展方法。

val text = client.post<String>("http://127.0.0.1:8080/")

当调用 request 方法时,可以提供一个 lambda 表达式来构建请求参数,如 URL、HTTP 方法(动词)、正文或者请求头。 HttpRequestBuilder 类似这样:

class HttpRequestBuilder : HttpMessageBuilder {
    var method: HttpMethod

    val url: URLBuilder
    fun url(block: URLBuilder.(URLBuilder) -> Unit)

    val headers: HeadersBuilder
    fun header(key: String, value: String)
    fun headers(block: HeadersBuilder.() -> Unit)

    var body: Any = EmptyContent

    val executionContext: CompletableDeferred<Unit>
    fun setAttributes(block: Attributes.() -> Unit)
    fun takeFrom(builder: HttpRequestBuilder): HttpRequestBuilder
}

HttpClient 类只提供一些基本功能,而所有构建请求的方法都是扩展方法。 参见标准的 HttpClient 构建扩展方法

submitFormsubmitFormWithBinaryData 方法

There are a couple of convenience extension methods for submitting form information. The detailed refrence is listed here.

submitForm 方法:

client.submitForm(
    formData: Parameters = Parameters.Empty,
    encodeInQuery: Boolean = false,
    block: HttpRequestBuilder.() -> Unit = {}
)

It allows requesting with the Parameters encoded in the query string(GET by default) or requesting with the Parameters encoded as multipart(POST by default) depending on the encodeInQuery parameter.

submitFormWithBinaryData 方法:

client.submitFormWithBinaryData(
    formData: List<PartData>,
    block: HttpRequestBuilder.() -> Unit = {}
): T

It allows to generate a multipart POST request from a list of PartData. PartData can be PartData.FormItem, PartData.BinaryItem or PartData.FileItem.

To build a list of PartData, you can use the formData builder:

val data: List<PartData> = formData {
    // Can append: String, Number, ByteArray and Input.
    append("hello", "world")
    append("number", 10)
    append("ba", byteArrayOf(1, 2, 3, 4))
    append("input", inputStream.asInput())
    // Allow to set headers to the part:
    append("hello", "world", headersOf("X-My-Header" to "MyValue"))
}

指定自定义头

When building requests with HttpRequestBuilder, you can set custom headers. There is a final property val headers: HeadersBuilder that inherits from StringValuesBuilder. You can add or remove headers using it, or with the header convenience methods.

// this : HttpMessageBuilder

// Convenience method to add a header
header("My-Custom-Header", "HeaderValue")

// Calls methods from the headers: HeadersBuilder to manipulate the headers
headers.clear()
headers.append("My-Custom-Header", "HeaderValue")
headers.appendAll("My-Custom-Header", listOf("HeaderValue1", "HeaderValue2"))
headers.remove("My-Custom-Header")

// Applies the headers with the `headers` convenience method
headers { // this: HeadersBuilder
    clear()
    append("My-Custom-Header", "HeaderValue")
    appendAll("My-Custom-Header", listOf("HeaderValue1", "HeaderValue2"))
    remove("My-Custom-Header")
}

Complete HeadersBuilder API is listed here.

指定请求的正文

For POST and PUT requests, you can set the body property:

client.post<Unit> {
    url("http://127.0.0.1:8080/")
    body = // ...
}

The HttpRequestBuilder.body property can be a subtype of OutgoingContent as well as a String instance:

  • body = "HELLO WORLD!"
  • body = TextContent("HELLO WORLD!", ContentType.Text.Plain)
  • body = ByteArrayContent("HELLO WORLD!".toByteArray(Charsets.UTF_8))
  • body = LocalFileContent(File("build.gradle"))
  • body = JarFileContent(File("myjar.jar"), "test.txt", ContentType.fromFileExtension("txt").first())
  • body = URIFileContent("https://en.wikipedia.org/wiki/Main_Page")

If you install the JsonFeature, and set the content type to application/json you can use arbitrary instances as the body, and they will be serialized as JSON:

data class HelloWorld(val hello: String)

val client = HttpClient(Apache) {
    install(JsonFeature) {
        serializer = GsonSerializer {
            // Configurable .GsonBuilder
            serializeNulls()
            disableHtmlEscaping()
        }
    }
}

client.post<Unit> {
    url("http://127.0.0.1:8080/")
    body = HelloWorld(hello = "world")
}

Alternatively(using the integrated JsonSerializer):

val json = io.ktor.client.features.json.defaultSerializer()
client.post<Unit>() {
    url("http://127.0.0.1:8080/")
    body = json.write(HelloWorld(hello = "world")) // Generates an OutgoingContent
}

Or using Jackson(JVM only):

val json = jacksonObjectMapper()
client.post<Unit> {
    url("http://127.0.0.1:8080/")
    body = TextContent(json.writeValueAsString(userData), contentType = ContentType.Application.Json)
}

Remember that your classes must be top-level to be recognized by Gson.
If you try to send a class that is inside a function, the feature will send a null.

上传 multipart/form-data

Ktor HTTP Client has support for making MultiPart requests. The idea is to use the MultiPartFormDataContent(parts: List<PartData>) as OutgoingContent for the body of the request.

The easiest way is to use the submitFormWithBinaryData method.

Alternatively, you can set the body directly:

val request = client.request {
    method = HttpMethod.Post
    body = MultiPartFormDataContent(formData {
        append("key", "value")
    })
}