SwiftNIO

概念性概述

SwiftNIO 从根本上来说是一个用 Swift 构建高性能网络应用的低级工具。它特别针对那些使用 thread-per-connection 并发模型导致效率低下或无法维持的场景。在构建使用大量相对低利用率连接的服务器(如 HTTP 服务器)时,这是一个常见的限制。

为了实现其目标,SwiftNIO 广泛使用了 "非阻塞 IO":因此而得名! 非阻塞 IO 不同于更常见的阻塞 IO 模型,因为应用程序不会等待数据被发送出去或从网络接收:相反,SwiftNIO 要求内核通知它何时可以执行 IO 操作,而无需等待。

SwiftNIO 并不像网络框架那样,旨在提供高级解决方案。相反,SwiftNIO 专注于为这些更高级别的应用提供低级别的构件。当涉及到构建一个 Web 应用程序时,大多数用户不会想直接使用 SwiftNIO:相反,他们会想使用 Swift 生态系统中许多优秀的 Web 框架之一。然而,那些 web 框架可能会选择使用 SwiftNIO 来提供它们的网络支持。

下面的章节将描述 SwiftNIO 提供的低级工具,并提供如何使用它们的快速概述。如果您对这些概念感到满意,那么您可以直接跳到本 README 的其他部分。

基本架构

SwiftNIO 的基本组件是以下 8 种类型的对象:

`EventLoopGroup`,一个协议
`EventLoop`,一个协议
`Channel`,一个协议
`ChannelHandler`,一个协议
`Bootstrap`,几个相关的结构体
`ByteBuffer`,一个结构体
`EventLoopFuture`,一个泛型类
`EventLoopPromise`,一个泛型结构体

所有的 SwiftNIO 应用最终都是由这些不同的组件构成的。

EventLoops 和 EventLoopGroups

SwiftNIO 的基本 IO 基元是事件循环。事件循环是一个对象,它等待事件(通常是与 IO 相关的事件,如 "收到数据")发生,然后在事件发生时启动某种回调。在几乎所有的 SwiftNIO 应用中,事件循环的数量都会相对较少:通常每个应用要使用的 CPU 核心只有一个或两个。一般来说,事件循环会在你的应用程序的整个生命周期中运行,在一个无休止的循环中旋转着调度事件。

事件循环被聚集在一起,形成事件循环组。这些组提供了一种机制来分配事件循环周围的工作。例如,当监听入站连接时,监听套接字将被注册在一个事件循环上。然而,我们不希望该监听套接字上接受的所有连接都被注册到同一个事件循环中,因为这有可能使一个事件循环过载,而使其他事件循环空着。出于这个原因,事件循环组提供了在多个事件循环中分散负载的能力。

在今天的 SwiftNIO 中,有一个 EventLoopGroup 的实现,和两个 EventLoop 的实现。对于生产应用来说,有一个 MultiThreadedEventLoopGroup,这个 EventLoopGroup 创建了许多线程(使用 POSIX pthreads 库),并在每个线程上放置一个 SelectableEventLoopSelectableEventLoop 是一个事件循环,它使用一个选择器(根据目标系统的不同,可以是 kqueueepoll )来管理来自文件描述符的 I/O 事件,并调度工作。此外,还有 EmbeddedEventLoop,它是一个假的事件循环,主要用于测试目的。

EventLoops 有许多重要的属性。最重要的是,它们是 SwiftNIO 应用程序中所有工作的完成方式。为了保证线程安全,任何想要在 SwiftNIO 中几乎所有其他对象上完成的工作都必须通过 EventLoop 来调度。EventLoop 对象几乎拥有 SwiftNIO 应用程序中的所有其他对象,了解它们的执行模型对于构建高性能的 SwiftNIO 应用程序至关重要。

Channels, Channel Handlers, Channel Pipelines, and Channel Contexts。

虽然 EventLoops 对 SwiftNIO 的工作方式至关重要,但大多数用户除了要求他们创建 EventLoopPromises 和安排工作之外,不会与它们进行实质性的交互。 SwiftNIO 应用程序中,大多数用户会花最多时间与之交互的部分是 ChannelChannelHandlers。

