跳转至

第4章:异步

在本章中,您将了解异步和非阻塞架构。 您会发现Vapor对这些架构的处理方法以及如何使用它。 最后,本章简要概述了Vapor使用的核心技术SwiftNIO

Async

Vapor最重要的特性之一是Async。它也可能是最令人困惑的问题之一。它为什么如此重要?

考虑这样一种情况,您的服务器只有一个线程和四个客户端请求,顺序如下:

  1. 股票报价请求。这会导致调用另一台服务器上的API
  2. 请求静态CSS样式表。CSS无需查找即可立即使用。
  3. 请求用户资料。 配置文件必须从数据库中获取。
  4. 对一些静态HTML的请求。无需查找即可立即获得HTML`。

在同步服务器中,服务器的唯一线程会阻塞,直到返回股票报价。 然后它返回股票报价和CSS样式表。 当数据库获取完成时,它再次阻塞。 只有这样,在发送用户配置文件后,服务器才会将静态HTML返回给客户端。

另一方面,在异步服务器中,线程启动获取股票报价的调用并将请求搁置直到完成。 然后它返回CSS样式表,启动数据库获取并返回静态HTML。 当被搁置的请求完成时,线程恢复对它们的处理并将它们的结果返回给客户端。

img

“但是,等等!”,你说,“服务器有多个线程。” 你是对的。但是,服务器可以拥有的线程数是有限制的。 创建线程使用资源。 在线程之间切换上下文代价高昂,确保所有数据访问都是线程安全的既费时又容易出错。 因此,尝试仅通过添加线程来解决问题是一种糟糕、低效的解决方案。

Futurespromises

为了在等待响应时“搁置”请求,您必须将其包装在promise中,以便在收到响应时继续处理它。

实际上,这意味着您必须更改可以搁置的方法的返回类型。 在同步环境中,您可能有一个方法:

func getAllUsers() -> [User] {
  // do some database queries
}

在异步环境中,这将不起作用,因为在getAllUsers()必须返回时您的数据库调用可能尚未完成。 你知道你将来可以返回[User]但现在不能这样做。 在Vapor中,您返回包含在EventLoopFuture中的结果。 这是SwiftNIOEventLoop特有的未来。 您将编写如下所示的方法:

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
  }
}

这是它的作用:

  1. 从数据库中获取所有用户。正如您在上面看到的,getAllUsers() 返回EventLoopFuture<[User]>。 由于完成此 EventLoopFuture的结果是另一个EventLoopFuture(参见步骤 3),请使用flatMap(_:)来解析结果。 flatMap(_:)的闭包接收完整的未来users——数据库中所有用户的数组,类型为[User]——作为它的参数。 这个 .flatMap(_:)返回EventLoopFuture<HTTPStatus>
  2. 更新第一个用户的名字。
  3. 将更新的用户保存到数据库中。这将返回EventLoopFuture<User>,但您需要返回的HTTPStatus值还不是EventLoopFuture,因此请使用map(_:)
  4. 返回适当的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
    }
}

在这个代码中,你:

  1. 定义一个EventLoopFuture<User>数组,即步骤2中save(on:)的返回类型。
  2. 循环浏览users中的每个用户,将user.save(on:)的返回值追加到数组中。
  3. 使用flatten(on:)来等待所有期货的完成。这需要一个EventLoop,基本上是实际执行工作的线程。这通常是从VaporRequest中获取的,但你将在后面学习这个。如果需要的话,flatten(on:)的闭包需要返回的集合作为参数。
  4. 循环浏览每个被保存的用户并打印出他们的用户名。
  5. 返回一个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)
}

下面是这个的作用:

  1. 调用getAllUsers()以获得第一个future的结果。
  2. 使用and(_:)将第二个future连接到第一个future
  3. 使用flatMap(_:)来等待future的返回。该闭包将期货的解析结果作为参数。
  4. 调用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
}

下面是这个的作用:

  1. 调用getAllUsers()以获得第一个future的结果。
  2. 使用and(_:)将第二个未来future到第一个future
  3. 使用map(_:)来等待future的返回。该闭包将future的解析结果作为参数。
  4. 调用同步的syncAddData(_:)
  5. 返回.noContent

Note

你可以根据需要用and(_:)把许多future链在一起,但是flatMapmap闭包以图元形式返回已解决的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)
}

下面是这个的作用:

  1. 定义一个方法,从请求中创建一个TrackingSession。这将返回EventLoopFuture<TrackingSession>
  2. 定义一个方法,从请求中获取一个跟踪会话。
  3. 尝试使用请求的key来创建一个跟踪会话。如果不能创建跟踪会话,则返回nil
  4. 确保会话被成功创建,否则创建一个新的跟踪会话。
  5. 使用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]
}

下面是这个例子的作用:

  1. 向一个外部API发出请求,该API返回EventLoopFuture<Response>。你使用flatMapThrowing(_:)来为未来提供一个回调,可以抛出一个错误。
  2. 将响应解码为[User]。这可以抛出一个错误,flatMapThrowing将其转换为一个失败的future
  3. 返回第一个用户--一个非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)
  }
}

以下是正在发生的事情:

  1. 从外部API获得一个用户。由于闭包将返回一个EventLoopFuture,所以使用flatMap(_:)
  2. 从响应中对用户进行解码。由于这将抛出一个错误,所以用do/catch来捕捉这个错误。

  3. 保存用户并返回EventLoopFuture

  4. 如果发生错误,捕捉错误。在EventLoop上返回一个失败的未来。

由于flatMap(_:)的回调不能抛出,你必须捕捉错误并返回一个失败的未来。API是这样设计的,因为返回既能同步抛出又能异步抛出的东西会让人困惑。

处理未来的错误

在异步世界中,处理错误是有点不同的。你不能使用Swiftdo/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")
}

下面是这个的作用:

  1. 试图保存用户。使用flatMapErrorThrowing(_:)来处理错误,如果有错误发生的话。该闭包将错误作为参数,并且必须返回已解决的未来的类型--在这里是User
  2. 记录收到的错误。
  3. 创建一个默认的用户来返回。

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(_:)类似。

flatMapErrorflatMapErrorThrowing只在失败时执行它们的关闭。但是如果你想同时处理错误和处理成功的情况呢?很简单! 只需链到相应的方法即可!

链式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")
}

下面是这个的作用:

  1. 保存一个用户并将结果保存在userResult中。这是EventLoopFuture<User>的类型。
  2. 将一个always链接到结果中。
  3. 当应用程序执行未来时,打印一个字符串。

无论未来的结果如何,不管是失败还是成功,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是一个跨平台的异步网络库,就像JavaNetty。它是开源的,就像Swift本身一样。

SwiftNIOVapor处理所有的HTTP通信。它是允许Vapor接收请求和发送响应的管道。SwiftNIO管理连接和数据的传输。

它还为你的future管理所有的EventLoop,这些期货执行工作并执行你的承诺。每个EventLoop都有自己的线程。

Vapor管理所有与NIO的交互,并提供一个干净的、SwiftyAPI来使用。Vapor负责服务器的高层方面,如路由请求。它提供了建立伟大的服务器端Swift应用程序的功能。SwiftNIO提供了一个坚实的基础,可以在此基础上发展。

接下来去哪?

虽然没有必要了解关于EventLoopFutureEventLoop如何在引擎盖下工作的所有细节,但你可以在Vapor的API文档SwiftNIO的API文档中找到更多信息。Vapor的文档网站也有一个关于异步和future大章节