SwiftNIO
SwiftNIO是一个支持跨平台异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。
它类似于Netty,但是是用Swift编写的。
概念概述
SwiftNIO本质上是用于在Swift中构建高性能网络应用的底层工具。它特别针对那些使用“每个连接一个线程”的并发模型效率低下或不切合实际的场景。这是在构建使用大量相对低利用率的连接的服务器时的一个常见限制,例如HTTP服务器。
为了实现其目标,SwiftNIO广泛使用了“非阻塞I/O”:这就是其名称的由来!非阻塞I/O与更常见的阻塞I/O模型的不同之处在于,应用不会等待数据被发送到网络或接收来自网络:相反,SwiftNIO请求内核在I/O操作可以不需要等待的情况下通知它。
SwiftNIO并不旨在提供类似网络框架那样的高级解决方案。相反,SwiftNIO专注于为这些高级应用提供底层构建块。当涉及到构建Web应用时,大多数用户不会想直接使用SwiftNIO:相反,他们想使用Swift生态系统中可用的许多优秀的Web框架之一。然而,那些Web框架可能会在内部选择使用SwiftNIO来提供他们的网络支持。
以下部分将描述SwiftNIO提供的底层工具,并提供如何使用它们的快速概述。如果您对这些概念感到舒适,可以直接跳到本README的其它部分。
支持的平台
SwiftNIO旨在支持Swift支持的所有平台。目前,它是在macOS和Linux上开发和测试的,并且已知支持以下操作系统版本
- Ubuntu 14.04+
- macOS 10.12+
基本架构
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》 的 《EventLoopGroup》。《SelectableEventLoop》 是一个使用选择器(根据目标系统,可能是 《kqueue》 或 《epoll》)来管理文件描述符上的 I/O 事件和分派工作的事件循环。另外,还有一个 《EmbeddedEventLoop》,它是一个主要用于测试目的的傀儡事件循环。
《EventLoop》 具有许多重要的特性。最重要的是,它们是 SwiftNIO 应用中所有工作执行的方式。为了确保线程安全,几乎所有在 SwiftNIO 其他对象上要执行的工作都必须通过 《EventLoop》 分派。 《EventLoop》 对象拥有 SwiftNIO 应用中的几乎所有其他对象,理解它们的执行模型对于构建高性能的 SwiftNIO 应用至关重要。
Channels, Channel Handlers, Channel Pipelines, and Channel Contexts
虽然 《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
的引用,确保 ChannelHandler
在管道中始终可以发出事件。
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 应用程序时是一个常见问题。如果在您的管道中完成代码后要以阻塞的方式编写代码,非常建议您在淡出时将其调度到不同的线程。
引导程序(Bootstrap)
虽然直接使用 EventLoop
配置和注册 Channel
是可能的,但通常更有用,因为有一个高级抽象来处理这项工作。
为此,SwiftNIO 提供了一系列 Bootstrap
对象,其目的是简化通道的创建。一些 Bootstrap
对象还提供其他功能,例如通过 Happy Eyeballs 支持制作 TCP 连接尝试。
目前,SwiftNIO 包含了三个 Bootstrap
对象:用于引导监听通道的 ServerBootstrap
、用于引导客户端 TCP 通道的 ClientBootstrap
和用于引导 UDP 通道的 DatagramBootstrap
。
ByteBuffer
在SwiftNIO应用中,大部分的工作涉及对字节数据缓冲区的操作。至少,数据以字节数据缓冲区的形式在网络中发送和接收。因此,拥有一个针对SwiftNIO应用执行的工作类型进行了优化的高性能数据结构是非常重要的。
因此,SwiftNIO提供了ByteBuffer
,这是一个快速复制写入的字节数据缓冲区,是大多数SwiftNIO应用的关键构建块。
ByteBuffer
提供了一系列有用的功能,此外还提供了一系列钩子,在“不安全”模式下使用它。这关闭了边界检查,从而提高了性能,但可能会使您的应用程序面临潜在的内存正确性问题。
通常,强烈建议您始终以安全模式使用ByteBuffer
。
有关ByteBuffer
API的更多详情,请参阅下述链接中的API文档。
承诺和未来
编写并发代码与编写同步代码的一个主要区别是,并非所有操作都会立即完成。例如,当您在通道上写入数据时,事件循环可能无法立即将数据写入网络。因此,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还专注于为此用例提供有用的抽象,以便可以直接构建性能极高的网络服务器。
SwiftNIO核心仓库将包含一些直接在树中的极其重要的协议实现,例如HTTP。然而,我们认为大多数协议实现应该与底层网络堆栈的发布周期解耦,因为发布节奏可能会有很大不同(要么快得多,要么慢得多)。为此,我们积极鼓励社区在树外开发和维护其协议实现。实际上,一些SwiftNIO协议实现(包括我们的TLS和HTTP/2绑定)都是树外开发的!
有用的协议实现
以下项目包含在SwiftNIO树外的不在树中的有用协议实现:
- 与OpenSSL兼容库的TLS绑定: swift-nio-ssl
- SwiftNIO的HTTP/2支持: swift-nio-http2
- Network.framework 在 iOS、tvOS 和 macOS 上对 SwiftNIO 的支持:swift-nio-transport-services
- 关于 SwiftNIO 的实用代码:swift-nio-extras
文档
示例用法
目前有几个示例项目展示了如何使用 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
- HTTP 服务器 https://github.com/apple/swift-nio/tree/master/Sources/NIOHTTP1Server
入门指南
SwiftNIO 主要使用 SwiftPM 作为其构建工具,所以我们建议你也使用这个。如果你想在你的项目中依赖 SwiftNIO,只需要在你的 Package.swift
中添加一个 dependencies
子句
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "1.0.0")
]
然后将适当的 SwiftNIO 模块添加到你的目标依赖项中。
要处理 SwiftNIO 本身或调查一些示例应用程序,可以直接克隆存储库,并使用 SwiftPM 帮助构建。例如,你可以运行以下命令来编译并运行示例回声服务器
swift build
swift test
swift run NIOEchoServer
要验证其是否正常工作,你可以使用另一个 shell 尝试连接到它
echo "Hello SwiftNIO" | nc localhost 9999
如果一切顺利,你应该会看到消息被回显回来。
为在 Xcode 中工作于 SwiftNIO 生成 Xcode 项目
swift package generate-xcodeproj
这会使用 SwiftPM 生成一个 Xcode 项目。你可以使用以下命令打开项目
open swift-nio.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 的开发过程和其他任何 SwiftPM 项目的开发一样简单。也就是说,我们有一些过程在您贡献之前值得了解。详细信息请参见此仓库中的 CONTRIBUTING.md
。
先决条件
为了能够编译和运行 SwiftNIO 以及集成测试,您需要在系统上安装一些先决条件。
macOS
- Xcode 9 或更高版本
Linux
- Swift 4.0 或更高版本
- zlib 及其开发头文件
- netcat(仅用于集成测试)
- lsof(仅用于集成测试)
- shasum(仅用于集成测试)
Ubuntu
# install swift tarball from https://swift.org/downloads
apt-get install -y zlib1g-dev netcat-openbsd lsof perl
Fedora 28+
dnf install swift-lang zlib-devel /usr/bin/nc /usr/bin/lsof /usr/bin/shasum