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 提供-first-class 支持 |
版本: "1.0.0" |
版本: "0.1.0" |
协议实现
以下是一些使用 SwiftNIO 实现的协议列表。这是一个非详尽的列表,列出的库都在使用 SwiftNIO 以非阻塞方式执行所有 I/O。这些库既包括 SwiftNIO 项目的一部分,也已被接受进入 SSWG 的孵化过程中。
底层协议实现
底层协议实现通常是一组实现协议但仍然需要用户对SwiftNIO有良好理解的 ChannelHandler
集合。通常,底层的协议实现将被包装在具有更友好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特定知识。下面列出的实现仍然在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 的版本范围来依赖,从所需的最低版本开始,直到下一个主要版本。在 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并不仅限于提供高层解决方案,如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
,这是一个通过POSIX pthreads
库创建多个线程的EventLoopGroup
,并在每个线程上放置一个SelectableEventLoop
。SelectableEventLoop
是一个使用选择器的事件循环(根据目标系统,选择kqueue
或epoll
)来管理文件描述符上的I/O事件并调度任务的循环。此外,还有EmbeddedEventLoop
,这是一个用于测试目的的虚拟事件循环。
EventLoop
具有多个重要属性。最重要的是,它们是在SwiftNIO应用中完成所有工作的方式。为了保证线程安全,任何想要在SwiftNIO的其他几乎任何对象上执行的工作都必须通过EventLoop
来分配。大部分其他对象都由EventLoop
对象拥有,理解它们的执行模型对于构建高性能的SwiftNIO应用至关重要。
通道、通道处理器、通道流水线和通道上下文
虽然EventLoop
对SwiftNIO的工作方式至关重要,但大多数用户最多只会与之实质性地交互,要求它创建EventLoopPromise
和调度工作。用户在SwiftNIO应用中花费最多时间的部分是Channel
和ChannelHandler
。
几乎在SwiftNIO程序中,用户与之交互的每个文件描述符都与一个单独的Channel
相关联。该Channel
拥有该文件描述符,并负责管理其生命周期。它还负责处理该文件描述符上的入站和出站事件:当事件循环有与文件描述符对应的事件时,它会通知拥有该文件描述符的Channel
。
Channel
本身并没有什么用处。毕竟,很少有应用程序不想对在套接字上发送或接收的数据做任何处理!因此,Channel
的另一个重要部分就是 ChannelPipeline
。
ChannelPipeline
是一个 Channel
上的 ChannelHandler
对象序列,负责处理事件。这些 ChannelHandler
会按顺序依次处理这些事件,修改和转换它们。这可以看作是数据处理管道;因此,命名为 ChannelPipeline
。
所有 ChannelHandler
都是从 ChannelHandler
或两者都是。入站处理程序处理“入站”事件:如从套接字读取数据、读取套接字关闭或其他由远程对端发起的事件。出站处理程序处理“出站”事件,如写入、连接尝试和本地套接字关闭。
每个处理程序都按顺序处理事件。例如,读取事件将从管道的前端传递到后端,每次一个处理程序,而写入事件则从管道的后端传递到前端。每个处理程序在任何时候都可能生成入站或出站事件,并将它们发送到适当的下一个处理程序。这样,处理程序可以分割读取,合并写入,延迟连接尝试,并通常进行事件任意转换。
通常,ChannelHandler
被设计成高度可重用的组件。这意味着它们倾向于尽量小,执行一种特定的数据转换。这使得处理程序能够以新颖和灵活的方式组合在一起,有助于代码重用和封装。
ChannelHandler
可以通过使用 ChannelHandlerContext
来跟踪在 ChannelPipeline
中的位置。这些对象包含对管道中上一个和下一个通道处理程序的引用,确保 ChannelHandler
在管道中时始终可以发出事件。
SwiftNIO附带了许多用于提供有用功能(如 HTTP 解析)的内置 ChannelHandler
。此外,高性能应用程序会希望尽可能在 ChannelHandler
中实现其逻辑,因为它有助于避免上下文切换的问题。
此外,SwiftNIO 包含一些 Channel
实现。特别是,它包含 ServerSocketChannel
,这是一个用于接收入站连接的 Channel
;SocketChannel
,这是一个用于 TCP 连接的 Channel
;,这是一个用于 UDP 套接字的
Channel
;以及 EmbeddedChannel
,这是一个主要用于测试的 Channel
。
关于阻塞的说明
关于 ChannelPipeline
的重要说明之一是它们是线程安全的。这对于编写 SwiftNIO 应用程序非常重要,因为它允许您在不需要进行同步的情况下编写更简单的 ChannelHandler
。
然而,这是通过对 ChannelPipeline
上的所有代码进行调度在同一个线程上 EventLoop
来实现的。这意味着,作为一个一般规则,ChannelHandler
在没有将其调度到后台线程的情况下不得调用阻塞代码。如果 ChannelHandler
因任何原因而阻塞,则父 EventLoop
上附加的所有 Channel
都将无法进步,直到阻塞调用完成。
这在编写 SwiftNIO 应用程序时是一个常见的问题。如果您有用阻塞样式编写代码的需求,则强烈建议您在管道中完成工作后将工作调度到不同的线程。
引导
虽然可以直接使用 Channel
与 EventLoop
进行配置和注册,但通常一个更高级的抽象来处理这项工作更有用。
因此,SwiftNIO 提供了许多 Bootstrap
对象,其目的是简化通道的创建。一些 Bootstrap
对象还提供其他功能,例如对 Happy Eyeballs 的支持,以进行 TCP 连接尝试。
目前SwiftNIO自带了三个Bootstrap
对象:用于启动监听通道的ServerBootstrap
,用于启动客户端TCP通道的ClientBootstrap
,以及用于启动UDP通道的DatagramBootstrap
。
ByteBuffer
SwiftNIO应用程序中的大部分工作都涉及字节缓冲区的排序。至少,网络中的数据发送和接收都是以字节缓冲区的形式进行的。因此,拥有一个性能优良、针对SwiftNIO应用程序工作的数据结构非常重要。
因此,SwiftNIO提供了ByteBuffer
,这是一种高效的update-on-write字节缓冲区,是大多数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-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
将创建一个基础镜像,编译 SwiftNIO,并在
localhost:9999
上的一个示例NIOEchoServer
运行。通过运行echo Hello SwiftNIO | nc localhost 9999
测试它。 -
docker-compose -f docker/docker-compose.yaml up http
将创建一个基础镜像,编译SwiftNIO,并在本地主机: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