几乎用户在 SwiftNIO 程序中交互的每一个文件描述符都与一个 Channel 相关联。Channel 拥有这个文件描述符,并负责管理它的生命周期。它还负责处理该文件描述符上的入站和出站事件:每当事件循环有对应于文件描述符的事件时,它就会通知拥有该文件描述符的 Channel

然而,Channel 本身并没有用处。毕竟,很少有应用程序不想对它在套接字上发送或接收的数据做任何事情的! 所以 Channel 的另一个重要部分是ChannelPipeline

ChannelPipeline 是一个对象序列,称为ChannelHandlers,用于处理Channel 上的事件。ChannelHandlers 按顺序一个接一个地处理这些事件,并在处理过程中对事件进行突变和转换。这可以被认为是一个数据处理管道,因此被称为 ChannelPipeline

所有的 ChannelHandlers 都是 InboundOutbound 处理程序,或者两者都是。入站处理程序处理 "入站 "事件:比如从套接字中读取数据,读取套接字关闭,或者其他由远程对等体发起的事件。出站处理程序处理 "出站 "事件,如写入、连接尝试和本地套接字关闭。

每个处理程序按顺序处理这些事件。例如,读事件每次由一个处理程序从管道的前部传递到后部,而写事件则从管道的后部传递到前部。每个处理程序可以在任何时候产生入站或出站事件,这些事件将以任何合适的方向发送到下一个处理程序。这使得处理程序可以拆分读,凝聚写,延迟连接尝试,和执行事件的任意转换。

一般来说,ChannelHandlers 被设计成高度可重复使用的组件。这意味着它们往往被设计得尽可能小,只执行一种特定的数据转换。这允许处理程序以新颖灵活的方式组合在一起,这有助于代码的重用和封装。

ChannelHandler 能够通过使用 ChannelHandlerContext 来跟踪它们在 ChannelPipeline 中的位置。这些对象包含了对管道中上一个和下一个通道处理程序的引用,确保了 ChannelHandler 在保持在管道中时,总是可以发出事件。

SwiftNIO 内置了许多 ChannelHandler,这些 ChannelHandler 提供了有用的功能,例如 HTTP 解析。此外,高性能的应用程序会希望在 ChannelHandlers 中提供尽可能多的逻辑,因为这有助于避免上下文切换的问题。

此外,SwiftNIO 还提供了一些通道实现。特别是,它提供了 ServerSocketChannel,一个用于接受入站连接的套接字的通道;SocketChannel,一个用于 TCP 连接的通道;DatagramChannel,一个用于UDP 套接字的通道;以及 EmbeddedChannel,一个主要用于测试的通道。

堵塞的注意事项

关于 ChannelPipeline 的一个重要说明是它们是线程安全的。这对于编写 SwiftNIO 应用程序非常重要,因为它允许你在不需要同步的情况下编写更简单的 ChannelHandlers。

然而,这是通过在与 EventLoop 相同的线程上调度 ChannelPipeline 上的所有代码来实现的。这意味着,作为一般规则,ChannelHandler 不得调用阻塞代码而不将其调度到后台线程。如果一个 ChannelHandler 由于任何原因阻塞,所有连接到父 EventLoopChannel 将无法进展,直到阻塞调用完成。

这是在编写 SwiftNIO 应用程序时常见的问题。如果以阻塞风格编写代码是有用的,强烈建议你在管道中完成工作后,将工作派遣到不同的线程。

Bootstrap

虽然可以直接用 EventLoops 配置和注册 Channel,但一般来说,有一个更高级别的抽象来处理这项工作更有用。

出于这个原因,SwiftNIO 提供了许多 Bootstrap 对象,其目的是简化通道的创建。一些 Bootstrap 对象还提供了其他功能,例如支持 Happy Eyeballs 进行 TCP 连接尝试。

