尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Swift NIO 实战:用 NIOTCPEchoServer 搭建基于 async/await 的 TCP Echo 服务

Swift NIO 实战:用 NIOTCPEchoServer 搭建基于 async/await 的 TCP Echo 服务 后端网络【免费下载链接】swift-nioEvent-driven network application framework for high performance protocol servers clients, non-blocking.项目地址https://gitcode.com/gh_mirrors/sw/swift-nio点击查看免费下载SwiftNIO 官方示例NIOTCPEchoServer是一个最精简的 TCP 回显echo服务端它将客户端发来的数据原样返回但代码中浓缩了 SwiftNIO 2.x 时代推荐的整套现代编程模式——ServerBootstrap绑定、pipeline 编解码、NIOAsyncChannel结构化并发封装以及用withThrowingDiscardingTaskGroup管理海量连接的内存安全细节。读完本文你能独立读懂这个示例的每一行代码掌握从仓库根目录一键启动服务并用配套客户端验证的完整流程并理解其中每个关键 API 背后的设计动机。示例定位与快速启动NIOTCPEchoServer的定位在 Sources/NIOTCPEchoServer/README.md 中一句话说明这是一个把客户端发来的任何内容原样发回去的简单 TCP 服务器。启动方式同样由该文档给出在仓库根目录下执行swift run NIOTCPEchoServer服务启动后官方推荐的验证方式是运行配套的 NIOTCPEchoClientswift run NIOTCPEchoClient客户端会建立多条连接、发送消息并等待回显响应二者配合构成一个最小可闭环的端到端 demo。在构建层面Package.swift 将其声明为一个可执行目标仅依赖NIOPosix和NIOCore两个产品库README.md被显式 exclude 于编译之外.executableTarget( name: NIOTCPEchoServer, dependencies: [ NIOPosix, NIOCore, ], exclude: [README.md], swiftSettings: swiftSettings ),这一依赖面本身就是一个信号一个生产级 TCP 服务所需的网络原语只需要 NIOCore核心抽象加 NIOPosixPOSIX 平台实现两个模块即可承载。适用前提Server.swift的入口类型标注了available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)。由于 Linux 平台不受该available列表约束*兜底在 Linux 上运行需要工具链支持 Swift 结构化并发与NIOAsyncChannel的稳定 API 即可而 Apple 平台则需要 macOS 14 / iOS 17 及以上。服务端整体架构三段式数据流完整实现见 Sources/NIOTCPEchoServer/Server.swift。整个服务由三层结构组成理解这条链路是读懂示例的关键传输层ServerBootstrap负责监听 socketaccept 到每个新连接后创建ChildChannel编解码层pipeline 中安装两个 handler负责把字节流按\n拆分为String消息解码再把String消息追加\n写回字节流编码业务层NIOAsyncChannelString, String把 channel 包装为 async/await 风格的入站AsyncSequence与出站 writer业务代码直接以for try await消费消息。对应的核心代码骨架如下摘自 Server.swiftfunc run() async throws { let channel try await ServerBootstrap(group: self.eventLoopGroup) .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) .bind(host: self.host, port: self.port) { channel in channel.eventLoop.makeCompletedFuture { try channel.pipeline.syncOperations.addHandler(ByteToMessageHandler(NewlineDelimiterCoder())) try channel.pipeline.syncOperations.addHandler(MessageToByteHandler(NewlineDelimiterCoder())) return try NIOAsyncChannel( wrappingChannelSynchronously: channel, configuration: NIOAsyncChannel.Configuration( inboundType: String.self, outboundType: String.self ) ) } } try await withThrowingDiscardingTaskGroup { group in try await channel.executeThenClose { inbound in for try await connectionChannel in inbound { group.addTask { print(Handling new connection) await self.handleConnection(channel: connectionChannel) print(Done handling connection) } } } } }为什么绑定到 localhost:8765 并复用 so_reuseaddrServer.main()中硬编码了服务参数let server Server( host: localhost, port: 8765, eventLoopGroup: .singleton )host: localhost、port: 8765这是 demo 的默认监听地址配套客户端 Client.swift 也硬编码了相同的地址与端口因此两者必须成对启动。eventLoopGroup: .singleton使用 SwiftNIO 提供的全局单例MultiThreadedEventLoopGroup。从 GlobalSingletons.swift 的文档注释可以看到SwiftNIO 为不需要精细控制线程资源的程序提供了单例资源单例的线程数可以在任何单例被使用之前通过NIOEventLoopGroupPreferences相关配置项建议但一旦单例创建后修改会触发崩溃。示例选择单例是为了零配置运行。.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)设置SO_REUSEADDR允许服务在重启时快速重新绑定同一端口避免TIME_WAIT导致的bind失败这是 TCP 服务端几乎必加的选项。bind的 completion 闭包在每个子 channel 创建时被调用注意其中的channel.eventLoop.makeCompletedFuture写法handler 的安装与NIOAsyncChannel的包装必须在 channel 所在 event loop 上同步完成详见下文事件循环约束。换行符编解码NewlineDelimiterCoder字节流本身没有消息边界TCP 是流协议一条消息必须由应用层协议定义。本示例用\n作为分隔符NewlineDelimiterCoder同时实现了ByteToMessageDecoder和MessageToByteEncoderprivate final class NewlineDelimiterCoder: ByteToMessageDecoder, MessageToByteEncoder { typealias InboundIn ByteBuffer typealias InboundOut String private let newLine UInt8(ascii: \n) func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws - DecodingState { let readableBytes buffer.readableBytesView if let firstLine readableBytes.firstIndex(of: self.newLine).map({ readableBytes[..$0] }) { buffer.moveReaderIndex(forwardBy: firstLine.count 1) // Fire a read without a newline context.fireChannelRead(Self.wrapInboundOut(String(buffer: ByteBuffer(firstLine)))) return .continue } else { return .needMoreData } } func encode(data: String, out: inout ByteBuffer) throws { out.writeString(data) out.writeInteger(self.newLine) } }解码侧有两个值得注意的实现细节返回.needMoreData表示当前 buffer 中还没有一个完整消息解码器会等待更多字节到达后被再次调用。这正确处理了半包一条消息分多个 TCP 段到达的场景。返回.continue表示还有数据可继续解析。decode中只消费第一条消息firstLine不含换行符并通过moveReaderIndex(forwardBy: firstLine.count 1)跳过换行符本身然后返回.continue让框架继续用剩余字节调用解码。这天然支持粘包——一次read收到多行时循环解码会依次切出每条消息。编码侧则是对称的把String写入ByteBuffer并追加\n。该 coder 在客户端 Client.swift 中以完全相同的方式被复用客户端文件里有一份独立私有副本双方必须使用同一帧协议这正是协议编解码作为独立 handler 的价值——业务 handler 只看到String完全不知道换行符的存在。NIOAsyncChannel把 pipeline 封装成 async/awaitNIOAsyncChannel(wrappingChannelSynchronously:configuration:)的调用必须发生在 channel 的 event loop 上。从 AsyncChannel.swift 的源码注释可以看到这一约束的原因Thismustbe called on the channels event loop otherwise this init will crash. This is necessary because we must install the handlers before any other event in the pipeline happens otherwise we might drop reads.即异步 handler 的安装必须先于 pipeline 中任何其他事件发生否则会丢失读事件。示例中makeCompletedFuture闭包恰好运行在 event loop 内因此wrappingChannelSynchronously在此是安全且同步的。包装后的NIOAsyncChannelString, String配置了inboundType: String.self与outboundType: String.self意味着入站流吐出String、出站 writer 接受String——类型即协议编译器保证消息与 codec 约定一致。从 AsyncChannel.swift 中Configuration的定义还可以了解到两个示例未显式设置、但有默认值的配置项实际项目中可按需调整backPressureStrategy入站流的背压策略默认是高低水位策略HighLowWatermark(lowWatermark: 2, highWatermark: 10)。当入站消息积压超过高水位时暂停底层读取回落到低水位后恢复防止慢消费者拖垮内存isOutboundHalfClosureEnabled默认false若开启则出站 writer 结束或释放时触发出站半关闭发送 FIN 但保留接收适用于需要优雅写关闭的长连接协议。executeThenClose是推荐的作用域 API在闭包内使用入站流与出站 writer闭包退出后 channel 自动关闭。这与早期需要手动管理channel.close()的生命周期相比把资源必然释放变成了语言结构层面的保证。连接级任务管理为什么必须用 discarding task group服务端的连接分发逻辑是这段示例中最容易被忽略、也最容易被写错的部分try await withThrowingDiscardingTaskGroup { group in try await channel.executeThenClose { inbound in for try await connectionChannel in inbound { group.addTask { print(Handling new connection) await self.handleConnection(channel: connectionChannel) print(Done handling connection) } } } }每个NIOAsyncChannel即一条客户端连接都在group.addTask中开一个子任务处理。源码注释直接点明了为什么必须使用withThrowingDiscardingTaskGroup而不是普通的withThrowingTaskGroupIt is important to use a discarding task group here which automatically discards finished child tasks. A normal task group retains all child tasks and their outputs in memory until they are consumed by iterating the group or by exiting the group. Since, we are never consuming the results of the group we need the group to automatically discard them; otherwise, this would result in a memory leak over time.也就是说普通 task group 会把所有已结束子任务及其返回值保留在内存中直到被迭代消费或 group 退出本场景中连接是无限流子任务结果永远不会被消费若用普通 group服务运行越久内存占用越大——对长期运行的服务端而言这是一条必须遵守的准则。注意外层channel.executeThenClose消费的是server channel 的入站流每 accept 一个新连接就产出一个NIOAsyncChannel循环随服务关闭而自然结束整个run()的退出路径也因此是确定性的。单连接处理与故障隔离连接处理逻辑独立在handleConnection中它体现了单条连接的异常不得拖垮整个服务的设计private func handleConnection(channel: NIOAsyncChannelString, String) async { // Note that this method is non-throwing and we are catching any error. // We do this since we dont want to tear down the whole server when a single connection // encounters an error. do { try await channel.executeThenClose { inbound, outbound in for try await inboundData in inbound { print(Received request (\(inboundData))) try await outbound.write(inboundData) } } } catch { print(Hit error: \(error)) } }方法签名无throws内部do/catch捕获全部错误。对比外层的withThrowingDiscardingTaskGroup异常会向上传播并终止服务这里的吞错是故意的某个客户端异常断开或协议出错时只打印日志并结束该连接不影响其他连接与服务本身。回显逻辑本身只有两行for try await inboundData in inbound逐条消费消息try await outbound.write(inboundData)原样写回。outbound.write是 await 的天然受背压约束——如果下游写得太慢socket 缓冲已满写入会挂起而不是无限缓冲。连接何时结束当客户端发送 FIN出站半关闭导致入站流结束时for循环退出executeThenClose关闭整个 channel。端到端验证配套客户端的行为验证服务是否工作运行 NIOTCPEchoClientswift run NIOTCPEchoClient从 Client.swift 可以看到客户端的具体行为func run() async throws { try await withThrowingTaskGroup(of: Void.self) { group in for i in 0...20 { group.addTask { try await self.sendRequest(number: i) } } try await group.waitForAll() } }一次性并发发起21 个连接0...20每个连接发送Hello on connection N收到一条回显后即break退出循环退出循环后NIOAsyncChannel引用被释放连接自动关闭——客户端注释明确说明once we exit out of this loop and the references to the NIOAsyncChannel are dropped the connection is going to close itself与 server 端刻意使用 discarding group 不同这里使用普通withThrowingTaskGroupwaitForAll()是恰当的因为所有子任务完成后任务就结束了不存在累积问题。server 与 client 的完整配合链路为clientClientBootstrap.connect→ server accept 并进入for try await connectionChannel in inbound→ 双方 pipeline 中各自的NewlineDelimiterCoder完成编解码 → serveroutbound.write(inboundData)回显 → client 打印响应并关闭。小结NIOTCPEchoServer虽然只有百行代码却完整覆盖了用 SwiftNIO 编写现代 TCP 服务的关键决策点关注点示例做法依据服务启动ServerBootstrapso_reuseaddrlocalhost:8765Server.swift消息分帧NewlineDelimiterCoder同时实现解码.needMoreData/.continue与编码Server.swift并发模型NIOAsyncChannelexecuteThenClose作用域 APIAsyncChannel.swift连接管理withThrowingDiscardingTaskGroup防止子任务累积导致内存泄漏Server.swift故障隔离单连接do/catch异常不传播至服务层Server.swift端到端验证配套NIOTCPEchoClient并发 21 连接验证回显Client.swift如果要在此基础上扩展真实业务更换帧协议、持久化事件循环组、调整背压水位都可以直接在Server.swift的对应位置修改而无需改变整体结构——这正是官方示例保持如此简练的原因它示范的不是一个 echo 服务而是 SwiftNIO 2.x 时代编写网络服务的标准姿势。赞分享后端网络【免费下载链接】swift-nioEvent-driven network application framework for high performance protocol servers clients, non-blocking.项目地址https://gitcode.com/gh_mirrors/sw/swift-nio点击查看免费下载相关推荐swift-nio NIOTCPEchoClient 详解基于 Swift 并发async/await的 TCP 回显客户端实战swift nio NIOTCPEchoClient 详解基于 Swift 并发async/await的 TCP 回显客户端实战 本文以 NIOTCPEc后端网络Tornado TCP Echo 实战用 TCPServer 与 TCPClient 构建异步流式服务Tornado TCP Echo 实战用 TCPServer 与 TCPClient 构建异步流式服务 导读 Tornado 不仅是 Web 框架其底层还提后端Web框架异步编程WebSocketQuick 框架中的 Async/Await基于 AsyncSpec 编写 Swift 并发测试Quick 框架中的 Async/Await基于 AsyncSpec 编写 Swift 并发测试 本指南围绕 Quick 测试框架的 Async/Await测试开发工具上一篇Chameleon框架与Core Animation结合高性能渐变动画实现下一篇PhotoGIMP让Photoshop用户无缝切换到开源图像编辑的完美方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表