SwiftNIO
SwiftNIO 是一个跨平台、基于异步事件驱动的网络应用框架,旨在快速开发可维护的高性能协议服务器和客户端。
它类似于 Netty,但它是用 Swift 编写的。
概念概述
SwiftNIO 根本上是一个用于在 Swift 中构建高性能网络应用的底层工具。它特别针对使用“每连接一个线程”的并发模型效率低下或不切实际的那些用例。这在构建使用大量相对低利用率连接的服务器时很常见,例如 HTTP 服务器。
为了实现其目标,SwiftNIO 广泛使用“非阻塞 I/O”:因此得名!非阻塞 I/O 与较常见的阻塞 I/O 模型不同,因为应用程序不会等待数据通过网络发送或接收:相反,SwiftNIO 请求内核在 I/O 操作可以执行而不等待时通知它。
SwiftNIO 并不旨在提供类似网页框架这样的高级解决方案。相反,SwiftNIO 关注为这些高级应用提供底层构建块。在构建网页应用时,大多数用户不会想直接使用 SwiftNIO:相反,他们想使用 Swift 生态系统中可用的许多优秀的网页框架之一。然而,这些网页框架可能会选择在底层使用 SwiftNIO 来提供他们的网络支持。
以下部分将描述 SwiftNIO 提供的底层工具,并快速概述如何使用它们。如果您对这些概念感到熟悉,则可以跳过 README 的其他部分。
支持的平台
SwiftNIO旨在支持所有支持Swift的平台。目前,它已在macOS和Linux上进行开发和测试,并已知支持以下操作系统版本
- Ubuntu 14.04+
- macOS 10.12+
基本架构
SwiftNIO的基本构建块包括以下6种类型的对象
EventLoopGroup
,一个协议EventLoop
,一个协议Channel
,一个协议ChannelHandler
,一个协议Bootstrap
,多个相关结构ByteBuffer
,一个结构体EventLoopPromise
和EventLoopFuture
,两个泛型类。
所有SwiftNIO应用程序最终都是由这些各种组件构成的。
事件循环和事件循环组
SwiftNIO的基本I/O原语是事件循环。事件循环是一个等待事件(通常是I/O相关事件,例如“收到数据”)发生的对象,当事件发生时触发某种类型的回调。在几乎所有SwiftNIO应用程序中,都会相对较少的事件循环:通常每个应用希望使用的CPU核心只有一个或两个。一般来说,事件循环运行于整个应用程序的生命周期,不断循环分发事件。
事件循环被汇聚为事件循环组。这些组提供了一种将工作分配到事件循环上的机制。例如,当监听传入的连接时,监听套接字将注册在一个事件循环上。然而,我们不想将监听套接字上接受的所有连接都注册在同一事件循环上,因为这可能会将一个事件循环过载,而使其他事件循环闲置。因此,事件循环组提供了将负载分散到多个事件循环上的能力。
在今天,SwiftNIO中有一个EventLoopGroup
实现和两个EventLoop
实现。对于生产应用程序,有一个MultiThreadedEventLoopGroup
,这是一个在POSIX pthreads
库中创建多个线程的EventLoopGroup
,并为每个线程放置一个SelectableEventLoop
。可选择的SelectableEventLoop
是一个使用选择器(取决于目标系统的kqueue
或epoll
)来管理文件描述符的I/O事件并派发工作的事件循环。此外,还有一个EmbeddedEventLoop
,这是一个主要用于测试目的的虚拟事件循环。
事件循环有多个重要属性。最重要的是,它们是以所有工作在SwiftNIO应用程序中完成的方式。为了确保线程安全,几乎所有其他对象上的工作都需要通过一个EventLoop
来派发。几乎在SwiftNIO应用程序中的所有其他对象都属于EventLoop
,理解它们的执行模型对于构建高性能的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 随附了许多提供有用功能(如 HTTP 解析)的内置 ChannelHandler
。此外,高性能应用程序希望在 ChannelHandler
中尽可能多地将它们的逻辑提供出来,因为它有助于避免上下文切换问题。
此外,SwiftNIO自带了一些Channel
实现。特别是,它自带了ServerSocketChannel
,这是一个用于接收传入连接的Channel
;SocketChannel
,这是一个用于TCP连接的Channel
;DatagramChannel
,这是一个用于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
,用于启动监听通道;ClientBootstrap
,用于启动客户端TCP通道;以及DatagramBootstrap
,用于启动UDP通道。
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 code> 之间的差异。例如,传递给
close
的承诺将在 Channel
关闭后、ChannelPipeline
完全清除之前成功。如果需要在对 ChannelPipeline
完全清除之前采取行动,这将非常有用。如果想要等待 Channel
关闭和对 ChannelPipeline
进行清除而无需采取任何进一步的操作,则最好是等待 closeFuture
成功。
根据您想要以何种方式和何时执行它们,有一些功能可以应用回调到 EventLoopFuture<T>
。有关这些函数的详细信息,请参阅 API 文档。
设计哲学
SwiftNIO被设计为一个强大的工具,用于构建网络应用程序和框架,但并非旨在为所有抽象级别提供完美解决方案。SwiftNIO专注于在较低抽象级别提供基本的I/O原语和协议实现,将更易于表达但较慢的抽象留给更广泛的社区去构建。其意图是使SwiftNIO成为服务器端应用程序的构建块,而不是这些应用程序直接使用的框架。
需要从其网络堆栈获得极高性能的应用程序可以选择直接使用SwiftNIO,以减少其抽象的开销。这些应用程序应该能够在相对较低的管理成本下保持极高的性能。SwiftNIO还专注于为此用例提供有用的抽象,以便可以直接构建性能极高的网络服务器。
SwiftNIO的核心仓库将包含一些非常重要的协议实现,如HTTP,直接包含在树中。然而,我们认为大多数协议实现应与基础网络堆栈的发布周期解耦,因为其发布周期可能非常不同(要么快得多,要么慢得多)。因此,我们积极鼓励社区在不脱离树的情况下开发和维护他们的协议实现。实际上,一些第一方SwiftNIO协议实现,包括我们的TLS和HTTP/2绑定,都是在树外开发的!
有用的协议实现
以下项目包含在SwiftNIO树外使用的有用协议实现:
文档
示例用法
目前有几个示例项目演示了如何使用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来辅助构建。例如,您可以运行以下命令来编译和运行示例echo服务器:
swift build
swift test
swift run NIOEchoServer
要验证它是否正在工作,您可以使用另一个shell尝试连接到它。
echo "Hello SwiftNIO" | nc localhost 9999
如果一切顺利,您会看到消息被回显。
要生成Xcode项目以便在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
将创建一个包含Swift运行时和其他构建和测试依赖项的基础镜像,编译SwiftNIO,并在
localhost:9999
上运行一个示例NIOEchoServer
。通过运行echo Hello SwiftNIO | nc localhost 9999
进行测试。 -
docker-compose -f docker/docker-compose.yaml up http
将创建一个包含Swift运行时和其他构建和测试依赖项的基础镜像,编译SwiftNIO,并在
localhost:8888
上运行一个示例NIOHTTP1Server
。通过运行curl http://localhost:8888
进行测试。
开发SwiftNIO
大体上,SwiftNIO的开发与其他任何SwiftPM项目一样简单。但是,我们有一些流程在您贡献之前值得了解。请参阅此仓库中的CONTRIBUTING.md
以获取详细信息。