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。
底层协议实现
底层协议实现通常是一组实现协议但仍需要用户对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 时依赖一个版本范围,该范围覆盖您所需的最小 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
库)并在每个线程上放置一个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
都属于 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
相同的线程上调度所有代码来实现的。这意味着,作为一般规则,ChannelHandler
必须在调用外部线程之前将阻塞代码调度到后台线程。如果一个ChannelHandler
因任何原因而阻塞,所有连接到父EventLoop
的Channel
都将无法进展,直到阻塞调用完成。
这在编写SwiftNIO应用程序时是一个常见的关注点。如果您需要以阻塞方式写入代码,则强烈建议在您的管道中完成工作时将工作调度到不同的线程。
启动
虽然可以直接配置和注册Channel
和EventLoop
,但通常有一个更高级别的抽象来处理这项工作会更加有用。
因此,SwiftNIO提供了一些Bootstrap
对象,其目的是简化通道的创建。一些Bootstrap
对象还提供了其他功能,例如,支持Happy Eyeballs进行TCP连接尝试。
目前,SwiftNIO包括三个Bootstrap
对象:ServerBootstrap
,用于启动监听通道;ClientBootstrap
,用于启动客户端TCP通道;以及用于启动UDP通道的DatagramBootstrap
。
ByteBuffer
在SwiftNIO应用程序中,大部分的工作涉及移动字节缓冲区。至少,数据在网络中以字节缓冲区的形式发送和接收。因此,拥有一个针对SwiftNIO应用进行优化的高性能数据结构非常重要。
因此,SwiftNIO提供ByteBuffer
,这是一个快速复制字节缓冲区,它是大多数SwiftNIO应用程序的关键构件。
ByteBuffer
提供了一些有用功能,并且在Unsafe模式下使用时还提供了一些钩子。这会关闭边界检查以改善性能,但可能会让您的应用程序面临内存正确性问题。
一般来说,强烈推荐您始终以安全模式使用ByteBuffer
。
有关ByteBuffer
API的更多详细信息,请参阅下面的API文档。
承诺(Promises)和未来(Futures)
编写并发代码和同步代码之间的一个主要区别是,并非所有操作都会立即完成。例如,当您在通道上写入数据时,事件循环可能无法立即将此写入操作发送到网络。因此,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主要使用SwiftPM作为其构建工具,因此我们建议也使用它。如果要在自己的项目中依赖SwiftNIO,只需将dependencies
子句添加到您的Package.swift
即可
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-tools-version:5.2
)
Swift 5.2 (dependencies: [.product(name: "NIO", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio")]
使用Xcode包支持
如果您的项目设置为Xcode项目且您正在使用Xcode 11以上版本,您可以通过点击File -> Swift Packages -> Add Package Dependency将SwiftNIO作为依赖项添加到您的Xcode项目中。在即将出现的对话框中,请输入https://github.com/apple/swift-nio.git
并点击两次Next。最后,选择您计划使用的目标(例如 NIO
、NIOHTTP1
和 NIOFoundationCompat
)并点击完成。现在您将能够在您的项目中import NIO
(以及您选择的其它所有目标)。
要处理SwiftNIO本身或研究一些演示应用,您可以直接克隆仓库并使用SwiftPM帮助构建。例如,您可以使用以下命令编译和运行示例echo服务器:
swift build
swift test
swift run NIOEchoServer
为了验证其是否正常工作,您可以使用另一个shell尝试连接到它
echo "Hello SwiftNIO" | nc localhost 9999
如果一切正常,您将看到信息被 echoed 回来。
要在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 的情况。如果您只是想将 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