第4章:异步¶
在本章中,您将了解异步和非阻塞架构。 您会发现Vapor
对这些架构的处理方法以及如何使用它。 最后,本章简要概述了Vapor
使用的核心技术SwiftNIO
。
Async
¶
Vapor
最重要的特性之一是Async
。它也可能是最令人困惑的问题之一。它为什么如此重要?
考虑这样一种情况,您的服务器只有一个线程和四个客户端请求,顺序如下:
- 股票报价请求。这会导致调用另一台服务器上的
API
。 - 请求静态
CSS
样式表。CSS
无需查找即可立即使用。 - 请求用户资料。 配置文件必须从数据库中获取。
- 对一些静态
HTML
的请求。无需查找即可立即获得
HTML`。
在同步服务器中,服务器的唯一线程会阻塞,直到返回股票报价。 然后它返回股票报价和CSS
样式表。 当数据库获取完成时,它再次阻塞。 只有这样,在发送用户配置文件后,服务器才会将静态HTML
返回给客户端。
另一方面,在异步服务器中,线程启动获取股票报价的调用并将请求搁置直到完成。 然后它返回CSS
样式表,启动数据库获取并返回静态HTML
。 当被搁置的请求完成时,线程恢复对它们的处理并将它们的结果返回给客户端。
“但是,等等!”,你说,“服务器有多个线程。” 你是对的。但是,服务器可以拥有的线程数是有限制的。 创建线程使用资源。 在线程之间切换上下文代价高昂,确保所有数据访问都是线程安全的既费时又容易出错。 因此,尝试仅通过添加线程来解决问题是一种糟糕、低效的解决方案。
Futures
和promises
¶
为了在等待响应时“搁置”请求,您必须将其包装在promise
中,以便在收到响应时继续处理它。
实际上,这意味着您必须更改可以搁置的方法的返回类型。 在同步环境中,您可能有一个方法:
func getAllUsers() -> [User] {
// do some database queries
}
在异步环境中,这将不起作用,因为在getAllUsers()
必须返回时您的数据库调用可能尚未完成。 你知道你将来可以返回[User]
但现在不能这样做。 在Vapor
中,您返回包含在EventLoopFuture
中的结果。 这是SwiftNIO
的EventLoop
特有的未来。 您将编写如下所示的方法:
func getAllUsers() -> EventLoopFuture<[User]> {
// do some database queries
}
返回 EventLoopFuture<[User]>
允许您向方法的调用者返回一些东西,即使此时可能没有任何东西可以返回。 但是调用者知道该方法会在将来的某个时间点返回 [User]
。 您将在本章末了解更多关于SwiftNIO
的知识。
使用Futures
¶
使用 EventLoopFuture
一开始可能会让人感到困惑,但由于Vapor
广泛使用它们,它们很快就会成为第二天性。 在大多数情况下,当您从某个方法接收到EventLoopFuture
时,您希望对EventLoopFuture
中的实际结果进行处理。 由于该方法的结果尚未实际返回,因此您提供一个回调以在EventLoopFuture
完成时执行。
在上面的示例中,当您的程序到达getAllUsers()
时,它会在EventLoop
上发出数据库请求。 一个EventLoop
进程工作,简单来说可以被认为是一个线程。getAllUsers()
不会立即返回实际数据,而是返回一个EventLoopFuture
。这意味着EventLoop
会暂停该代码的执行,并处理在该EventLoop
上排队的任何其他代码。例如,这可能是您代码的另一部分,其中返回了不同的EventLoopFuture
结果。一旦数据库调用返回,EventLoop
就会执行回调。
如果回调调用另一个返回EventLoopFuture
的方法,则您在原始回调中提供另一个回调以在第二个EventLoopFuture
完成时执行。 这就是为什么您最终会链接或嵌套许多不同的回调。 这是使用期货的困难部分。 异步方法需要彻底转变对代码的思考方式。
解决futures
¶
Vapor
提供了许多方便的方法来处理futures
,以避免直接处理它们的必要性。但是,在许多情况下,您必须等待未来的结果。为了演示,假设您有一个返回HTTP
状态代码204 No Content
的路由。该路由使用类似于上述方法的方法从数据库中获取用户列表,并在返回之前修改列表中的第一个用户。
为了使用对数据库调用的结果,您必须提供一个闭包,以便在EventLoopFuture
解析后执行。您将使用两种主要方法来执行此操作:
flatMap(_:)
:在future
执行并返回另一个future
。 回调接收已解析的未来并返回另一个EventLoopFuture
。map(_:)
:在future
执行并返回另一个future
。 回调接收已解析的future
并返回不同于EventLoopFuture
的类型,然后map(_:)
将其包装在EventLoopFuture
中。
这两种选择都采用未来并产生不同的EventLoopFuture
,通常是不同的类型。重申一下,不同之处在于,如果处理EventLoopFuture
结果的回调返回一个EventLoopFuture
,请使用flatMap(_:)
。如果回调返回的类型不是EventLoopFuture
,请使用map(_:)
。
例如:
// 1
return database.getAllUsers().flatMap { users in
// 2
let user = users[0]
user.name = "Bob"
// 3
return user.save(on: req.db).map { user in
//4
return .noContent
}
}
这是它的作用:
- 从数据库中获取所有用户。正如您在上面看到的,
getAllUsers()
返回EventLoopFuture<[User]>
。 由于完成此EventLoopFuture
的结果是另一个EventLoopFuture
(参见步骤 3),请使用flatMap(_:)
来解析结果。flatMap(_:)
的闭包接收完整的未来users
——数据库中所有用户的数组,类型为[User]
——作为它的参数。 这个.flatMap(_:)
返回EventLoopFuture<HTTPStatus>
。 - 更新第一个用户的名字。
- 将更新的用户保存到数据库中。这将返回
EventLoopFuture<User>
,但您需要返回的HTTPStatus
值还不是EventLoopFuture
,因此请使用map(_:)
。 - 返回适当的
HTTPStatus
值。
如您所见,对于顶级promise
,您使用 flatMap(_:)
,因为您提供的闭包返回一个 EventLoopFuture
。 返回非future HTTPStatus
的内在承诺使用 map(_:)
。
转换¶
有时你不关心future
的结果,只关心它成功完成。在上面的示例中,您没有使用save(on:)
的解析结果,而是返回了不同的类型。 对于这种情况,您可以使用transform(to:)
来简化第3步:
return database.getAllUsers().flatMap { users in
let user = users[0]
user.name = "Bob"
return user
.save(on: req.db)
.transform(to: HTTPStatus.noContent)
}
这有助于减少嵌套的数量,可以使你的代码更容易阅读和维护。你会在本书中看到这个用法。
扁平化¶
有的时候,你必须等待一些期货的完成。一个例子是当你在数据库中保存多个模型时。在这种情况下,你使用flatten(on:)
。例如:
static func save(_ users: [User], request: Request)
-> EventLoopFuture<HTTPStatus> {
// 1
var userSaveResults: [EventLoopFuture<User>] = []
// 2
for user in users {
userSaveResults.append(user.save(on: request.db))
}
// 3
return userSaveResults
.flatten(on: request.eventLoop)
.map { savedUsers in
// 4
for user in savedUser {
print("Saved \(user.username)")
}
// 5
return .created
}
}
在这个代码中,你:
- 定义一个
EventLoopFuture<User>
数组,即步骤2中save(on:)
的返回类型。 - 循环浏览
users
中的每个用户,将user.save(on:)
的返回值追加到数组中。 - 使用
flatten(on:)
来等待所有期货的完成。这需要一个EventLoop
,基本上是实际执行工作的线程。这通常是从Vapor
的Request
中获取的,但你将在后面学习这个。如果需要的话,flatten(on:)
的闭包需要返回的集合作为参数。 - 循环浏览每个被保存的用户并打印出他们的用户名。
- 返回一个
201 Created
的状态。
flatten(on:)
等待所有期货的返回,因为它们是由同一个EventLoop
异步执行。
多个futures
¶
偶尔,你需要等待一些不同类型的期货,这些期货并不相互依赖。例如,在从数据库中检索用户并向外部API
发出请求时,你可能会遇到这种情况。SwiftNIO
提供了许多方法,允许一起等待不同的期货。这有助于避免深度嵌套的代码或混乱的链路。
如果你有两个未来--从数据库中获取所有用户并从外部API
中获取一些信息--你可以像这样使用and(_:)
:
// 1
getAllUsers()
// 2
.and(req.client.get("http://localhost:8080/getUserData"))
// 3
.flatMap { users, response in
// 4
users[0].addData(response).transform(to: .noContent)
}
下面是这个的作用:
- 调用
getAllUsers()
以获得第一个future
的结果。 - 使用
and(_:)
将第二个future
连接到第一个future
。 - 使用
flatMap(_:)
来等待future
的返回。该闭包将期货的解析结果作为参数。 - 调用
addData(_:)
,它返回一些future
的结果,并将返回的结果转化为.noContent
。
如果闭包返回一个非future
的结果,你可以在连锁的期货上使用map(_:)
代替:
// 1
getAllUsers()
// 2
.and(req.client.get("http://localhost:8080/getUserData"))
// 3
.map { users, response in
// 4
users[0].syncAddData(response)
// 5
return .content
}
下面是这个的作用:
- 调用
getAllUsers()
以获得第一个future
的结果。 - 使用
and(_:)
将第二个未来future
到第一个future
。 - 使用
map(_:)
来等待future
的返回。该闭包将future
的解析结果作为参数。 - 调用同步的
syncAddData(_:)
。 - 返回
.noContent
。
Note
你可以根据需要用and(_:)
把许多future
链在一起,但是flatMap
或map
闭包以图元形式返回已解决的future
。例如,对于三个期货:
getAllUsers()
.and(getAllAcronyms())
.and(getAllCategories()).flatMap { result in
// Use the different futures
}
result
是(([User], [Acronyms]), [Categories])
的类型。你用and(_:)
串联的future
越多,你得到的嵌套图元就越多。这可能会让人有点困惑! :]
创建future
¶
有时你需要创建你自己的future
。如果一个if
语句返回一个非future
,而else
块返回一个EventLoopFuture
,编译器会抱怨说这些必须是相同的类型。为了解决这个问题,你必须使用request.eventLoop.future(_:)
将非future
语句转换成EventLoopFuture
。比如说:
// 1
func createTrackingSession(for request: Request)
-> EventLoopFuture<TrackingSession> {
return request.makeNewSession()
}
// 2
func getTrackingSession(for request: Request)
-> EventLoopFuture<TrackingSession> {
// 3
let session: TrackingSession? =
TrackingSession(id: request.getKey())
// 4
guard let createdSession = session else {
return createTrackingSession(for: request)
}
// 5
return request.eventLoop.future(createdSession)
}
下面是这个的作用:
- 定义一个方法,从请求中创建一个
TrackingSession
。这将返回EventLoopFuture<TrackingSession>
。 - 定义一个方法,从请求中获取一个跟踪会话。
- 尝试使用请求的
key
来创建一个跟踪会话。如果不能创建跟踪会话,则返回nil
。 - 确保会话被成功创建,否则创建一个新的跟踪会话。
- 使用
request.eventLoop.future(_:)
从createdSession
创建一个EventLoopFuture<TrackingSession>
。这将返回请求的EventLoop
上的未来。
由于createTrackingSession(for:)
返回EventLoopFuture<TrackingSession>
,你必须使用request.eventLoop.future(_:)
将createdSession
变成EventLoopFuture<TrackingSession>
以使编译器满意。
处理错误¶
Vapor
在整个框架中大量使用了Swift
的错误处理。许多方法要么throw
,要么返回一个失败的未来,允许你在不同层次上处理错误。你可以选择在你的路由处理程序中处理错误,或通过使用中间件在更高层次上捕捉错误,或两者兼而有之。你还需要处理你提供给flatMap(_:)
和map(_:)
的回调里面抛出的错误。
处理回调中的错误¶
map(_:)
和flatMap(_:)
的回调都是不抛出的。如果你在闭包内调用一个抛出的方法,这就会产生问题。当用一个需要抛出的闭包返回一个非未来类型时,map(_:)
有一个抛出的变体,叫做flatMapThrowing(_:)
,令人困惑。说白了,flatMapThrowing(_:)
的回调会返回一个非future
类型。
比如说:
// 1
req.client.get("http://localhost:8080/users")
.flatMapThrowing { response in
// 2
let users = try response.content.decode([User].self)
// 3
return users[0]
}
下面是这个例子的作用:
- 向一个外部
API
发出请求,该API
返回EventLoopFuture<Response>
。你使用flatMapThrowing(_:)
来为未来提供一个回调,可以抛出一个错误。 - 将响应解码为
[User]
。这可以抛出一个错误,flatMapThrowing
将其转换为一个失败的future
。 - 返回第一个用户--一个非
future
类型。
在回调中返回一个未来类型时,情况就不同了。考虑一下你需要解码一个响应,然后返回一个future
的情况:
// 1
req.client.get("http://localhost:8080/users/1")
.flatMap { response in
do {
// 2
let user = try response.content.decode(User.self)
// 3
return user.save(on: req.db)
} catch {
// 4
return req.eventLoop.makeFailedFuture(error)
}
}
以下是正在发生的事情:
- 从外部
API
获得一个用户。由于闭包将返回一个EventLoopFuture
,所以使用flatMap(_:)
。 -
从响应中对用户进行解码。由于这将抛出一个错误,所以用
do
/catch
来捕捉这个错误。 -
保存用户并返回
EventLoopFuture
。 - 如果发生错误,捕捉错误。在
EventLoop
上返回一个失败的未来。
由于flatMap(_:)
的回调不能抛出,你必须捕捉错误并返回一个失败的未来。API
是这样设计的,因为返回既能同步抛出又能异步抛出的东西会让人困惑。
处理未来的错误¶
在异步世界中,处理错误是有点不同的。你不能使用Swift
的do
/catch
,因为你不知道承诺何时会执行。SwiftNIO
提供了一些方法来帮助处理这些情况。在基本层面上,你可以将whenFailure(_:)
链接到你的未来:
let futureResult = user.save(on: req)
futureResult.map { user in
print("User was saved")
}.whenFailure { error in
print("There was an error saving the user: \(error)")
}
如果save(on:)
成功,.map
块会以future
的解析值为参数执行。如果future
失败了,它将执行.whenFailure
块,传入Error
。
在Vapor
中,当处理请求时,你必须返回一些东西,即使它是一个future
。使用上面的map
/whenFailure
方法并不能阻止错误的发生,但它可以让你看到错误是什么。如果save(on:)
失败了,你返回futureResult
,失败仍然会在链上传播。然而,在大多数情况下,你想尝试纠正这个问题。
SwiftNIO
提供了flatMapError(_:)
和flatMapErrorThrowing(_:)
来处理这种类型的失败。这允许你处理该错误,要么修复它,要么抛出一个不同的错误。比如说:
// 1
return saveUser(on: req.db)
.flatMapErrorThrowing { error -> User in
// 2
print("Error saving the user: \(error)")
// 3
return User(name: "Default User")
}
下面是这个的作用:
- 试图保存用户。使用
flatMapErrorThrowing(_:)
来处理错误,如果有错误发生的话。该闭包将错误作为参数,并且必须返回已解决的未来的类型--在这里是User
。 - 记录收到的错误。
- 创建一个默认的用户来返回。
Vapor
还提供了相关的flatMapError(_:)
,当相关的闭包返回一个未来时:
return user.save(on: req).flatMapError {
error -> EventLoopFuture<User> in
print("Error saving the user: \(error)")
return User(name: "Default User").save(on: req)
}
由于save(on:)
返回一个未来,你必须调用flatMapError(_:)
来代替。
Note
flatMapError(_:)
的闭包不能抛出错误 - 你必须捕获错误并返回一个新的失败的未来,与上面的flatMap(_:)
类似。
flatMapError
和flatMapErrorThrowing
只在失败时执行它们的关闭。但是如果你想同时处理错误和处理成功的情况呢?很简单! 只需链到相应的方法即可!
链式future
¶
处理期货问题有时会让人不知所措。这很容易导致代码被嵌套到多个层次。
Vapor
允许你将期货链在一起,而不是嵌套它们。例如,考虑一个像下面这样的片段:
return database
.getAllUsers()
.flatMap { users in
let user = users[0]
user.name = "Bob"
return user.save(on: req.db)
.map { user in
return .noContent
}
}
map(_:)
和flatMap(_:)
可以链在一起以避免像这样的嵌套:
return database
.getAllUsers()
// 1
.flatMap { users in
let user = users[0]
user.name = "Bob"
return user.save(on: req.db)
// 2
}.map { user in
return .noContent
}
改变flatMap(_:)
的返回类型,允许你连锁map(_:)
,它接收EventLoopFuture<User>
。最后的map(_:)
会返回你最初返回的类型。链式期货允许你减少代码中的嵌套,并可能使其更容易推理,这在异步世界中特别有用。然而,是嵌套还是链式,完全是个人的偏好。
Always
¶
有时,无论future
的结果如何,你都想执行一些东西。你可能需要关闭连接,触发一个通知,或者只是记录未来的执行。为此,使用always
回调。
比如说:
// 1
let userResult: EventLoopFuture<User> = user.save(on: req.db)
// 2
userResult.always {
// 3
print("User save has been attempted")
}
下面是这个的作用:
- 保存一个用户并将结果保存在
userResult
中。这是EventLoopFuture<User>
的类型。 - 将一个
always
链接到结果中。 - 当应用程序执行未来时,打印一个字符串。
无论未来的结果如何,不管是失败还是成功,always
闭包都会被执行。它对future
也没有影响。你也可以将其与其他链结合起来。
Waiting
¶
在某些情况下,你可能想实际等待结果的返回。要做到这一点,请使用wait()
。
Note
这方面有一个很大的注意事项。你不能在主事件循环中使用wait()
,这意味着所有的请求处理程序和大多数其他情况。
然而,正如你将在第11章"测试"中看到的,这在测试中特别有用,因为编写异步测试很困难。比如说:
let savedUser = try user.save(on: database).wait()
savedUser
不是一个EventLoopFuture<User>
,因为你使用wait()
,savedUser
是一个User
对象。请注意wait()
在执行承诺失败时抛出一个错误。值得重申的是。这只能在主事件循环之外使用!
SwiftNIO
¶
Vapor
是建立在苹果的[SwiftNIO库](https://github.com/apple/swift-nio)之上的。SwiftNIO
是一个跨平台的异步网络库,就像Java
的Netty
。它是开源的,就像Swift
本身一样。
SwiftNIO
为Vapor
处理所有的HTTP
通信。它是允许Vapor
接收请求和发送响应的管道。SwiftNIO
管理连接和数据的传输。
它还为你的future
管理所有的EventLoop
,这些期货执行工作并执行你的承诺。每个EventLoop
都有自己的线程。
Vapor
管理所有与NIO
的交互,并提供一个干净的、Swifty
的API
来使用。Vapor
负责服务器的高层方面,如路由请求。它提供了建立伟大的服务器端Swift
应用程序的功能。SwiftNIO
提供了一个坚实的基础,可以在此基础上发展。
接下来去哪?¶
虽然没有必要了解关于EventLoopFuture
和EventLoop
如何在引擎盖下工作的所有细节,但你可以在Vapor的API文档或SwiftNIO的API文档中找到更多信息。Vapor
的文档网站也有一个关于异步和future
的大章节。