Skip to content

处理请求

Ktor 允许您在路由处理程序内部处理传入的请求并发送响应

路由处理程序在 ApplicationCall 上运行,该对象表示客户端与服务器之间的单个 HTTP 交换。它在路由处理程序中通过 call 属性可用,并且包含传入的请求 (ApplicationRequest) 和传出的响应 (ApplicationResponse)。

在路由处理程序内部,您可以使用 ApplicationCall 执行以下操作:

通用请求信息

您可以通过 call.request 属性访问请求数据。这将返回一个 ApplicationRequest 实例,该实例提供对底层 HTTP 请求信息的访问。

例如,您可以使用 call.request.uri 在 GET 请求处理程序中获取请求 URI:

kotlin
routing {
    get("/") {
        val uri = call.request.uri
        call.respondText("Request uri: $uri")
    }
}

call.respondText() 函数用于将纯文本响应发送回客户端。

标头

要访问所有 HTTP 请求标头,请使用 ApplicationRequest.headers 属性。

为了方便起见,Ktor 还提供了专用的扩展函数来访问常用的标头,例如 .acceptEncoding().contentType().cacheControl()

Cookie

要访问随请求发送的 Cookie,请使用 ApplicationRequest.cookies 属性。

有关使用 Cookie 处理会话的更多信息,请参阅会话部分。

连接详情

使用 ApplicationRequest.local 属性可以访问连接详情,例如主机名、端口和方案。

X-Forwarded- 标头

要获取通过 HTTP 代理或负载均衡器传递的请求信息,请安装 Forwarded headers 插件并使用 ApplicationRequest.origin 属性。

路径形参

处理请求时,您可以使用 ApplicationCall.parameters 属性获取路径形参的值。

例如,在下面的代码段中,对于 /user/admin 路径,call.parameters["login"] 将返回 "admin"

kotlin
get("/user/{login}") {
    if (call.parameters["login"] == "admin") {
        // ...
    }
}

查询形参

要获取 URL 查询字符串的形参,可以使用 ApplicationRequest.queryParameters 属性。

以下示例访问了对 /products?price=asc 发起的请求中的 price 查询形参:

kotlin
get("/products") {
    if (call.request.queryParameters["price"] == "asc") {
        // 按价格从低到高显示产品
    }
}

您还可以使用 ApplicationRequest.queryString() 函数获取整个查询字符串。

必选请求形参

处理请求时,通常会从路径形参查询形参标头Cookie 中提取值,并在继续处理请求之前验证它们是否存在。

Ktor 提供了一系列辅助函数来简化对必选请求数据的访问,从而避免在每个路由处理程序中手动检查缺失值:

  • ApplicationCall.requireQueryParameter() — 从请求 URL 中检索所需的查询形参。如果该形参缺失,则抛出异常。
  • ApplicationCall.requireHeader() — 检索所需的 HTTP 标头值。如果请求中不存在该标头,则抛出异常。
  • ApplicationCall.requireCookie() — 检索所需的 Cookie 值,(可选)使用指定的编码进行解码。如果 Cookie 缺失,则抛出异常。
  • RoutingCall.requirePathParameter() — 从路由定义中检索所需的路径形参。如果匹配的路由中不存在该形参,则抛出异常。

每个函数都会返回一个非空值,或者在值缺失时抛出 MissingRequestParameterException

kotlin
post("/checkout/{cartId}") {
    val userId = call.requireCookie("userId")
    val cartId = call.requirePathParameter("cartId")
    val amount = call.requireQueryParameter("amount").toLong()

    // 业务逻辑
}

正文内容

本节介绍如何接收随 POSTPUTPATCH 发送的正文内容。

原始载荷

要访问原始正文载荷并手动进行解析,请使用 ApplicationCall.receive() 函数,该函数接受要接收的载荷类型。假设您有以下 HTTP 请求:

HTTP
POST http://localhost:8080/text
Content-Type: text/plain

Hello, world!

您可以通过以下方式之一将此请求的正文作为指定类型的对象接收:

  • String

    要将请求正文作为 String 值接收,请使用 call.receive<String>()。您也可以使用 .receiveText() 来实现相同的结果:

    kotlin
    post("/text") {
        val text = call.receiveText()
        call.respondText(text)
    }
  • ByteArray

    要将请求正文作为字节数组接收,请调用 call.receive<ByteArray>()

    kotlin
            post("/bytes") {
                val bytes = call.receive<ByteArray>()
                call.respond(String(bytes))
            }
  • ByteReadChannel

    您可以使用 call.receive<ByteReadChannel>().receiveChannel() 来接收 ByteReadChannel,它支持异步读取字节序列:

    kotlin
    post("/channel") {
        val readChannel = call.receiveChannel()
        val text = readChannel.readRemaining().readString()
        call.respondText(text)
    }

    下面的示例展示了如何使用 ByteReadChannel 上传文件:

    kotlin
    post("/upload") {
        val file = File("uploads/ktor_logo.png")
        call.receiveChannel().copyAndClose(file.writeChannel())
        call.respondText("A file is uploaded")
    }

