SwiftNIO
SwiftNIO 是一个跨平台异步事件驱动网络应用程序框架,适用于快速开发可维护的高性能协议服务器和客户端。
它类似于 Netty,但是用 Swift 编写的。
仓库组织
SwiftNIO 项目跨多个仓库分割
仓库 | NIO 2 (Swift 5+) | NIO 1 (Swift 4+) |
---|---|---|
https://github.com/apple/swift-nio SwiftNIO 核心 |
版本: "2.0.0" |
版本: "1.0.0" |
https://github.com/apple/swift-nio-ssl TLS (SSL) 支持 |
版本: "2.0.0" |
版本: "1.0.0" |
https://github.com/apple/swift-nio-http2 HTTP/2 支持 |
版本: "1.0.0" |
版本: "0.1.0" |
https://github.com/apple/swift-nio-extras SwiftNIO 的一系列实用功能 |
版本: "1.0.0" |
版本: "0.1.0" |
https://github.com/apple/swift-nio-transport-services macOS、iOS 和 tvOS 的第一级支持 |
版本: "1.0.0" |
版本: "0.1.0" |
协议实现
以下列出了使用 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 项目 |
高级实现
高级实现通常是带有 API 的库,该 API 不公开 SwiftNIO 的 ChannelPipeline
,因此可以非常少地(或没有)使用 SwiftNIO 特定的知识。下面列出的实现仍然在其全部的 I/O 中使用 SwiftNIO,并且与 SwiftNIO 生态系统集成得很好。
协议 | 客户端 | 服务器 | 仓库 | 模块 | 注释 |
---|---|---|---|---|---|
HTTP | swift-server/async-http-client | AsyncHTTPClient |
SSWG 社区项目 | ||
gRPC | grpc/grpc-swift | GRPC |
也提供低级 API;社区项目 | ||
APNS | kylebrowning/APNSwift | APNSwift |
SSWG 社区项目 | ||
PostgreSQL | vapor/postgres-nio | PostgresNIO |
SSWG 社区项目 | ||
Redis | mordil/swift-redi-stack | RediStack |
SSWG 社区项目 |
支持平台
SwiftNIO 旨在支持所有支持 Swift 的平台。目前,它正在 macOS 和 Linux 上开发和测试,并且已知支持以下操作系统版本
- Ubuntu 14.04+
- macOS 10.12+;(macOS 10.14+, iOS 12+, 或 tvOS 12+ 使用 swift-nio-transport-services)
Swift 版本
SwiftNIO 1
最新发布的SwiftNIO 1版本支持Swift 4.0、4.1、4.2和5.0。
SwiftNIO 2
最新发布的SwiftNIO 2版本仅支持Swift 5.0、5.1和5.2。如果您想将SwiftNIO 1应用程序或库迁移到SwiftNIO 2,请查阅我们为您准备的迁移指南。
兼容性
SwiftNIO遵循SemVer 2.0.0,并有一个单独的文档声明SwiftNIO的公共API。
这意味着您应该使用SwiftNIO的版本范围,从您需要的最低SwiftNIO版本到下一个主要版本。在SwiftPM中,可以很容易地指定例如from: "2.0.0"
,这意味着您支持从2.0.0开始(不包括)3.0.0的每个版本。SemVer和SwiftNIO的公共API保证了无需担心兼容性而测试每个单个版本,程序都能正常工作。
概念概述
SwiftNIO是构建高性能网络应用程序的低级工具。它主要针对那些使用“每个连接一个线程”并发模型不高效或不可行的用例。这在构建使用大量相对低利用率连接的服务器时很常见,例如HTTP服务器。
为了实现其目标,SwiftNIO广泛使用“非阻塞I/O”:这就是其名称的由来!非阻塞I/O与更常见的阻塞I/O模型不同,因为应用程序不会等待网络中的数据发送或接收:相反,SwiftNIO请求内核在其I/O操作可以在不等待的情况下执行时通知它。
SwiftNIO的目的并不在于提供像Web框架那样的高级解决方案。相反,SwiftNIO致力于提供构建这些高级应用程序的低层次构建模块。在构建Web应用程序时,大多数用户并不想直接使用SwiftNIO:相反,他们希望使用Swift生态系统中的许多优秀的Web框架之一。然而,这些Web框架可能会在底层选择使用SwiftNIO来提供它们所需的网络支持。
以下部分将描述SwiftNIO提供的低层次工具,并提供关于如何使用它们的快速概述。如果您对这些概念感到舒适,则可以直接跳到本README的其它部分。
基本架构
SwiftNIO的基本构建块包括以下8种类型的对象:
EventLoopGroup
,一个协议EventLoop
,一个协议Channel
,一个协议ChannelHandler
,一个协议Bootstrap
,几个相关结构ByteBuffer
,一个结构体EventLoopFuture
,一个泛型类EventLoopPromise
,一个泛型结构体。
所有的SwiftNIO应用程序最终都是由这些各种组件构建的。
事件循环和事件循环组
SwiftNIO的基本I/O原语是事件循环。事件循环是一个等待事件发生(通常是I/O相关事件,如“接收到数据”)的客体,并在事件发生时触发某种回调。在几乎所有的SwiftNIO应用程序中,事件循环的数量相对较少:通常每个应用程序想要使用的CPU核心只有一个或两个。一般来说,事件循环会在整个应用程序生命周期中运行,在无尽的循环中分配事件。
事件循环被组合成事件循环的组。这些组提供了一种将工作分配到事件循环上的机制。例如,当监听传入连接时,监听套接字将注册在一个事件循环上。然而,我们不想将这个监听套接字上接受的所有连接都注册在相同的事件循环上,因为这样可能会使一个事件循环过载,而其它的事件循环则空闲。因此,事件循环组提供了在多个事件循环上分配负载的能力。
在SwiftNIO中,目前有一个 EventLoopGroup
实现,以及两个 EventLoop
实现。对于生产应用,有 MultiThreadedEventLoopGroup
,它是通过 POSXTM库(pthreads
)创建大量线程的 EventLoopGroup
,并每个线程放置一个 SelectableEventLoop
。 SelectableEventLoop
是一个使用选择器(根据目标系统可能是 kqueue
或 epoll
)管理的循环,用于从文件描述符处理I/O事件,并调度工作。此外,还有 EmbeddedEventLoop
,这是一个用于测试目的的虚拟事件循环。
EventLoop
具有许多重要特性。最重要的是,它们是SwiftNIO应用程序中所有工作的执行方式。为了确保线程安全,几乎任何要在SwiftNIO中的其他对象上执行的工作都必须通过 EventLoop
进行调度。《EventLoop
》对象拥有SwiftNIO应用程序中的几乎所有其他对象,了解它们的执行模型对于构建高性能的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
;以及EmbeddedChannel
,这是一个主要用于测试的Channel
。
关于阻塞的说明
ChannelPipeline
的一个重要特性是它们是线程安全的。这对于编写SwiftNIO应用来说非常重要,因为它允许您编写更简单的ChannelHandler
,并且知道它们不需要同步。
然而,这是通过将在同一个线程上执行的ChannelPipeline
的所有代码来实现的。这意味着,作为一般规则,ChannelHandler
**必须**在不将其派发到后台线程的情况下调用阻塞代码。如果ChannelHandler
因为任何原因而阻塞,则连接到父EventLoop
的所有Channel
都无法继续进行,直到阻塞调用完成。
这在编写SwiftNIO应用时是一个常见的问题。如果您需要使用阻塞样式编写代码,强烈建议您在管道中处理完代码后将工作派发到另一个线程。
启动程序
虽然直接使用Channel
和EventLoop
进行配置和注册是可行的,但通常有一个更高级的抽象来处理这项工作会更有用。
因此,SwiftNIO提供了一些Bootstrap
对象,其目的是简化通道的创建。一些Bootstrap
对象还提供其他功能,例如Happy Eyeballs的支持,以便进行TCP连接尝试。
目前,SwiftNIO附带三个Bootstrap
对象:ServerBootstrap
,用于启动监听通道;ClientBootstrap
,用于启动客户端TCP通道;以及DatagramBootstrap
,用于启动UDP通道。
ByteBuffer
在SwiftNIO应用程序中,大部分的工作都涉及将字节数据在缓冲区之间移动。至少,数据是通过字节数据缓冲区以网络的形式发送和接收的。因此,为了优化SwiftNIO应用程序的工作,拥有一个高性能且专门为此工作类型优化的数据结构非常重要。
因此,SwiftNIO提供了一种名为 ByteBuffer
的快速复制的字节数据缓冲区,这是许多SwiftNIO应用程序的核心构建块。
ByteBuffer
提供了许多有用的功能,并提供了一些挂钩以便在不安全模式下使用。这关闭了边界检查以改善性能,但可能使应用程序容易遭受内存正确性问题。
一般情况下,强烈建议您始终使用 ByteBuffer
的安全模式。
有关 ByteBuffer
API 的更多详细信息,请参阅以下链接的API文档。
承诺(Promises)和未来(Futures)
在编写并发代码与同步代码之间的一个主要区别是,并非所有操作都会立即完成。例如,当你在通道上写数据时,可能会出现事件循环无法立即将数据写入网络的情况。因此,SwiftNIO提供了 EventLoopPromise<T>
和 EventLoopFuture<T>
来管理最终会异步完成的操作。
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/master/Sources/NIOChatClient
- 聊天服务器 https://github.com/apple/swift-nio/tree/master/Sources/NIOChatServer
- 回声客户端 https://github.com/apple/swift-nio/tree/master/Sources/NIOEchoClient
- 回声服务器 https://github.com/apple/swift-nio/tree/master/Sources/NIOEchoServer
- UDP回声客户端 https://github.com/apple/swift-nio/tree/master/Sources/NIOUDPEchoClient
- UDP回声服务器 https://github.com/apple/swift-nio/tree/master/Sources/NIOUDPEchoServer
- HTTP客户端 https://github.com/apple/swift-nio/tree/master/Sources/NIOHTTP1Client
- HTTP服务器 https://github.com/apple/swift-nio/tree/master/Sources/NIOHTTP1Server
- WebSocket客户端 https://github.com/apple/swift-nio/tree/master/Sources/NIOWebSocketClient
- WebSocket服务器 https://github.com/apple/swift-nio/tree/master/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中略有不同。例如,如果您想依赖NIO
和NIOHTTP1
模块,请指定以下依赖项
Swift 5.0 和 5.1(《swift-tools-version:5.[01]》)
dependencies: ["NIO", "NIOHTTP1"]
Swift 5.2(《swift-tools-version:5.2》)
dependencies: [.product(name: "NIO", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio")]
使用 Xcode 包支持
如果你的项目设置为 Xcode 项目且使用 Xcode 11 或更高版本,你可以通过点击“文件” -> “包” -> “添加包依赖项”将 SwiftNIO 添加为 Xcode 项目的依赖项。在即将出现的对话框中,请输入 https://github.com/apple/swift-nio.git
并连续点击“下一步”两次。最后,选择你打算使用的目标(例如 NIO
、NIOHTTP1
和 NIOFoundationCompat
)并点击“完成”。现在你将在项目中能够使用 import NIO
(以及你选择的其它所有目标)。
要处理 SwiftNIO 本身,或调查某些示例应用程序,你可以直接克隆存储库并使用 SwiftPM 帮助构建它。例如,你可以运行以下命令来编译和运行示例 echo 服务器:
swift build
swift test
swift run NIOEchoServer
为了验证其是否正常工作,你可以使用另一个 shell 尝试连接到它。
echo "Hello SwiftNIO" | nc localhost 9999
如果一切顺利,你会看到信息被Echo回你。
要在 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
开发 SwiftNIO
注意:此部分仅适用于希望自行开发 SwiftNIO 的情况。如果您只想将 SwiftNIO 作为 SwiftPM 包使用,可忽略此处信息。
SwiftNIO 的开发过程与任何其他 SwiftPM 项目大致相同。尽管如此,我们有一些流程在您贡献之前需要了解。有关详情,请参阅此存储库中的 CONTRIBUTING.md
。
先决条件
SwiftNIO 的 master
分支是 SwiftNIO 2 下一个版本的开发分支,仅适用于 Swift 5。
要能够编译和运行 SwiftNIO 及其集成测试,您需要在系统中安装一些先决条件。
macOS
- Xcode 10.2 或更新版本,建议使用 Xcode 11。
Linux
- 从 swift.org/download 安装的 Swift 5.0、5.1 或 5.2。我们始终建议使用最新发布的版本。
- 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