阿特我自己
[email protected]
Hello WvT
Ktor 服务端
Ktor 服务端

1. 以 Gradle 创建工程

添加 jcenter 仓库

添加 io.ktor:ktor-server-core 依赖, 这是 Ktor 的核心库

选择运行引擎, 如果使用 Netty 引擎, 则添加 io.ktor:ktor-server-netty 依赖

为了使用日志系统, 需要添加 ch.qos.logback:logback-classic 依赖

2. 配置应用

要启动一个服务器, 最简单的方式是在 main 函数中使用 embeddedServer 函数获取一个 ApplicationEngine, 并调用其 start() 方法启动一个新的嵌入式服务器

通常使用如下重载形式, 只需要简单地指定引擎, module 是一个带接收者的函数, 如果使用 lambda, 那么表达式内会将 Application 的实例当作 this, 这有点像接受一个 Application 的扩展函数
事实上, module 参数既可以传入一个 Application 的扩展函数引用, 也可以传入一个首参数为 Application 类型的函数引用
当 embeddedServer 调用时, 会提供一个 Application 的实例给 module 引用的函数

为了使程序更加模块化, 如果不想在 main 函数中手动调用 embeddedServer 函数, 可以把服务器的配置抽取成一个配置文件

首先, 在 Gradle 安装 application 插件

使用 application 插件配置引擎, 首选 netty

接下来在 src/main/resources 文件夹内创建 application.conf 文件, 内容如下, 这很像 Kotlin DSL 的语法

modules 可以指定多个模组, 一个模组相当于 embeddedServer 函数的 module 参数, 所引用的函数推荐写成 Application 的扩展函数

最后, 将主函数删除, 通过 Gradle 的 run 任务即可启动应用

3. 应用

一个 Application 实例是一个 Ktor 应用的主单元. 当接收到请求时, 该请求转换为 ApplicationCall 并经过 Application 持有的管道(PipeLine), 事实上 Application 就继承自 PipeLine, 管道由一个或多个预先安装的拦截器组成, 这些拦截器通常提供某些功能, 例如路由, 压缩等, 最终将处理请求

通常, Ktor 程序通过安装和配置 功能模块(Features) 来部署 Application 管道

功能 (Features) 是可以为管道而安装和配置的单例 (通常是一个伴生对象). Ktor 包含了一些标准功能, 添加第三方的功能. 你可以在任何管道中安装功能, 例如 Application 本身, 或特定的路由 (Route)

通过在管道中调用 install(feature, configure) 扩展函数, 你可以安装和配置功能

通常情况下, 会安装 DefaultHeaders, CallLogginRouting 功能

在这里查看更多功能

4. 拦截器与路由

拦截器

为了能够拦截请求, 需要在管道中添加 拦截器

管道阶段 (PipelinePhase)

拦截器有多个阶段, 用来控制拦截器的执行顺序

PipelinePhase 代表一个管道阶段, 使用构造器创建并指定阶段的名称即可

在使用 阶段 设置拦截器前, 需要对阶段进行注册, 拦截器将按照阶段的注册顺序执行, 同阶段的拦截器将按照其在管道中的定义顺序执行

PipeLine 有如下几个方法用于注册阶段

  • addPhase(phase): 添加(注册)一个阶段
  • insertPhaseAfter(reference, phase): 将 phase 阶段添加到 reference 阶段后
  • insertPhaseBefore(reference, phase): 将 phase 阶段添加到 reference 阶段前

设置拦截器

接下来, 可以在管道中设置指定阶段的拦截器

使用 PipeLine 的 intercept(phase, block) 方法设置拦截器
block 参数是一个接收者为 PipelineContext 的函数/lambda, 该函数用于处理所需的内容

PipelineContext 的 context 属性通常是一个 ApplicationCall, 通过 ApplicationCall 即可处理请求
它还有一个 call 扩展属性, 其 getter 直接返回 context 属性, 事实上就是 context 属性的捷径

拦截器可以通过以下方式控制流

  • 抛出异常: 异常会传播回来, 并且管道被关闭
  • 调用 proceed 或 proceedWith 方法: 拦截器被挂起, 管道的剩余部分被执行. 完成后将恢复功能, 并执行 proceed/proceedWith 代码块
  • 调用 finish() 函数: 管道结束, 没有任何异常, 也没有执行管道的剩余部分
  • 其他情况: 调用下一个函数, 或者如果它是最后一个函数, 则管道结束

Subject

在执行期间, PipelineContext 还包含一个 subject 属性

TODO

合并

