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项目 |
高级实现
高级实现通常是带有不暴露SwiftNIO的 ChannelPipeline
的API的库,因此可以发挥很少(或没有)SwiftNIO特定知识的优势。以下列出的实现仍然使用SwiftNIO进行所有的I/O操作,并与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开始的所有版本的SwiftNIO(不包括3.0.0)。SemVer和SwiftNIO的Public API保证应确保程序能正常工作,而不必担心每个版本的兼容性测试。
概念概述
SwiftNIO本质上是一个用于在Swift中构建高性能网络应用的底层工具。它特别针对那些使用“线程模型”的并发效率低下或不切实际的情况。这通常是构建服务器时使用大量相对低利用率连接时的常见限制,例如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
,这是一种EventLoopGroup
,它会创建一定数量的线程(使用POSIX的pthreads
库),并在每个线程上放置一个SelectableEventLoop
。该SelectableEventLoop
是一个事件循环,它使用选择器(根据目标系统为kqueue
或epoll
)来管理文件描述符的I/O事件并进行工作调度。此外,还存在一个EmbeddedEventLoop
,这是一个用于测试目的的虚拟事件循环。
EventLoop
具有许多重要属性。最重要的是,它们是SwiftNIO应用程序中所有工作的完成方式。为了确保线程安全,几乎所有想在不同对象上执行的任务都必须通过EventLoop
来调度。EventLoop
对象拥有SwiftNIO应用程序中几乎所有其他对象,理解它们的执行模型对于构建高性能的SwiftNIO应用程序至关重要。
通道、通道处理器、通道流水线以及通道上下文
EventLoop
对于SwiftNIO的工作方式至关重要,但大多数用户不会实质性地与它们进行交互,而只是在请求创建EventLoopPromise
和安排工作。大多数用户将花费最多时间与之交互的SwiftNIO应用程序的部分是Channel
和ChannelHandler
。
在SwiftNIO程序中,几乎每个与用户交互的文件描述符都关联着一个单个的Channel
。该Channel
拥有该文件描述符,并负责管理其生命周期。它还负责处理该文件描述符的入站和出站事件:每当事件循环有与文件描述符对应的事件时,它都会通知拥有该文件描述符的Channel
。
然而,仅靠Channel
本身并没有什么用处。毕竟,很少有应用程序不希望对通过套接字发送或接收的数据做点什么!因此,Channel
的另一个重要部分是ChannelPipeline
。
一个 ChannelPipeline
是一系列对象,被称为 ChannelHandler
,这些对象在 Channel
上处理事件。这些 ChannelHandler
依次按顺序处理事件,修改并转换事件。这可以被视为一个数据处理管道;因此命名为 ChannelPipeline
。
所有 ChannelHandler
要么是 inbound 或 outbound 处理器,或者两者都是。inbound 处理器处理“进入”事件:如从套接字读取数据、读取套接字关闭或其他由远程对端发起的事件。Outbound 处理器处理“外出”事件,例如写入、连接尝试和本地套接字关闭。
每个处理器按顺序处理事件。例如,读取事件从前端管道传递到后端,一次一个处理器,而写入事件则从管道后端传递到前端。每个处理器在任何时候都可以生成 inbound 或 outbound 事件,这些事件将以适当方向发送给下一个处理器。这允许处理器分解读取、合并写入、延迟连接尝试,通常对事件执行任意转换。
一般来说,ChannelHandler
被设计成高度可重用组件。这意味着它们往往设计得很小,执行一个特定的数据转换。这允许以新的、灵活的方式组合处理器,有助于代码重用和封装。
ChannelHandler
可以通过使用 ChannelHandlerContext
来跟踪它们在 ChannelPipeline
中的位置。这些对象包含对管道中前一个和下一个通道处理器的引用,确保即使处理器仍然在管道中也可以随时生成事件。
SwiftNIO内置了许多 ChannelHandler
,例如 HTTP 解析,提供了有用的功能。此外,高性能应用程序会想尽可能将它们的逻辑放在 ChannelHandler
中,因为它有助于避免上下文切换问题。
此外,SwiftNIO还附带了一些 Channel
实现。特别是,它包括 ServerSocketChannel
,一个用于接受进入连接的 Channel
;SocketChannel
,一个用于 TCP 连接的 Channel
;DatagramChannel
,一个用于 UDP 套接字的 Channel
;以及 EmbeddedChannel
,一个主要用于测试的 Channel
。
关于阻塞的注意事项
关于 ChannelPipeline
的重要注意事项之一是它们是线程安全的。这对于编写 SwiftNIO 应用程序来说非常重要,因为它允许你编写更加简单的 ChannelHandler
,并且知道它们不需要同步。
然而,这是通过将所有代码调度到与 EventLoop
相同的线程上的 ChannelPipeline
来实现的。这意味着,作为一个一般规则,ChannelHandler
必须 不要在不将其调度到后台线程的情况下调用阻塞代码。如果 ChannelHandler
因任何原因而阻塞,则附加到父 EventLoop
的所有 Channel
都无法在阻塞调用完成之前进步。
这在编写 SwiftNIO 应用程序时是一个常见的问题。如果你需要以阻塞方式编写代码,强烈建议你在完成管道中的工作后,将工作调度到不同的线程。
Bootstrap
虽然可以直接使用 EventLoop
配置和注册 Channel
,但通常有一个更高层次的抽象来处理这项工作会更加有用。
因此,SwiftNIO 提供了一系列 Bootstrap
对象,其目的是简化通道创建的过程。一些 Bootstrap
对象还提供了其他功能,例如支持 Happy Eyeballs 来进行 TCP 连接尝试。
目前,SwiftNIO 附带三个 Bootstrap
对象:用于配置监听通道的 ServerBootstrap
、用于配置客户端 TCP 通道的 ClientBootstrap
以及用于配置 UDP 通道的 DatagramBootstrap
。
ByteBuffer
在 SwiftNIO 应用程序中的大部分工作都涉及在字节缓冲区中进行操作。至少,数据是以缓冲区形式发送和接收的,形成为网络。因此,拥有一个针对 SwiftNIO 应用程序执行的高性能数据结构非常重要。
因此,SwiftNIO 提供了 ByteBuffer
,这是一个快速复制-on-write 字节缓冲区,是大多数 SwiftNIO 应用程序的关键构建块。
ByteBuffer
提供了一系列有用的功能,并且还提供了一些钩子来以“不安全”模式使用它。这关闭了边界检查以提升性能,但可能会让你的应用程序暴露于潜在的内存正确性问题。
通常情况下,强烈建议您始终在安全模式下使用ByteBuffer
。
有关ByteBuffer
API的更多详情,请参阅以下链接中的API文档。
承诺和未来
编写并发代码与编写同步代码的一个主要区别是,并非所有操作都会立即完成。例如,当你在一个通道上写入数据时,事件循环可能无法立即将此写入操作刷新到网络中。因此,SwiftNIO提供了EventLoopPromise<T>
和EventLoopFuture<T>
来管理那些将异步完成的操作。
EventLoopFuture<T>
本质上是一个在未来某个时间点填充返回值的函数的容器。每个EventLoopFuture<T>
都有一个对应的EventLoopPromise<T>
,这是将结果放入的对象。当承诺成功时,未来将会实现。
如果你必须轮询未来以检测其何时完成,那会很低效,因此EventLoopFuture<T>
被设计为有管理的回调。基本上,你可以在未来挂起回调,当结果可用时执行这些回调。甚至EventLoopFuture<T>
还会仔细安排调度,以确保这些回调总是最初创建承诺的事件循环上执行,这有助于确保你不需要在EventLoopFuture<T>
回调周围进行过多的同步。
需要考虑的另一个重要话题是,向 close
传入的 Promise 与在 Channel
上的 closeFuture
之间的区别。例如,通过 close
传入的 Promise 在 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主要使用Swift PM作为其构建工具,因此我们也建议使用该工具。如果您想在自己的项目中依赖SwiftNIO,只需在Package.swift
中添加一个dependencies
子句即可
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0")
]
并将适当的SwiftNIO模块添加到您的目标依赖项中。添加目标依赖项的语法在不同的Swift版本中略有不同。例如,如果您想依赖NIO
和NIOHTTP1
模块,请指定以下依赖项
swift-tools-version:5.[01]
)
Swift 5.0和5.1(dependencies: ["NIO", "NIOHTTP1"]
swift-tools-version:5.2
)
Swift 5.2 (dependencies: [.product(name: "NIO", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio")]
使用Xcode包支持
如果您的项目设置为Xcode项目,并且正在使用Xcode 11或更高版本,您可以通过点击“文件” -> “Swift包” -> “添加包依赖项”将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
如果一切顺利,您将看到消息被回显给您。
要在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
将会创建一个包含Swift运行时和其他构建和测试依赖的基本镜像,编译SwiftNIO,并在本地的9999端口上运行示例
NIOEchoServer
。通过执行echo Hello SwiftNIO | nc localhost 9999
来测试。 -
docker-compose -f docker/docker-compose.yaml up http
将会创建一个包含Swift运行时和其他构建和测试依赖的基本镜像,编译SwiftNIO,并在本地的8888端口上运行示例
NIOHTTP1Server
。通过执行curl http://localhost:8888
来测试。
开发 SwiftNIO
注意:本节仅适用于您想自己开发 SwiftNIO 的情况。如果您只想作为 SwiftPM 包使用 SwiftNIO,则可以忽略此处信息。
就大多数方面而言,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