SwiftNIO
SwiftNIO是一个跨平台异步事件驱动的网络应用程序框架,可用于快速开发可维护的高性能协议服务器和客户端。
它与Netty类似,但专为Swift编写。
仓库组织
SwiftNIO项目分布在多个仓库中
| 仓库 | NIO 2 (Swift 5.2+) |
|---|---|
| https://github.com/apple/swift-nio SwiftNIO核心 |
版本: "2.0.0" |
| https://github.com/apple/swift-nio-ssl TLS (SSL)支持 |
版本: "2.0.0" |
| https://github.com/apple/swift-nio-http2 HTTP/2支持 |
版本: "1.0.0" |
| https://github.com/apple/swift-nio-extras SwiftNIO周围的有用的补充 |
版本: "1.0.0" |
| https://github.com/apple/swift-nio-transport-services 针对macOS、iOS、tvOS和watchOS的高级支持 |
版本: "1.0.0" |
| https://github.com/apple/swift-nio-ssh SSH支持 |
.upToNextMinor(from: "0.2.0") |
NIO 2.29.0及更老版本支持Swift 5.0+
在这个仓库中,我们有许多提供不同功能的产品。这个包包含以下产品
NIO. 这是一个导出NIOCore、NIOEmbedded和NIOPosix的架构模块。NIOCore. 它为使用SwiftNIO提供了核心抽象和类型(参见"概念性概述"获取更多详细信息)。大多数为提供类似新的EventLoop和Channel或新的协议实现的项目而创建的NIO扩展项目,应该只需要依赖NIOCore。NIOPosix。这提供了 POSIX 系统上的主要 [EventLoopGroup]、EventLoop和Channel。这是我们高性能的核心 I/O 层。一般来说,只有计划进行一些实际 I/O 的项目(如高级协议实现或应用程序)才应该导入此库。NIOEmbedded。这提供了EmbeddedChannel和EmbeddedEventLoop,这些是对 NIOCore 抽象的实现,提供了对其执行的精细控制。这些最常用于测试,但也可以用于以完全解耦于网络的方式驱动协议实现。NIOConcurrencyHelpers。这提供了一些低级并发原语,用于 NIO 实现中,如锁和原子操作。NIOFoundationCompat。这扩展了多个 NIO 类型以提高与 Foundation 数据类型的互操作性。如果您正在使用Data等Foundation数据类型,则应导入此库。NIOTLS。這提供了一些在多個 TLS 實現中工作的常见抽象类型。请注意,这本身不提供 TLS:请调研 swift-nio-ssl 和 swift-nio-transport-services 以获取具体的实现。NIOHTTP1。这提供了一个底层的 HTTP/1.1 协议实现。NIOWebSocket。这提供了一个底层的 WebSocket 协议实现。NIOTestUtils。这为使用 SwiftNIO 的测试项目提供了一些助手。
协议实现
以下是一份使用 SwiftNIO 实现的协议实现的列表。这是一份不完整的协议列表,它们要么是 SwiftNIO 项目的一部分,要么已被接受到 SSWG 的孵化过程中。以下列出的所有库都使用 SwiftNIO 在非阻塞方式下执行所有 I/O。
底层协议实现
底层的协议实现通常是 ChannelHandler 的一系列集合,这些实现了一个协议,但仍需要用户对 SwiftNIO 有良好的理解。通常,低级协议实现会被包装在具有更好、更用户友好的 API 的高级库中。
| 协议 | 客户端 | 服务器 | 仓库 | 模块 | 注释 |
|---|---|---|---|---|---|
| HTTP/1 | apple/swift-nio | NIOHTTP1 |
官方 NIO 项目 | ||
| HTTP/2 | apple/swift-nio-http2 | NIOHTTP2 |
官方 NIO 项目 | ||
| WebSocket | apple/swift-nio | NIOWebSocket |
官方 NIO 项目 | ||
| TLS | apple/swift-nio-ssl | NIOSSL |
官方 NIO 项目 | ||
| SSH | apple/swift-nio-ssh | 不适用 | 官方 NIO 项目 |
高级实现
高级实现通常是带有API的库,该API不暴露SwiftNIO的 ChannelPipeline,因此可以几乎不需要(或不需要)SwiftNIO特定的知识来使用。以下列出的实现仍全部在SwiftNIO中执行其I/O操作,并与SwiftNIO生态系统集成得很好。
| 协议 | 客户端 | 服务器 | 仓库 | 模块 | 注释 |
|---|---|---|---|---|---|
| HTTP | swift-server/async-http-client | AsyncHTTPClient |
SSWG社区项目 | ||
| gRPC | grpc/grpc-swift | GRPC |
同时提供了一个低级API;SSWG社区项目 | ||
| APNS | kylebrowning/APNSwift | APNSwift |
SSWG社区项目 | ||
| PostgreSQL | vapor/postgres-nio | PostgresNIO |
SSWG社区项目 | ||
| Redis | mordil/swift-redi-stack | RediStack |
SSWG社区项目 |
支持版本
SwiftNIO 2
这是当前版本的SwiftNIO,将在可预见的未来得到支持。
最新发布的SwiftNIO 2版本支持Swift 5.2+。NIO 2.29.0及更早的版本支持Swift 5.0+。
SwiftNIO 1
SwiftNIO 1被认为已经停止维护 - 强烈建议您升级到新版本。Core NIO团队不再积极为此版本工作。不会为此版本添加新功能,但将接受直到2022年5月底的PR来解决错误或安全问题。
如果您有一个需要迁移到SwiftNIO 2的SwiftNIO 1应用程序或库,请查看我们为您准备的迁移指南。
最新发布的SwiftNIO 1版本支持Swift 4.0、4.1、4.2和5.0。
支持的平台
SwiftNIO旨在支持所有支持Swift的平台。目前,它在macOS和Linux上开发和测试,并且已知支持以下操作系统版本
- Ubuntu 18.04+
- macOS 10.9+, iOS 7+;(macOS 10.14+, iOS 12+, tvOS 12+ 或 watchOS 6+,需安装swift-nio-transport-services)
兼容性
SwiftNIO遵循SemVer 2.0.0规范,并有一个独立的文档声明了SwiftNIO公共API。
这意味着您应使用从所需的最低SwiftNIO版本至下一个主要版本的版本范围依赖SwiftNIO。在SwiftPM中,可以通过指定如from: "2.0.0"轻松实现,这意味着您从2.0.0版本开始支持SwiftNIO的每个版本,直到(不包括)3.0.0版本。SemVer和SwiftNIO公共API保证了无需为每个版本进行兼容性测试即可在程序中工作。
概念概述
SwiftNIO是一个用于在Swift中构建高性能网络应用的底层工具。它特别针对使用“每个连接一个线程”的并发模型效率低或不可行的情况。这是在构建使用大量相对低利用率连接的服务器时的一个常见限制,例如HTTP服务器。
为了实现其目标,SwiftNIO广泛使用“非阻塞I/O”:因此得名!非阻塞I/O与更常见的阻塞I/O模型不同,因为应用程序不会等待将数据发送到或从网络接收到:相反,SwiftNIO会请求内核在I/O操作可以执行而无需等待时通知它。
SwiftNIO并不旨在提供像网页框架那样的高级解决方案。相反,SwiftNIO专注于为这些高级应用程序提供底层构建块。在构建网页应用时,大多数用户不会想直接使用SwiftNIO:相反,他们想使用许多在Swift生态系统中的优秀网页框架之一。然而,这些网页框架可能会选择在内部使用SwiftNIO来提供其网络支持。
以下部分将描述SwiftNIO提供的底层工具,并提供如何使用它们的快速概述。如果您对这些概念感到舒适,则可以直接查看本README的其他部分。
基本架构
SwiftNIO的基本构建块包括以下8种对象:
EventLoopGroup,一个由NIOCore提供的协议。EventLoop,一个由NIOCore提供的协议。Channel,一个由NIOCore提供的协议。ChannelHandler,一个由NIOCore提供的协议。Bootstrap,由NIOCore提供的多个相关结构。ByteBuffer,一个由NIOCore提供的结构体。EventLoopFuture,一个通用的类,由NIOCore提供。EventLoopPromise,一个通用结构体,由NIOCore提供。
所有SwiftNIO应用程序最终都是由这些不同组件构建的。
事件循环和事件循环组
SwiftNIO的基本I/O原语是事件循环。事件循环是一个等待事件发生(通常是I/O相关的事件,例如“接收数据”)并在它们发生时触发某种回调的对象。在几乎所有的SwiftNIO应用程序中,都会有相对较少的事件循环:通常每个应用程序 wanted 使用的CPU核心只有一个或两个。一般来说,事件循环会运行整个应用程序生命周期,在无限循环中分派事件。
事件循环被组合成事件循环组。这些组提供了一种在工作之间进行分布的机制。例如,在监听传入连接时,监听套接字将被注册在一个事件循环上。然而,我们不希望在该监听套接字上接受的所有连接都注册在同一个事件循环上,因为这可能会潜在地压倒一个事件循环,而其他事件循环为空。因此,事件循环组提供了在多个事件循环之间分配负载的能力。
在SwiftNIO中,目前有一个EventLoopGroup实现,以及两个EventLoop实现。对于生产应用程序,有一个MultiThreadedEventLoopGroup,这是一个创建多个线程(使用POSIX pthreads库)并在每个线程上放置一个SelectableEventLoop的EventLoopGroup。根据目标系统,SelectableEventLoop使用选择器(要么是kqueue,要么是epoll)来管理文件描述符上的I/O事件并调度工作。这些EventLoop和EventLoopGroup由NIOPosix模块提供。此外,还有由NIOEmbedded模块提供的EmbeddedEventLoop,这是一个用于测试目的的虚拟事件循环。
EventLoop具有许多重要属性。最重要的是,它们是SwiftNIO应用程序中所有工作的执行方式。为了确保线程安全,几乎所有在SwiftNIO中的其他对象上都必须通过一个EventLoop来调度工作。大部分工作都由EventLoop对象拥有,因此理解它们的执行模型对于构建高性能的SwiftNIO应用程序至关重要。
通道、通道处理器、通道流水线和通道上下文
EventLoop对SwiftNIO的工作方式至关重要,但大多数用户除了要求它们创建EventLoopPromise和调度工作之外,并不会与它们进行实质性的交互。SwiftNIO应用程序用户将花最多时间与之交互的部分是Channel和ChannelHandler。
几乎在SwiftNIO程序中与用户交互的每个文件描述符都关联着一个单个的Channel。这个Channel拥有该文件描述符,并负责管理其生命周期。它还负责在该文件描述符上处理入站和出站事件:每当事件循环有对应文件描述符的事件时,它将通知拥有该文件描述符的Channel。
Channel 本身并没有什么用处。毕竟,很少有线程不想对其在套接字上发送或接收的数据做任何事情!因此,Channel 的另一个重要部分是 ChannelPipeline。
ChannelPipeline 是一系列称为 ChannelHandler 的对象,它们按照顺序在 Channel 上处理事件。这些 ChannelHandler 顺序处理这些事件,对事件进行修改和转换。这可以看作是一个数据处理管道;因此称其为 ChannelPipeline。
所有 ChannelHandler 都可以是入站或出站处理器,或两者兼具。入站处理器处理“入站”事件:如从套接字读取数据、读取套接字关闭或其他由远程对等方发起的事件。出站处理器处理“出站”事件,例如写入、连接尝试和本地套接字关闭。
每个处理器按顺序处理事件。例如,读取事件从前端管道传递到后端,每个处理器依次进行,而写入事件则从后端传递到前端。处理器可以在任何时候生成入站或出站事件,并将它们发送到下一个处理器,无论在哪个方向上是恰当的。这允许处理器分割读取、合并写入、延迟连接尝试,以及一般地执行事件的不规则转换。
通常,ChannelHandler 被设计成高度可重用的组件。这意味着它们倾向于设计得尽可能小,执行一个特定的数据转换。这使得处理器能够以新奇和灵活的方式组合在一起,有助于代码重用和封装。
ChannelHandler 通过使用 ChannelHandlerContext 来跟踪它们在 ChannelPipeline 中的位置。这些对象包含对管道中前一个和下一个通道处理器的引用,确保 ChannelHandler 在管道中保持时始终可以发出事件。
SwiftNIO自带了许多内置的 ChannelHandler,提供了有用的功能,如 HTTP 解析。此外,高性能应用程序将希望尽可能地将它们的逻辑嵌入到 ChannelHandler 中,因为它有助于避免上下文切换问题。
此外,SwiftNIO还自带了几种Channel实现。具体来说,它自带了ServerSocketChannel,这是一个用于接受入站连接的Channel;SocketChannel,这是一个用于TCP连接的Channel;以及DatagramChannel,这是一个用于UDP套接字的Channel。所有这些都是由NIOPosix模块提供的。它还提供了一个EmbeddedChannel,这是一个主要用于测试的Channel,由NIOEmbedded模块提供。
关于阻塞的注意
关于ChannelPipeline的一个重要注意点是它们是线程安全的。这对于编写SwiftNIO应用程序非常重要,因为它允许你编写更简单的ChannelHandler,因为你知道它们不需要同步。
但这是通过在同一个线程上调度所有ChannelPipeline上的代码来实现的。这意味着,一般而言,ChannelHandler不得在不将其调度到后台线程的情况下调用阻塞代码。如果一个ChannelHandler由于任何原因而被阻塞,则附加到父EventLoop的所有Channel都将无法进一步发展,直到阻塞调用完成。
在编写SwiftNIO应用程序时,这是一个常见的问题。如果你有用阻塞风格编写代码的用途,强烈建议你在完成管道中的代码后将其派发到不同的线程。
引导
虽然可以直接配置和注册Channel和EventLoop,但通常有一个更高级的抽象来处理这项工作更有用。
因此,SwiftNIO提供了一系列Bootstrap对象,其目的是简化通道创建的工作。一些Bootstrap对象还提供其他功能,例如支持Happy Eyeballs进行TCP连接尝试。
目前 SwiftNIO 在 NIOPosix 模块中提供了三个 Bootstrap 对象:用于启动监听通道的 ServerBootstrap;用于启动客户端 TCP 通道的 ClientBootstrap;以及用于启动 UDP 通道的 DatagramBootstrap。
ByteBuffer
SwiftNIO 应用中的大多数工作都涉及到字节缓冲区的操作。至少,数据是以字节缓冲区的形式发送和接收的。因此,拥有一个高性能且针对 SwiftNIO 应用的操作进行优化的数据结构非常关键。
因此,SwiftNIO 提供了 ByteBuffer,这是一种快速的复制写入字节缓冲区,构成了大多数 SwiftNIO 应用的重要构建块。此类型由 NIOCore 模块提供。
ByteBuffer 提供了许多有用的功能,并在“不安全”模式下提供了多个钩子。这关闭了边界检查以改进性能,但可能会使您的应用程序面临内存正确性问题。
通常情况下,强烈建议您始终使用 ByteBuffer 的安全模式。
关于 ByteBuffer 的 API 的更多详细信息,请参阅下面的链接文档。
承诺 (Promise) 和未来 (Future)
编写并发代码与编写同步代码之间的一个主要区别是,并非所有操作都会立即完成。例如,当您在一个通道上写入数据时,可能事件循环无法立即将这次写操作发送到网络。因此,SwiftNIO 提供了 EventLoopPromise<T> 和 EventLoopFuture<T> 来管理那些 异步完成 的操作。这些类型由 NIOCore 模块提供。
EventLoopFuture<T> 实质上是一个函数返回值的容器,该函数的返回值将在将来的某个时候填充。每个 EventLoopFuture<T> 都有一个相应的 EventLoopPromise<T>,将结果放入的对象。当承诺成功时,未来将会得到满足。
如果您需要对未来进行轮询以检测其何时完成,这将会非常低效。因此,《EventLoopFuture<T>》被设计为具有管理的回调。本质上,您可以在将结果可用时执行的未来上挂载回调。甚至《EventLoopFuture<T>》还会仔细安排调度,以确保这些回调始终在最初创建许诺的事件循环上执行,这有助于确保您在《EventLoopFuture<T>》回调周围不需要太多的同步。
另一个需要考虑的重要主题是,在Channel上,《code>close传递的许诺与《code>closeFuture之间的区别。例如,传递到《code>close的许诺将在《code>Channel关闭后成功,但在《code>ChannelPipeline完全清除之前。这将允许在需要的情况下在《code>ChannelPipeline完全清除之前对其进行操作。如果您希望在《code>Channel关闭并清除《code>ChannelPipeline而无需进一步操作时等待,那么更好的选择是等待《code>closeFuture成功。
根据您希望何时以及如何执行它们,有几种将回调应用于《EventLoopFuture<T>》的函数。这些函数的详细信息留待API文档说明。
设计哲学
SwiftNIO被设计为构建网络应用程序和框架的强大工具,但它并非旨在为所有抽象级别提供完美的解决方案。SwiftNIO在底层网络栈的抽象级别较低时,专注于提供基本的I/O原语和协议实现,并留下更多表达性但速度较慢的抽象由更广泛的社区来构建。目的是使SwiftNIO成为服务器端应用程序的构建块,而不一定是这些应用程序将直接使用的框架。
需要从其网络堆栈中获得极高性能的应用程序可以选择直接使用SwiftNIO以减少其抽象的开销。这些应用程序应该能够在相对较低的维护成本下保持极高的性能。SwiftNIO还专注于为这种用例提供有用的抽象,以便可以直接构建具有极高性能的网络服务器。
SwiftNIO的核心存储库将包含一些非常重要的协议实现,比如HTTP,直接在树形结构中。然而,我们认为大多数协议实现应该与基础网络栈的发布周期解耦,因为发布周期可能非常不同(要么快得多,要么慢得多)。因此,我们积极鼓励社区在树外开发和维护他们的协议实现。实际上,一些SwiftNIO的第一方协议实现,包括我们的TLS和HTTP/2绑定,都是在树外开发的!
文档
示例使用
目前有几个示例项目展示了如何使用SwiftNIO。
- 聊天客户端 https://github.com/apple/swift-nio/tree/main/Sources/NIOChatClient
- 聊天服务器 https://github.com/apple/swift-nio/tree/main/Sources/NIOChatServer
- 回显客户端 https://github.com/apple/swift-nio/tree/main/Sources/NIOEchoClient
- 回显服务器 https://github.com/apple/swift-nio/tree/main/Sources/NIOEchoServer
- UDP回显客户端 https://github.com/apple/swift-nio/tree/main/Sources/NIOUDPEchoClient
- UDP回显服务器 https://github.com/apple/swift-nio/tree/main/Sources/NIOUDPEchoServer
- HTTP客户端 https://github.com/apple/swift-nio/tree/main/Sources/NIOHTTP1Client
- HTTP服务器 https://github.com/apple/swift-nio/tree/main/Sources/NIOHTTP1Server
- WebSocket客户端 https://github.com/apple/swift-nio/tree/main/Sources/NIOWebSocketClient
- WebSocket服务器 https://github.com/apple/swift-nio/tree/main/Sources/NIOWebSocketServer
要构建和运行它们,请运行以下命令,将TARGET_NAME替换为./Sources下的文件夹名
swift run TARGET_NAME例如,要运行NIOHTTP1Server,请执行以下命令
swift run NIOHTTP1Server入门
SwiftNIO主要使用 SwiftPM 作为其构建工具,因此我们建议您也使用它。如果想在您的项目中依赖SwiftNIO,只需在您的 Package.swift 文件中添加一个 dependencies 子句即可
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0")
]并添加适当的SwiftNIO模块到您的目标依赖项。添加目标依赖项的语法在不同版本的Swift之间略有不同。例如,如果您想依赖NIOCore、NIOPosix和NIOHTTP1模块,指定以下依赖项
Swift 5.2 及以上版本 (swift-tools-version:5.2)
dependencies: [.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio")]
使用 Xcode 包支持
如果你的项目是设置为 Xcode 项目,并且你使用的是 Xcode 11 或更高版本,你可以通过点击“文件”->“Swift 包”->“添加包依赖”将 SwiftNIO 添加为 Xcode 项目的依赖。在即将打开的对话框中,请输入 https://github.com/apple/swift-nio.git 并连续点击两次“下一步”。最后,选择你计划使用的目标(例如 NIOCore,NIOHTTP1 和 NIOFoundationCompat)并点击完成。现在你可以在你的项目中导入 import NIOCore(以及其他你选择的全部目标)。
要处理 SwiftNIO 本身,或者调查一些示例应用程序,可以直接克隆仓库并使用 SwiftPM 帮助构建。例如,你可以运行以下命令来编译和运行示例 echo 服务器
swift build
swift test
swift run NIOEchoServer为了验证它是否正常工作,你可以使用另一个终端尝试连接到它
echo "Hello SwiftNIO" | nc localhost 9999如果一切顺利,你会看到消息被回送到你那里。
要使用 Xcode 11+ 处理 SwiftNIO,只需在 Xcode 中打开 Package.swift 文件并使用 Xcode 对 SwiftPM 包的支持。
如果想要使用 Xcode 10 开发 SwiftNIO,你必须生成一个 Xcode 项目
swift package generate-xcodeproj另一种选择:使用 docker-compose
或者,你可能想使用 docker-compose 进行开发或测试。
首先确保你已经安装了 Docker,然后运行以下命令
-
docker-compose -f docker/docker-compose.yaml run test将创建一个带有 Swift 运行时和其他构建和测试依赖项的基础镜像,编译 SwiftNIO 并运行单元和集成测试
-
docker-compose -f docker/docker-compose.yaml up echo将创建一个基础镜像,编译 SwiftNIO,并在本地的 9999 端口上运行一个示例
NIOEchoServer。通过echo Hello SwiftNIO | nc localhost 9999来测试它。 -
docker-compose -f docker/docker-compose.yaml up http将创建一个基础镜像,编译 SwiftNIO,并在本地的 8888 端口上运行一个示例
NIOHTTP1Server。通过curl https://:8888来测试它。 -
docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.2004.54.yaml run test将使用 Ubuntu 20.04 和 Swift 5.4 创建一个基础镜像,编译 SwiftNIO 并运行单元和集成测试。在 docker 目录中存在其他 ubuntu 和 Swift 版本的文件。
开发SwiftNIO
注意:本节只适用于想要自己开发SwiftNIO的情况。如果您只是想使用SwiftNIO作为SwiftPM包,则可以忽略此处信息。
原则上,SwiftNIO的开发与其他SwiftPM项目一样简单。不过,在您贡献前,我们确实有一些流程需要了解。详细信息请参阅本仓库中的CONTRIBUTING.md文件。
先决条件
SwiftNIO的main分支是SwiftNIO 2下一个版本的开发分支,它仅支持Swift 5。
为了能够编译和运行SwiftNIO以及集成测试,您需要在系统上安装一些先决条件。
macOS
- Xcode 11.4或更高版本,建议使用Xcode 12。
Linux
- 从swift.org/download安装Swift 5.2、5.3或5.4。我们始终推荐使用最新发布的版本。
- netcat(仅用于集成测试)
- lsof(仅用于集成测试)
- shasum(仅用于集成测试)
Ubuntu 18.04
# install swift tarball from https://swiftlang.cn/downloads
apt-get install -y git curl libatomic1 libxml2 netcat-openbsd lsof perl
Fedora 28+
dnf install swift-lang /usr/bin/nc /usr/bin/lsof /usr/bin/shasum
加速测试
可以在并行模式下运行测试套件,如果您拥有一台较大多核机器,这将节省大量时间。只需在运行测试时添加--parallel。这将使测试套件的运行时间加快30倍或更多。
swift test --parallel