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数据类型交互。如果您在使用Foundation数据类型(例如Data
),则应导入此。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+需要< a href="https://github.com/apple/swift-nio-transport-services">swift-nio-transport-services)
兼容性
SwiftNIO遵循SemVer 2.0.0,并通过单独的文档声明了SwiftNIO公共API。
这意味着对于您来说,您应该依赖于SwiftNIO的版本范围,从您所需的最小SwiftNIO版本开始,一直到下一个主要版本。在SwiftPM中,您可以通过指定例如from: "2.0.0"
来轻松实现,这意味着您支持从2.0.0开始的每个版本,直至(不包括)3.0.0。SemVer和SwiftNIO公共API的保证将确保程序可以在无需担心兼容性的每个版本中运行。
概念概述
SwiftNIO是一种本质上用于在Swift中构建高性能网络应用的底层工具。它特别针对使用“每个连接一个线程”的并发模型效率低下或难以实现的情况。这通常是构建服务器时的一个常见限制,比如HTTP服务器。
为了实现其目标,SwiftNIO广泛使用了“非阻塞I/O”——这就是其名称的由来!非阻塞I/O与更常见的阻塞I/O模型不同,因为应用程序不会等待数据传递到或从网络中接收:相反,SwiftNIO会向内核请求在无需等待的情况下执行I/O操作的通知。
SwiftNIO不旨在提供类似于例如网络框架等高级解决方案。相反,SwiftNIO专注于为这些高级应用程序提供低级构建块。当涉及到构建一个Web应用程序时,大多数用户不会想直接使用SwiftNIO:相反,他们想使用Swift生态系统提供的众多优秀的Web框架之一。然而,这些Web框架可能会选择在背后使用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应用程序中,将会有相对较少的事件循环:通常在每个应用程序想要使用的CPU核心上只有一个或两个。一般来说,事件循环在整个应用程序生命周期内运行,无限循环地调度事件。
事件循环被组织成事件循环组。这些组提供了在工作轮询之间分配工作的机制。例如,在监听传入连接时,监听套接字将在一个事件循环上注册。然而,我们不希望在该监听套接字上接受的连接都注册在同一个事件循环上,因为这可能会过度负载一个事件循环,而使其他的事件循环空闲。因此,事件循环组提供了在多个事件循环之间分配负载的能力。
在SwiftNIO中,当前有一个 EventLoopGroup
实现,以及两个 EventLoop
实现。对于生产级应用,有 MultiThreadedEventLoopGroup
,这是一个创建了多个线程(使用POSIX pthreads
库)的 EventLoopGroup
,并在每个线程上放置了一个 SelectableEventLoop
。这个 SelectableEventLoop
是一个事件循环,它使用选择器(根据目标系统是 kqueue
还是 epoll
)来管理文件描述符的I/O事件,并调度工作。这些 EventLoop
和 EventLoopGroup
由 NIOPosix
模块提供。另外,还有 EmbeddedEventLoop
,这是一个用于测试的主要目的的虚拟事件循环,由 NIOEmbedded
模块提供。
EventLoop
有许多重要属性。最重要的是,它们是SwiftNIO应用程序中所有工作的执行方式。为了确保线程安全,几乎所有在SwiftNIO中的应用程序中的其他对象上的工作都必须通过一个 EventLoop
来调度。几乎所有SwiftNIO应用程序中的对象都由 EventLoop
对象拥有,理解它们的执行模型对于构建高性能的SwiftNIO应用程序至关重要。
通道、通道处理器、通道流水线和通道上下文
尽管 EventLoop
对SwiftNIO的工作方式至关重要,但大多数用户与其交互并不多,只是要求它们创建 EventLoopPromise
并安排工作。大多数用户将花费最多时间的SwiftNIO应用程序的部分是与 Channel
和 ChannelHandler
交互。
几乎在SwiftNIO程序中用户与之交互的每个文件描述符都与单个 Channel
相关联。这个 Channel
拥有这个文件描述符,并负责管理其生命周期。它还负责处理该文件描述符上的入站和出站事件:每当事件循环有对应文件描述符的事件时,它将通知拥有该文件描述符的 Channel
。
然而,Channel
本身并没有多少用处。毕竟,不愿意对其在套接字上发送或接收的数据进行操作的罕见应用是很少的!所以,Channel
另一个重要组成部分就是 ChannelPipeline
。
ChannelPipeline
是一系列被称为 ChannelHandler
的对象序列,它们处理 Channel
上的事件。这些 ChannelHandler
按顺序逐个处理这些事件,在过程中修改并转换事件。这可以看作是一个数据处理管道;因此命名为 ChannelPipeline
。
所有的 ChannelHandler
要么是 Inbound(入站)处理器,要么是 Outbound(出站)处理器,或者两者兼而有之。Inbound 处理器处理“入站”事件:例如从套接字读取数据、读取套接字关闭或其他由远程对等方发起的事件。Outbound 处理器处理“出站”事件,例如写入、连接尝试和本地套接字关闭。
每个处理器按顺序处理事件。例如,读取事件从管道前端到后端,一个处理器一个处理器地传递,而写入事件从管道后端到前端传递。每个处理器随时可能生成入站或出站事件,并将这些事件发送到管道中适当的下一处理器。这允许处理器拆分读取操作、合并写入操作、延迟连接尝试,并通常对事件进行任意转换。
总的来说,ChannelHandler
被设计成可高度重用的组件。这意味着它们倾向于设计得尽可能小,执行一个具体的数据转换。这使得处理器能够以新颖和灵活的方式组合在一起,这有助于代码重用和封装。
ChannelHandler
能够通过使用 ChannelHandlerContext
来跟踪它们在 ChannelPipeline
中的位置。这些对象包含对管道中先前和下一个通道处理器的引用,确保 ChannelHandler
在管道中始终能够发射事件。
SwiftNIO自带了许多提供有用功能,如 HTTP 解析的内置 ChannelHandler
。此外,高性能应用程序希望尽可能多地将它们的逻辑放在 ChannelHandler
中,因为它有助于避免上下文切换的问题。
此外,SwiftNIO还附带了一些Channel
实现。特别地,它提供了ServerSocketChannel
,这是一种用于接受入站连接的Channel
;SocketChannel
,这是一种用于TCP连接的Channel
;以及DatagramChannel
,这是一种用于UDP套接字的Channel
。所有这些都由NIOPosix
模块提供。它还提供了EmbeddedChannel
,这是一个由NIOEmbedded
模块提供的Channel
,主要用于测试。
关于阻塞的说明
ChannelPipeline
的一个重要特性是它是线程安全的。这对于编写SwiftNIO应用程序非常重要,因为它可以让你在不需要同步的情况下编写更简单的ChannelHandler
。
然而,这是通过在同一个线程上调度所有该ChannelPipeline
上的代码来实现的,这个线程与EventLoop
相同。这意味着,作为一般规则,ChannelHandler
必须在不将其调度到后台线程的情况下调用阻塞代码。如果一个ChannelHandler
因为任何原因而阻塞,那么连接到父级EventLoop
的Channel
将无法继续进行,直到阻塞调用完成。
这在编写SwiftNIO应用程序时是一个常见的问题。如果你需要以阻塞的方式编写代码,强烈建议你在处理Pipeline之后的代码时将工作调度到不同的线程。
Bootstrap
虽然你可以直接通过配置和注册Channel
与EventLoop
,但使用更高层次的抽象来处理这项工作通常更有用。
因此,SwiftNIO提供了一系列Bootstrap
对象,其目的是简化通道的创建。一些Bootstrap
对象还提供了其他功能,例如Happy Eyeballs对TCP连接尝试的支持。
目前SwiftNIO随NIOPosix模块一起提供了三个Bootstrap
对象:ServerBootstrap
,用于启动监听通道;《a href="https://apple.github.io/swift-nio/docs/current/NIOPosix/Classes/ClientBootstrap.html" rel="nofollow">ClientBootstrap
》,用于启动客户端TCP通道;《a href="https://apple.github.io/swift-nio/docs/current/NIOPosix/Classes/DatagramBootstrap.html" rel="nofollow">DatagramBootstrap
》,用于启动UDP通道。
ByteBuffer
在SwiftNIO应用中,大部分的工作都涉及到字节数据的移动。至少,数据是通过字节数据的缓冲区在网络中进行发送和接收的。因此,拥有一个针对SwiftNIO应用性能优化的高性能数据结构非常重要。
因此,SwiftNIO提供了ByteBuffer
,这是一个快速的写时复制字节缓冲区,是大多数SwiftNIO应用的基石。此类型由NIOCore
模块提供。
ByteBuffer
提供了一些有用的功能,并且还提供了一些钩子来在“不安全”模式下使用它。这关闭了边界检查以改善性能,但这可能使您的应用程序面临内存正确性问题。
通常,建议您始终使用ByteBuffer
的安全模式。
有关ByteBuffer
的API详细信息,请参阅下面的API文档。
承诺和未来
在编写并发代码与同步代码之间有一个主要区别是,并非所有操作都会立即完成。例如,当你在通道上写下数据时,事件循环可能无法立即将该写入信息发送到网络。因此,SwiftNIO提供了EventLoopPromise{T}
和EventLoopFuture{T}
来管理那些异步完成的操作。这些类型由NIOCore
模块提供。
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绑定,都是在树外开发的!
文档
示例用法
目前有几个示例项目展示了如何使用 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-tools-version:5.2
)
Swift 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,并在
localhost:9999
上运行一个示例NIOEchoServer
。通过以下命令测试:echo Hello SwiftNIO | nc localhost 9999
。 -
docker-compose -f docker/docker-compose.yaml up http
将会创建一个基础镜像,编译 SwiftNIO,并在
localhost:8888
上运行一个示例NIOHTTP1Server
。通过以下命令测试:curl http://localhost: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://swift.org/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