目前 SwiftNIO 提供了三个 Bootstrap 对象。ServerBootstrap,用于引导监听信道;ClientBootstrap,用于引导客户端 TCP 信道; DatagramBootstrap 用于引导 UDP 信道。

ByteBuffer

SwiftNIO 应用程序中的大部分工作涉及到对缓冲区的字节进行洗牌。至少,数据是以字节缓冲区的形式在网络上发送和接收的。出于这个原因,拥有一个高性能的数据结构是非常重要的,这个结构要针对 SwiftNIO 应用所执行的工作进行优化。

出于这个原因,SwiftNIO 提供了 ByteBuffer,这是一个快速的 copy-on-write 字节缓冲区,它构成了大多数 SwiftNIO 应用的关键构建模块。

ByteBuffer 提供了许多有用的功能,此外还提供了许多钩子来在 "不安全 "模式下使用它。这关闭了边界检查以提高性能,但代价是可能会使您的应用程序面临内存正确性问题。

一般来说,强烈建议你在任何时候都在安全模式下使用 ByteBuffer

有关 ByteBuffer 的 API 的更多细节,请参见我们的 API 文档。

Promises and Futures

编写并发代码和编写同步代码的一个主要区别是,并不是所有的操作都会立即完成。例如,当你在通道上写入数据时,事件循环有可能无法立即将该写入数据发到网络。为此,SwiftNIO 提供了 EventLoopPromise 和 EventLoopFuture 来管理异步完成的操作。

EventLoopFuture<T> 本质上是一个函数返回值的容器,该函数的返回值将在未来某个时间被填充。每个 EventLoopFuture<T> 都有一个对应的 EventLoopPromise<T>,它是结果将被放入的对象。当承诺成功时,未来将被实现。

如果你必须对未来进行轮询来检测它何时完成,那将是相当低效的,所以 EventLoopFuture<T> 被设计成有管理的回调。本质上,你可以将回调挂在未来之外,当有结果时就会被执行。EventLoopFuture<T> 甚至会仔细安排调度,以确保这些回调总是在最初创建承诺的事件循环上执行,这有助于确保你不需要围绕 EventLoopFuture<T> 回调进行过多的同步。

另一个需要考虑的重要话题是传递给 close 的承诺与 Channel上closeFuture 的工作方式的区别。例如,传递到 close 的承诺将在 Channel 关闭后但在ChannelPipeline 完全清空之前成功。如果需要的话,这将允许您在ChannelPipeline 被完全清空之前对它采取行动。如果希望等待 Channel 关闭和 ChannelPipeline 被清空而不做任何进一步的操作,那么更好的选择是等待 closeFuture 成功。

有几个函数用于应用回调到 EventLoopFuture<T>,这取决于你希望它们如何以及何时执行。这些函数的详细内容留待API文档来介绍。

设计理念

SwiftNIO 被设计为构建网络应用和框架的强大工具,但它并不打算成为所有抽象层次的完美解决方案。SwiftNIO 紧紧地集中在提供基本的 I/O 基元和低抽象级别的协议实现上,将更多表现力强但速度较慢的抽象留给更广泛的社区来构建。我们的意图是,SwiftNIO 将成为服务器端应用的构建模块,而不一定是这些应用直接使用的框架。

需要从网络堆栈中获得极高性能的应用可能会选择直接使用 SwiftNIO,以减少其抽象的开销。这些应用应该能够以相对较少的维护成本来维持极高的性能。SwiftNIO 也专注于为这种用例提供有用的抽象,这样就可以直接构建极高性能的网络服务器。

核心的 SwiftNIO 资源库会直接以目录树的形式包含一些极其重要的协议实现,比如 HTTP。然而,我们认为,大多数协议实现应该与底层网络协议栈的发布周期脱钩,因为发布节奏可能非常不同(要么快得多,要么慢得多)。出于这个原因,我们积极鼓励社区在目录树外开发和维护他们的协议实现。事实上,一些第一方的 SwiftNIO 协议实现,包括我们的 TLS 和 HTTP/2 绑定,都是在目录树外开发的。