相同类型的管道可以合并

TODO

路由

路由 (Routing) 是一个安装到 Application 中的功能, 用于简化和构建页面请求处理

get, post, put, delete, head 和 options 函数都是达成灵活和强大的路由系统的捷径

特别地, get 是 route(HttpMethod.Get, path) { handle(body) } 的捷径, body 是传给 get 函数的 lambda 表达式

路由树

路由树是一个使用 DSL 以嵌套方式构建的树, 能够处理复杂的规则及处理请求

路由的构建系统有如下 dsl

  • route(path) 添加路径段匹配器
  • method(verb) 添加 HTTP 方法匹配器
  • param(name, value) – 添加查询指定参数指定值的匹配器
  • param(name) – 添加捕获指定参数的值(如果存在)的匹配器
  • optinalParam(name) – 添加匹配器以捕获查询参数的值(如果存在)
  • header(name, value) – 添加用于 HTTP 标头特定值的匹配器

handle() 函数用于在 Route 中安装一个处理器, 用于处理请求

get, post, delete 等 HTTP 动词用于匹配指定的 HTTP 请求, 它们都是 route(HttpMethod.{verb}, path) { handle(body) } 的捷径

路径

手动以路径段构建路由将非常不便, 因此 route 函数可以使用完整路径进行路由

例如, 以下这两个变体是等效的

参数

路径中还可以包含 “{name}” 形式用于匹配路径参数, 接下来在 ApplicationCall 中即可通过 parameters 属性捕获该参数

如上代码, 当访问 /login/1234567 时, parameters[“user”] 会返回 “1234567”
并且, /login 路径是不可访问的

参数并不是只能单独作为路径段的, 还可以插入到路径段中, 例如 /fuck{anyone} 可以匹配 /fuckyou, /fuckme

通配符

参数和路径段都是可选的, 也可以捕获 URI 的剩余部分

  • {param?} – 可选路径段, 如果存在, 则在捕获到参数中
  • * – 通配符, 捕获任何路径段, but shouldn’t be missing
  • {…} – 尾部通配符, 匹配 URI 的所有剩余部分, 应处于最后, 可空
  • {param…} – 匹配 URI 的所有剩余部分, 并将每个路径段值放入 paramters, 使用指定的 param 作为 key, 使用 call.parameters.getAll(“param”) 以获取所有值

示例

优先级

多个路由有可能匹配到同一个 HTTP 请求

TODO

拦截

TODO

可扩展性

ktor-server-core 模块已经包含了许多基本选择器, 如果想添加自己的选择器, 查看 RouteSelector 相关信息

跟踪路由方案

如果想跟踪路由方案, Ktor 在路由功能中提供了一个 trace 方法

这样, 当接收到请求时, 将会在日志中打印路由信息

5. 处理请求

当使用路由或直接拦截管道时, 会接收到一个 ApplicationCall 的实例: call

ApplicationCall 提供了两个主要的属性: ApplicationRequestApplicationResponse, 分别对应请求和响应

1. 接受请求

ApplicationCall 定义了一个 request 的属性, 它是一个 ApplicationRequest 的实例, 包含了请求的一些信息

request 内部也持有对其上下文的引用, 例如

2. 获取请求信息

request 提供了如下等属性用于获取请求的信息

  • version: String — HTTP 版本
  • httpMethod: HttpMethod — HTTP 请求方式
  • port: Int — 请求的端口
  • host: String — 本地主机, 不带端口
  • uri: String — origin.uri 的捷径

反向代理支持: origin 和 local

当我们使用了一个反向代理服务(例如 Nginx)时, 接收到的请求不一定是由终端用户执行的, 而是由反向代理服务执行的, 这意味着客户端的 IP 地址将会被代理服务器替换, 同时, 反代可能会通过 HTTPS 服务, 却通过 HTTP 来请求你的服务器, 主流的反代会发送 X-Forwarded- 头以便你访问这个信息

request 对象有 localorigin 两个属性, 允许你获取 original 请求或 local/proxied 请求的信息

为使这能够在反向代理服务器下工作, 你必须安装 XForwardedHeaderSupport 功能

以下是你能够从 RequestConnectionPoint 接口中获得的信息

  • scheme: String // “http” or “https”: The provided protocol (local) or X-Forwarded-Proto
  • version: String // “HTTP/1.1”
  • port: Int
  • host: String // The provided host (local) or X-Forwarded-Host
  • uri: String
  • method: HttpMethod
  • remoteHost: String // The client IP (the direct ip for local, or the redirected one X-Forwarded-For)

