- 后端
- 网络
【免费下载链接】swift-nio
Event-driven network application framework for high performance protocol servers & clients, non-blocking.
导读
本文是 NIOCore 官方文档 中swift-concurrency专题的深度解读,系统讲解 SwiftNIO 事件驱动网络框架与 Swift 原生并发(async/await、Actor、结构化并发)之间的互操作方案。NIO 诞生于 Swift 语言原生并发支持出现之前,因此早期只能依靠回调与 Future/Promise 解决异步问题;随着 Swift Concurrency 的成熟,NIO 陆续引入了EventLoopFuture.get()、NIOAsyncSequenceProducer、NIOAsyncWriter、NIOAsyncChannel以及异步版 Bootstrap 等机制。读完本文,你将掌握:如何在async上下文中安全地等待 Future 结果、如何用NIOAsyncChannel把双向流式 Channel 桥接为可for try await消费的 AsyncSequence、如何用异步ServerBootstrap/ClientBootstrap编写纯 Swift Concurrency 风格的 TCP 服务端与客户端,以及如何通过类型化的 ALPN / HTTP Upgrade 处理器处理动态协议协商。
背景:为什么 NIO 需要与 Swift Concurrency 互操作
SwiftNIO 在 2016 年前后诞生,彼时 Swift 还没有async/await与 Actor 等原生并发原语。为了在单线程 EventLoop 上实现高性能非阻塞 IO,NIO 设计了自己的异步模型:
- EventLoop:绑定线程的串行执行环境,所有 Channel 事件都在其上调度;
- Channel与ChannelPipeline:双向流式管道,数据以
ChannelHandler链的方式逐级处理; - EventLoopFuture / EventLoopPromise:回调风格的异步结果容器。
这些抽象在回调时代工作得很好,但与 Swift Concurrency 的协作却需要专门设计的"桥"。NIO 官方文档 swift-concurrency 明确说明了这一动机:NIO 的 Channel 事件系统与 Swift 并发原语之间的互操作要"尽可能简单",因此 NIO 引入了一系列桥接类型。这些类型可以按难度分为三层:
- Future/Promise 桥:让
async函数可以等待 NIO Future; - Channel 桥:把双向流式管道包装成可消费、可写入并带背压的并发友好接口(
NIOAsyncChannel系列); - 异步 Bootstrap 与动态管线配置:让服务端/客户端的启动与协议协商直接在
async上下文中完成。
第一座桥:EventLoopFuture / EventLoopPromise
文档介绍的第一组桥接方法是EventLoopFuture/get()与EventLoopPromise/completeWithTask(_:),二者定义于 AsyncAwaitSupport.swift 中:
get():在async函数中阻塞等待一个 Future 的结果。其实现(见AsyncAwaitSupport.swift第 92-103 行)通过withUnsafeThrowingContinuation在 Future 完成时恢复协程,Value必须满足Sendable约束。completeWithTask(_:):反向桥接——接受一个@Sendable () async throws -> Value闭包,在内部创建一个非结构化 Task执行它,成功时succeed(value)、失败时fail(error)(第 167-178 行)。因此文档特别提示:"completeWithTask方法在底层创建了一个非结构化任务"。
基础示例:async 等待与任务补全
let eventLoop: EventLoop let promise = eventLoop.makePromise(of: Bool.self) promise.completeWithTask { try await Task.sleep(for: .seconds(1)) return true } let result = try await promise.futureResult.get()这段代码展示了双向桥接的完整闭环:completeWithTask把一段async代码的结果"灌入" Promise,而get()把 Future 的结果在async上下文中取回。
关于取消的警告(务必阅读)
Warning:
EventLoopFuture/get()不支持任务取消。如果你需要在任务被取消时及时返回,请改用getAbandoningOnCancel():它会在 Task 被取消时抛出CancellationError,但这只是"放弃"(abandon)该 Future——底层操作很可能仍在继续执行并占用资源。
从源码可以更精确地理解二者区别(AsyncAwaitSupport.swift第 105-130 行):
getAbandoningOnCancel()内部先把自身 Futurecascade(to: promise)转发到一个新 Promise,再用withTaskCancellationHandler包裹;一旦收到取消信号,立即promise.fail(CancellationError()),从而保证协程及时返回;- 但它无法真正取消底层操作——文档注释明确警告:取消发生时操作"很可能仍在进行(holding on to its resources)",只是其结果会被丢弃;
- 若取消与 Future 完成发生竞态,则二者皆可能先发生,返回值可能是 Future 的结果,也可能是
CancellationError。
因此:对延迟敏感的代码请使用getAbandoningOnCancel(),而非get()。
第二座桥:把 Channel 桥接进 Swift Concurrency
EventLoopFuture桥只适用于"请求-响应"式接口。而 NIO 的Channel内嵌ChannelPipeline,本质是一个双向流式管道(bi-directional streaming pipeline)。把这样的管道桥接进 Concurrency,需要满足一个核心约束:必须维持 Channel 的背压(back pressure)与可写性(writability)保证。
为此 NIO 引入了一组基础类型,再在其上封装出面向用户的NIOAsyncChannel:
| 类型 | 作用 | 方向 |
|---|---|---|
NIOThrowingAsyncSequenceProducer/NIOAsyncSequenceProducer | 类似AsyncStream的异步序列,在同步生产者与异步消费者之间提供带背压的桥 | 入站(读) |
NIOAsyncWriter(NIOAsyncChannelOutboundWriter的底层) | 把异步生产者桥接到同步消费者,同样支持背压 | 出站(写) |
NIOAsyncChannel | 包装Channel,把读写两侧统一暴露给 Swift Concurrency | 双向 |
NIOAsyncSequenceProducer:带背压的入站桥
NIOAsyncSequenceProducer与NIOThrowingAsyncSequenceProducer是高度可配置、高度泛化的异步序列,用途与 Swift 标准库的AsyncStream类似,但针对 NIO 的 Channel 场景做了背压优化。
在NIOAsyncChannel中,入站流NIOAsyncChannelInboundStream实际由NIOThrowingAsyncSequenceProducer支撑(见 AsyncChannelInboundStream.swift 第 20-26 行)。默认使用HighLowWatermark高低水位背压策略(lowWatermark: 2, highWatermark: 10),该策略定义于 NIOAsyncSequenceProducerStrategies.swift:
- yield 时:只要缓冲元素数未达
highWatermark,就继续向生产者索要更多元素; - consume 时:缓冲元素数跌回
lowWatermark以下,才重新恢复对生产者的需求(第 38-50 行)。
这套机制与 Channel 的read()/ 自动读配合,把网络读取节奏与消费者消费节奏绑定在一起,避免无限缓冲。
文档建议:这类类型永远不要直接暴露在公开 API 中,而应包裹进你自己的异步序列。理由是它们"高度可配置且泛化",性能优秀但使用门槛高——对外暴露会增加 API 表面积并限制未来的演进空间。
NIOAsyncChannelOutboundWriter:带背压的出站桥
出站侧由NIOAsyncChannelOutboundWriter(基于NIOAsyncWriter)承担。它的write/write(contentsOf:)方法会把消息写入 ChannelPipeline 并立即flush;当底层 Channel 不可写(例如对端消费不过来)时,调用会挂起,直到 Channel 恢复可写再继续——这就是背压的体现(AsyncChannelOutboundWriter.swift 第 113-152 行)。文档特别强调,yield(contentsOf:)的挂起能力使消费者可以"暂停生产者"。
NIOAsyncChannel:把读写两侧统一包装
NIOAsyncChannel<Inbound, Outbound>(见 AsyncChannel.swift)是面向用户的最终形态,其头注释明确列出了它的能力边界:
提供的能力:读操作呈现为AsyncSequence;写操作通过带背压的 writer 以async函数完成;Channel 可无缝关闭。不提供的能力:用户事件(user events)、传统的 NIO 背压信号(writability 信号与 channel 的 read 调用)。
NIOAsyncChannel的Configuration提供两个可调参数(AsyncChannel.swift第 37-74 行):
| 参数 | 默认值 | 含义 |
|---|---|---|
backPressureStrategy | HighLowWatermark(lowWatermark: 2, highWatermark: 10) | 入站流的背压策略 |
isOutboundHalfClosureEnabled | false | 是否启用出站半关闭;当 writer 被 finish 或析构时触发 |
包装时机至关重要:init(wrappingChannelSynchronously:configuration:)必须在 Channel 的 EventLoop 上调用(否则会触发preconditionInEventLoop()崩溃)。在AsyncChannel.swift第 470-499 行可以看到,该初始化会向 Pipeline 末尾同步添加一个NIOAsyncChannelHandler,分别桥接读写两侧,并让这两个 handler 协作,在读写都结束后关闭 Channel。文档警告了两种容易"丢读"(lose reads)的时机:
- ServerBootstrap 新建的入站连接:Channel 一旦注册 IO 就可能开始产生读取,而注册发生在 channel initializer 之后——所以必须在注册 IO 之前包装 Channel;
- 协议协商场景:ALPN/HTTP Upgrade 处理器通常要等交换若干数据后才决定协议,随后修改 Pipeline 并追加对应 handler——此时包装
NIOAsyncChannel的时机同样必须精确,否则会丢失协商期间的数据。
包装现有 Channel 并回显
let channel = ... let asyncChannel = try NIOAsyncChannel<ByteBuffer, ByteBuffer>(wrappingChannelSynchronously: channel) try await asyncChannel.executeThenClose { inbound, outbound in for try await inboundData in inbound { try await outbound.write(inboundData) } }executeThenClose是推荐的作用域式用法:闭包结束后底层 Channel 会被关闭(AsyncChannel.swift第 282-324 行),无论闭包成功还是抛出错误都会执行outbound.finish()并关闭 Channel,从而避免资源泄漏。这与早期基于inbound/outbound属性 + deinit 清理的旧 API 不同——旧 API 在AsyncChannel.swift中已被标记为 deprecated,提示改用executeThenClose。
第三部分:异步 Bootstrap 方法
为了避免上述"丢读"问题,并让 NIO 在 Swift Concurrency 中的使用"无缝",各类 Bootstrap 都新增了泛型异步方法。下面分别给出 TCP 服务端与客户端的完整实现。
ServerBootstrap:异步 TCP 服务端
ServerBootstrap的bind异步重载(Bootstrap.swift 第 538-551 行)接受host/port与一个childChannelInitializer闭包,返回NIOAsyncChannel<Output, Never>——服务端 Channel 没有出站概念,因此出站类型恒为Never:
let serverChannel = try await ServerBootstrap(group: eventLoopGroup) .bind( host: "127.0.0.1", port: 1234 ) { childChannel in // This closure is called for every inbound connection childChannel.eventLoop.makeCompletedFuture { return try NIOAsyncChannel<ByteBuffer, ByteBuffer>( synchronouslyWrapping: childChannel ) } } try await withThrowingDiscardingTaskGroup { group in try await serverChannel.executeThenClose { serverChannelInbound in for try await connectionChannel in serverChannelInbound { group.addTask { do { try await connectionChannel.executeThenClose { connectionChannelInbound, connectionChannelOutbound in for try await inboundData in connectionChannelInbound { // Let's echo back all inbound data try await connectionChannelOutbound.write(inboundData) } } } catch { // Handle errors } } } } }结构解读:
bind的 trailing closure 对每个入站连接执行,返回的serverChannel其入站元素类型是NIOAsyncChannel(每个连接一个子 Channel),出站类型是Never;- 外层
for try await connectionChannel迭代接收新连接,每个连接由group.addTask派生独立子任务处理; - 内层
executeThenClose回显数据:入站逐帧读取、出站逐帧写回。
Important: 必须使用discarding task group(
withThrowingDiscardingTaskGroup)。普通任务组不会自动回收已完成的子任务,会导致内存泄漏——这正是 SwiftNIO 官方文档反复强调的要点。
ClientBootstrap:异步 TCP 客户端
ClientBootstrap.connect的异步重载(Bootstrap.swift第 1286-1301 行)返回channelInitializer闭包的输出类型:
let clientChannel = try await ClientBootstrap(group: eventLoopGroup) .connect( host: "127.0.0.1", port: 1234 ) { channel in channel.eventLoop.makeCompletedFuture { return try NIOAsyncChannel<ByteBuffer, ByteBuffer>( wrappingChannelSynchronously: channel ) } } try await clientChannel.executeThenClose { inbound, outbound in try await outbound.write(ByteBuffer(string: "hello")) for try await inboundData in inbound { print(inboundData) } }与传统的connect(返回EventLoopFuture<Channel>)不同,这里的connect在注册 IO 之前运行 channel initializer 并完成包装,从根本上规避了丢读问题。客户端发送 "hello" 后持续打印服务端回显,直到连接关闭。
动态管线修改:类型化的协议协商与升级
异步 Bootstrap 在编译期即可确定 Channel 类型时非常好用。但有些场景的类型只能在运行时决定,例如:
- ALPN(Application-Layer-Protocol-Negotiation,TLS 应用层协议协商);
- HTTP 协议升级(HTTP/1.1 Upgrade header)。
为支持这些场景,动态配置管线的 ChannelHandler 必须携带类型信息,让运行时能确定管线最终被配置成了什么形态。NIO 为此引入了一组类型化(typed)Handler 与对应的管线配置方法,全部泛化于升级/协商结果类型之上,从而允许用户对结果做穷尽式switch:
NIOTypedApplicationProtocolNegotiationHandler—— TLS 场景的 ALPN 处理;NIOTypedHTTPServerUpgradeHandler与configureUpgradableHTTPServerPipeline—— 服务端 HTTP 升级(实现在 HTTPTypedPipelineSetup.swift);NIOTypedHTTPClientUpgradeHandler与configureUpgradableHTTPClientPipeline—— 客户端 HTTP 升级(实现在 NIOTypedHTTPClientUpgradeHandler.swift)。
实战:客户端 WebSocket 升级
下面是一个完整的客户端 WebSocket 升级示例,展示如何组合NIOTypedHTTPClientUpgradeConfiguration、NIOTypedWebSocketClientUpgrader与NIOAsyncChannel:
enum UpgradeResult { case websocket(NIOAsyncChannel<WebSocketFrame, WebSocketFrame>) case notUpgraded } let upgradeResult: EventLoopFuture<UpgradeResult> = try await ClientBootstrap(group: eventLoopGroup) .connect( host: "127.0.0.1", port: 1234 ) { channel in channel.eventLoop.makeCompletedFuture { // Configure the websocket upgrader let upgrader = NIOTypedWebSocketClientUpgrader<UpgradeResult>( upgradePipelineHandler: { channel, _ in // This configures the pipeline after the websocket upgrade was successful. // We are wrapping the pipeline in a NIOAsyncChannel. channel.eventLoop.makeCompletedFuture { let asyncChannel = try NIOAsyncChannel<WebSocketFrame, WebSocketFrame>(wrappingChannelSynchronously: channel) return UpgradeResult.websocket(asyncChannel) } } ) var headers = HTTPHeaders() headers.add(name: "Content-Type", value: "text/plain; charset=utf-8") headers.add(name: "Content-Length", value: "0") let requestHead = HTTPRequestHead( version: .http1_1, method: .GET, uri: "/", headers: headers ) let clientUpgradeConfiguration = NIOTypedHTTPClientUpgradeConfiguration( upgradeRequestHead: requestHead, upgraders: [upgrader], notUpgradingCompletionHandler: { channel in channel.eventLoop.makeCompletedFuture { return UpgradeResult.notUpgraded } } ) let upgradeResult = try channel.pipeline.syncOperations.configureUpgradableHTTPClientPipeline( configuration: .init(upgradeConfiguration: clientUpgradeConfiguration) ) return upgradeResult } }代码要点:
NIOTypedHTTPClientUpgradeConfiguration<UpgradeResult>的三个字段(NIOTypedHTTPClientUpgradeHandler.swift 第 45-65 行):upgradeRequestHead(激活后发送的初始请求头)、upgraders(候选升级器数组,至少一个,否则触发precondition)、notUpgradingCompletionHandler(确定不发生升级时的兜底闭包);NIOTypedWebSocketClientUpgrader(NIOWebSocketClientUpgrader.swift 第 83-111 行)默认参数:requestKey随机生成、maxFrameSize默认1 << 14(16384 字节)、enableAutomaticErrorHandling默认true(自动添加WebSocketProtocolErrorHandler);- 升级成功后,upgrader 会向管线追加
WebSocketFrameEncoder、ByteToMessageHandler(WebSocketFrameDecoder(...))等 handler(第 191-198 行),再回调upgradePipelineHandler完成NIOAsyncChannel包装。
配置完成后,我们必须先await升级结果——因为它要在连接上进行协商:
switch try await upgradeResult.get() { case .websocket(let websocketChannel): print("Handling websocket connection") try await self.handleWebsocketChannel(websocketChannel) print("Done handling websocket connection") case .notUpgraded: // The upgrade to websocket did not succeed. print("Upgrade declined") }得益于所有类型化处理器都泛化于UpgradeResult,switch是穷尽式的:要么拿到升级后的NIOAsyncChannel,要么明确得知未升级,编译器会保证每个分支都被处理。
NIOAny 的弃用与 Sendable 迁移
自 NIO 2.77.0 起,一批以NIOAny为参数的方法开始产生弃用警告。文档指出,这些警告是"你可能本会看到的并发警告的替代品"——即用弃用警告来提示并发不安全性。
问题根源
这些方法(多数定义在ChannelInvoker上,见 ChannelInvoker.swift 第 48-69 行)的问题是:它们既可以在 EventLoop 上调用,也可以在 EventLoop 外调用。这意味着它们必须有能力把值跨隔离域(isolation domain)发送进 EventLoop,因此参数必须是Sendable(或标记为sending)。而NIOAny本质是一个类型擦除盒子,无法被做成Sendable。
用户在Channel(遵循ChannelInvoker)以及ChannelPipeline上调用这些方法时最常遇到此警告。
迁移方式
这些方法已被替换为接受泛型Sendable参数的等价方法,由新方法负责内部包装成NIOAny。最常见的修法就是移除手动NIOAny包装:
// 旧:手动包装 + 弃用警告 channel.writeAndFlush(NIOAny(myMessage), promise: nil) // 新:直接传 Sendable 值 channel.writeAndFlush(myMessage, promise: nil)writeAndFlush的异步重载同样如此——AsyncAwaitSupport.swift 第 262-270 行中,接收NIOAny的writeAndFlush(_ data: NIOAny)async 重载已被弃用,提示 "NIOAny is not Sendable: avoid wrapping the value in NIOAny to silence this warning"。
必须发送非 Sendable 值的例外场景
如果确实需要把非Sendable值送入管线,仍有少数方法可用:它们位于ChannelPipeline/SynchronousOperations(通过channel.pipeline.syncOperations访问,见 ChannelPipeline.swift 第 1230 行起)。该类型只能在 EventLoop 上访问,因此不存在跨隔离域发送值的问题,自然也就不需要Sendable约束。
通用指导:业务逻辑与协议逻辑的代码布局
文档最后给出了重要的架构建议,核心思想是按职责分层:
业务逻辑应该写在哪里?
在 Swift Concurrency 出现之前,网络协议实现和业务逻辑常常混在同一个ChannelHandler里。这虽然上手快,但有明显缺点:
- 业务逻辑被迫处理
ChannelHandler协议带来的全部不变量,往往需要编写复杂的状态机; - 业务逻辑与 NIO 强耦合,难以移植到其他系统。
因此官方建议:
- 业务逻辑(business logic)→ 使用 Swift Concurrency 原语 + 基于
NIOAsyncChannel的 Bootstrap 编写; - 网络协议实现(protocol-specific logic,如解析器、编码器)→ 仍然作为
ChannelHandler实现。
NIOAsyncChannel的头注释(AsyncChannel.swift 第 31-34 行)给出了同样的分层指引,并指出NIOAsyncChannel有意不暴露用户事件与传统的 writability 背压信号——协议级细节留在 Handler 层,业务层只需面对干净的AsyncSequence读写接口。这种分层也是测试友好性的来源:NIOAsyncChannelInboundStream.makeTestingStream()与NIOAsyncChannelOutboundWriter.makeTestingWriter()提供了专门的测试源/测试汇(见 AsyncChannelInboundStream.swift 第 71-77 行与 AsyncChannelOutboundWriter.swift 第 78-85 行),而 AsyncChannelTests.swift 中诸如testChannelBecomingNonWritableDelaysWriters、testManagingBackPressure、testExecuteThenCloseFromActor等测试用例也印证了背压、关闭与 Actor 隔离等行为。
小结
| 场景 | 推荐方案 |
|---|---|
async中等待 NIO Future | EventLoopFuture.get()(不响应取消)/getAbandoningOnCancel()(响应取消) |
| 把异步代码结果回填 Promise | EventLoopPromise.completeWithTask(_:) |
| 桥接双向流式 Channel | NIOAsyncChannel+executeThenClose作用域用法 |
| 启动 TCP 服务端/客户端 | 异步ServerBootstrap.bind(...)/ClientBootstrap.connect(...)+NIOAsyncChannel |
| ALPN / HTTP Upgrade 动态协商 | NIOTypedApplicationProtocolNegotiationHandler、NIOTypedHTTPServerUpgradeHandler、NIOTypedHTTPClientUpgradeHandler+ 类型化配置 |
| 向 Pipeline 写入跨隔离域的值 | 使用接受泛型Sendable的新方法;EventLoop 内可用syncOperations |
| 业务逻辑 vs 协议逻辑 | 业务逻辑用 Swift Concurrency +NIOAsyncChannel;协议逻辑用ChannelHandler |
这套互操作体系的完整代码与测试都可以在当前仓库中继续研读:核心类型位于 Sources/NIOCore/AsyncChannel 与 Sources/NIOCore/AsyncSequences,桥接实现见 AsyncAwaitSupport.swift,异步 Bootstrap 见 Sources/NIOPosix/Bootstrap.swift,类型化升级处理器见 Sources/NIOHTTP1 与 Sources/NIOWebSocket,测试用例可参考 Tests/NIOCoreTests/AsyncChannel 与 Tests/NIOPosixTests/AsyncChannelBootstrapTests.swift。
- 后端
- 网络
【免费下载链接】swift-nio
Event-driven network application framework for high performance protocol servers & clients, non-blocking.
相关推荐
SwiftMessages与Swift Concurrency:异步显示与取消操作
SwiftMessages与Swift Concurrency:异步显示与取消操作 在iOS开发中,消息提示是用户交互的重要组成部分。传统的消息提示往往面临异步
移动开发AI System Design Guide快速开始:5步搭建你的第一个AI系统原型
AI System Design Guide快速开始:5步搭建你的第一个AI系统原型 想要快速入门AI系统设计吗?无论你是AI工程师、软件开发者还是技术管理者,
RxSwift 与 Swift Concurrency 实战指南:async/await 双向桥接 Observable 与 AsyncSequence
RxSwift 与 Swift Concurrency 实战指南:async/await 双向桥接 Observable 与 AsyncSequence 导读
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考