有关在 Ktor 通道与 RawSinkRawSourceOutputStream 等类型之间进行转换的信息,请参阅 I/O 互操作性

有关完整示例,请参阅 post-raw-data

对象

Ktor 提供了 ContentNegotiation 插件来协商请求的媒体类型并将内容反序列化为所需类型的对象。

要为请求接收并转换内容,请调用 ApplicationCall.receive() 函数,该函数接受一个数据类作为形参:

kotlin
post("/customer") {
    val customer = call.receive<Customer>()
    customerStorage.add(customer)
    call.respondText("Customer stored correctly", status = HttpStatusCode.Created)
}

欲了解更多信息,请参阅 Ktor Server 中的内容协商与序列化

表单形参

Ktor 允许您使用 receiveParameters 函数接收通过 x-www-form-urlencodedmultipart/form-data 类型发送的表单形参。下面的示例展示了一个 HTTP 客户端 POST 请求,其中表单形参在正文中传递:

HTTP
POST http://localhost:8080/signup
Content-Type: application/x-www-form-urlencoded

username=JetBrains&[email protected]&password=foobar&confirmation=foobar

您可以按如下方式在代码中获取形参值:

kotlin
post("/signup") {
    val formParameters = call.receiveParameters()
    val username = formParameters["username"].toString()
    call.respondText("The '$username' account is created")
}

有关完整示例,请参阅 post-form-parameters

多部分表单数据

要接收作为多部分请求的一部分发送的文件,请调用 .receiveMultipart() 函数,然后根据需要循环处理每个部分。

多部分请求数据是按顺序处理的,因此您无法直接访问其特定部分。此外,这些请求可能包含不同类型的部分,例如表单字段、文件或二进制数据,需要以不同方式进行处理。

该示例演示了如何接收文件并将其保存到文件系统:

kotlin
import io.ktor.server.application.*
import io.ktor.http.content.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.cio.*
import io.ktor.utils.io.*
import java.io.File

fun Application.main() {
    routing {
        post("/upload") {
            var fileDescription = ""
            var fileName = ""
            val multipartData = call.receiveMultipart(formFieldLimit = 1024 * 1024 * 100)

            multipartData.forEachPart { part ->
                when (part) {
                    is PartData.FormItem -> {
                        fileDescription = part.value
                    }

                    is PartData.FileItem -> {
                        fileName = part.originalFileName as String
                        val file = File("uploads/$fileName")
                        part.provider().copyAndClose(file.writeChannel())
                    }

                    else -> {}
                }
                part.dispose()
            }

            call.respondText("$fileDescription is uploaded to 'uploads/$fileName'")
        }
    }
}

默认文件大小限制

默认情况下,可以接收的二进制项和文件项的允许大小限制为 50 MiB。如果接收到的文件或二进制项超过 50 MiB 限制,则会抛出 IOException

要替代默认的表单字段限制,请在调用 .receiveMultipart() 时传递 formFieldLimit 形参:

kotlin
val multipartData = call.receiveMultipart(formFieldLimit = 1024 * 1024 * 100)

在此示例中,新限制设置为 100 MiB。

表单字段

PartData.FormItem 代表表单字段,其值可以通过 value 属性访问:

kotlin
when (part) {
    is PartData.FormItem -> {
        fileDescription = part.value
    }
}

文件上传

PartData.FileItem 代表文件项。您可以将文件上传作为字节流进行处理:

kotlin
when (part) {
    is PartData.FileItem -> {
        fileName = part.originalFileName as String
        val file = File("uploads/$fileName")
        part.provider().copyAndClose(file.writeChannel())
    }
}

.provider() 函数返回一个 ByteReadChannel,允许您增量读取数据。使用 .copyAndClose() 函数,您可以将文件内容写入指定目的地,同时确保正确的资源清理。

要确定上传的文件大小,您可以在 post 处理程序内部获取 Content-Length 标头值

kotlin
post("/upload") {
    val contentLength = call.request.header(HttpHeaders.ContentLength)
    // ...
}

资源清理

一旦表单处理完成,每个部分都会使用 .dispose() 函数进行处置以释放资源。

kotlin
part.dispose()

要了解如何运行此示例,请参阅 upload-file