3. 处理 GET 请求

如果你想以集合的形式访问 ?param1=value&param2=value 一类的参数, 你可以使用 request 对象的 queryParameters 属性

这是一个 Parameters 对象, 它实现了 StringValues 接口, 每个 key 都有与之对应的字符串列表

你也可以访问原始的字符串, 使用 queryString() 方法

4. 处理 POST, PUT 和 PATCH 请求

POST PUT PATCH 请求通常都有已编码的请求正文

原始 Payload

要访问原始的请求正文, 可以使用 receiveChannel, 它是 call 的一部分, 而不是 request

call 还为其他的常用类型提供了便捷的方法

这些方法都是 call.receive<T> 具有指定类型的别名.

ByteReadChannel, ByteArray, InputStream, MultiPartData, StringParameters 都由 ApplicationReceivePipeline.installDefaultTransformations 处理, 并且是默认安装的

接收参数

使用 receiveParameters() 或 receive<Parameters> 可以解析表单参数

接收指定类型的对象, Content-Type, JSON

call 支持 receive<T>() 和 receiveOrNull<T>() 两个通用对象

为了能够从 Payload 中接收自定义对象, 你可以使用 ContentNegotiantion 功能

如上代码就在 ContentNegotiantion 功能中安装了 gson 模块

注意: 如果想要使用 Gson, 需要添加 io.ktor:ktor-gson 依赖

以下是接收 Gson 对象的示例

Multipart, Files 和 Uploads

查看 Uploads 文章

自定义接收转换器

你可以通过调用 application.receivePipeline.intercept(ApplicationReceivePipeline.Transform) { query -> 创建自定义转换器, 并调用 proceedWith(ApplicationReceiveRequest(query.type, transformed) 来完成 ContentNegotiation 功能

5. Cookies

ApplicationRequest 有一个 cookies 属性, 它是一个 RequestCookies 实例, 用于访问客户端发送的 Cookie

RequestCookies 重载了操作符, 你可以像集合一样访问它, 例如 request.cookies[“mycookie”]

要想使用 cookies 处理 sessions, 请查看 Sessions 功能

6. Headers

ApplicationRequest 实例还有一个 headers: Headers 属性, 它实现了 StringValues 接口, 每个 key 都可以有一个关联的 String 列表

request 对象有一些便捷的方法用于访问常用 header

  • contentType() : ContentType // Parsed Content-Type
  • contentCharset() : Charset? // Content-Type JVM charset
  • authorization() : String? // Authorization header
  • location() : String? // Location header
  • accept() : String? // Accept header
  • acceptItems() : List<HeaderValue> // Parsed items of Accept header
  • acceptEncoding() : String? // Accept-Encoding header
  • acceptEncodingItems() : List<HeaderValue> // Parsed Accept-Encoding items
  • acceptLanguage() : String? // Accept-Language header
  • acceptLanguageItems() : List<HeaderValue>// Parsed Accept-Language items
  • acceptCharset() : String? // Accept-Charset header
  • acceptCharsetItems() : List<HeaderValue> // Parsed Accept-Charset items
  • userAgent() : String? // User-Agent header
  • cacheControl() : String? // Cache-Control header
  • ranges() : RangesSpecifier? // Parsed Ranges header
  • isChunked() : Boolean // Transfer-Encoding: chunked
  • isMultipart() : Boolean // Content-Type matches Multipart

6. 响应请求

与接收请求类似, call 对象也有一个 response 属性, 它是一个 ApplicationResponse 实例

与 request 类似, 通过 response 对象也可以访问其对应的上下文

1. 控制 HTTP Headers 和状态

HTTP 状态

HttpResponse 有如下方法用于获取和设置 HTTP 状态

  • fun status(): HttpStatusCode?
  • fun status(value: HttpStatusCode)

HttpStatusCode 代表一个 HTTP 状态, 它有一些内置了一些常用的属性, 也可以通过其构造器(Int, String) 创建自定义状态码

Headers

response 的 headers 属性是一个 ResponseHeaders 实例, 它重载了操作符, 可以像使用 map 一样设置 Headers

header() 方法也可以添加一个 Header

HttpHeaders 有一些类型化的常用 Header 字符串

除此之外, HttpResponse 还提供了 contentLength(), contentType(), cacheControl 等便捷的方法用于设置常见 Header

Cookies

cookies: ResponseCookies 是一个便捷的, 属性用于设置响应的 Set-Cookie

ResponseCookies 重载了操作符, 可以像使用数组下标一样设置 Cookie

2. HTTP/2 推送和 HTTP/1 Header

ApplicationCall 支持 推送 功能

  • 在 HTTP/2 中, 它使用 push 功能
  • 在 HTTP/1.2 中, 它将隐式添加 link

注意: 推送将会减少请求时间, 但可能会发送客户端已经缓存的内容

3. 重定向

通过 ApplicationCall.respondRedirect(redirectTo, permanent = false) 函数, 可以简单地生成 301 或 302 重定向

注意: 一旦该函数被执行, 剩余的函数依然会继续执行, 如果你使用了 guard 子句, 你应该手动 return 以停止继续执行. 如果想通过抛出异常来结束流, 查看示例

4. 发送响应内容

有一些 ApplicationCall 的扩展函数, 用于发送响应内容, 这些函数基本以 respond 动词开头

这些函数通常会提供两种接收内容的重载形式, 一种是直接在形参列表内接收内容, 另一种则接收一个 suspend 的 lambda 函数, 后者是 Kotlin 协程中的挂起函数

这些函数通常需要指定 ContentTypeHttpStatusCode, 它们也提供了默认值
ContentType 会影响 Content-Type 头, 也可能会影响 Ktor 如何处理传入的数据

例如, 如果指定为 ContentType.Text.Html, Content-Type 头会变成 text/html
如果指定为 ContentType.Image.Any, Content-Type 头会变成 image/*

此外, 有些重载形式可能需要接收一个接收者为 OutgoingContent 的 lambda 当作配置

OutgoingContent 有如下属性和方法用于 response 的一些配置

  • contentType: ContentType? – 指定该资源的 ContentType
  • contentLength: Long? – 指定该资源的大小, 单位为 byte. 如果为 null, 则该资源将以 “Transfer-Encoding: chunked” 发送
  • status: HttpStatusCode? – 指定 Http 状态码
  • headers: Headers – 指定头
  • fun <T : Any> getProperty(key: AttributeKey<T>): T? = extensionProperties?.getOrNull(key) – 获取该内容的扩展属性
  • fun <T : Any> setProperty(key: AttributeKey<T>, value: T?) – 设置该内容的扩展属性

发送特定内容

Ktor 能够使用一个对象作为响应内容, 这些对象会按照经过用户安装的插件进行转换, 例如, 如果安装了 Gson 功能, 那么对象会被序列化成 Json 数据

  • call.respond(MyClass(“Hello, world!”))
  • call.respond(HttpStatusCode.NotFound, MyClass(“hello”))

发送文本

respondText() 的多种重载形式可以发送文本作为响应内容

文本包括但不限于 Plain, CSS, HTML

发送 ByteArrays

respondBytes() 的多种重载形式可以发送一个 ByteArray 作为二进制数据

通过与 Content-Type 结合, 你可以发送一个媒体资源

发送文件

respondFile() 可以发送一个 File

发送 URL-Encoded 表单

使用 Parameters.formUrlEncode 构建表单响应内容, 查看官方描述

使用 Writer 分块发送内容

使用 respondWrite()

使用 WriteChannelContent 分块发送任意内容

你可以实现 OutgoingContent.WriteChannelContent 抽象类, 实现其 contentType 属性及 writeTo(channel) 方法

接下来将该对象传入 respond() 方法

使用 OutputStream

respondOutputStream() 可以使用 OutputStream 来响应内容, 但不推荐, 因为 OutputStream 可能会阻塞线程

指定默认的 ContentType

call.defaultTextContentType(contentType: ContentType?)

5. 使文件”可被下载”

通过为响应内容设置 Content-Disposition 头, 可以标记该内容为一个可被下载的文件

Content-Disposition 的第一个参数是页面的展示形式
inline默认值, 表示消息体会以页面的一部分或者整个页面的形式展示
attachment 意味着消息体应该被下载到本地

如果第一个参数指定为 attachment, 那么还可以指定一个 filename 参数, 浏览器通常会使用 filename 参数的值作为下载的文件名

可以通过 header() 方法手动添加

但 Ktor 也提供了一个类型化的方式

打包

TODO

SSL

TODO

赞赏

发表评论

textsms
account_circle
email

Hello WvT

Ktor 服务端
1. 以 Gradle 创建工程 添加 jcenter 仓库 添加 io.ktor:ktor-server-core 依赖, 这是 Ktor 的核心库 选择运行引擎, 如果使用 Netty 引擎, 则添加 io.ktor:ktor-server-netty 依赖…
扫描二维码继续阅读
2019-10-21


没有激活的小工具