Network.framework 入门

1,915 阅读7分钟
原文链接: mp.weixin.qq.com

现代化的传输 API

说起 Socket  ,我回头望了一眼书架上厚厚的 UNIX 网络编程 卷1: 套接字联网 API(第 3 版)  ,而她的姊妹进程间通信我连塑封膜都没拆开。的确,这套最早来自 BSD 的 API 很让人头疼。虽然她们依然是跨平台程序的最佳选择,但是我想应该没有哪个小伙伴在项目中会有勇气从这些 API 开始构筑,至少是 CFNetwork  或者 NSNetwork  中的现成接口。更一般性的是选一些面向对象的第三方库,比如老牌的 CocoaAsyncSocket 。当然作为 Swift 老法师我也会推荐你看看 IBM 出品的 BlueSocket

Socket 编程有很多需要解决的问题,最重要的 3 个大问题,以及更多的细节问题:

  • 建立连接

  • 数据传输

  • 连接的变动

当前,URLSession  底层就是使用 Network.framework  完成基础连接的。特地查了一下,相关私有 API 是从 iOS 9 开始存在的。在未来,Apple 希望你能够将原来的 Socket API 全部替换为全新的 Network.framework 。(iOS 又有人要了!)

Network.framework 的特点

  • 智能建立连接

  • 经优化的数据传输

  • 内建的安全加密

  • 无缝兼容移动网络

  • 原生 Swift 支持🔥🔥🔥

开始你的第一次连接

Socket 主要使用的三种场景:游戏联机、流式视频传输、在线聊天。

使用传统 Socket 建立连接

  • 使用 getaddrinfo() 查询 DNS

  • 使用正确的地址族去调用 socket()

  • 使用 setsockopt() 设置 socket 选项

  • 调用 connect() 开始 TCP 连接

  • 等待直到一个可写入的事件回调

使用 Network.framework 建立连接

  • 使用 NWEndPoint 与 NWParameters 创建连接

  • 调用 connection.start()

  • 等待连接进入 .ready 的状态

对就是这么简单,完全的原生 Swift 支持,又面向对象,又支持闭包。这样的接口,你不心动么?

连接的生命周期

在连接设置完毕以后,就会进入 准备 状态。而针对移动设备复杂的网络状态,你需要更加智能的建立连接。

而使用 Network.framework ,你可以十分简单的对网络路径进行配置,比如下面的例子中,指定了仅使用蜂窝网络、使用 IPv6 协议、与禁止代理。都仅是一行命令就完成了。特别当你需要为特定连接指定连接方式时,这个框架能极大提高你的效率。

在准备完毕以后,连接可能进入 等待 、就绪 或 失败 状态。当然在你取消连接时也会进入 取消 状态。

案例:流式视频传输

该案例使用 UDP 进行视频的实时传输,出于简化考虑,并未对视频帧做任何编码,直接把裸数据封包,并通过 UDP 传输。在接收端,解包数据并重新封装为视频帧,直接进行播放。案例中也使用了 Bonjour 服务来进行快速设备配对连接。

在监听端的代码异常简单,甚至连 Bonjour  服务也已经整合好了。你要做的仅仅是指定 .udp  并指定正确的 Bonjour 服务名称。

最佳的数据传输方式

数据的发送与接收

单帧发送

// Send a single framefunc sendFrame(_ connection: NWConnection, frame: Data) {    // The .contentProcessed completion provides sender-side back-pressure    connection.send(content: frame, completion: .contentProcessed { (sendError) in        if let sendError = sendError {            // Handle error in sending        } else {            // Send has been processed, send the next frame            let nextFrame = generateNextFrame()            sendFrame(connection, frame: nextFrame)        }    })}

使用 batch 发送多个数据报

// Hint that multiple datagrams should be sent as one batchconnection.batch {    for datagram in datagramArray {        connection.send(content: datagramArray, completion: .contentProcessed { (error) in            // Handle error in sending        }    })}

在接收时,提供了方便的方法来读取消息头

// Read one header from the connectionfunc readHeader(connection: NWConnection) {    // Read exactly the length of the header    let headerLength: Int = 10    connection.receive(minimumIncompleteLength: headerLength, maximumLength: headerLength) { (content, contentContext, isComplete, error) in        if let error = error {            // Handle error in reading        } else {         // Parse out body length        readBody(connection, bodyLength: bodyLength)        }    }}// Follow the same pattern as readHeader() to read exactly the body lengthfunc readBody(_ connection: NWConnection, bodyLength: Int) { ... }

高级选项

显式拥塞通知(Explicit Congestion Notification)

在所有 TCP 连接中 ECN 是默认开启的。

在 UDP 连接中为每个数据包标记 ECN 的方法:

let ipMetadata = NWProtocolIP.Metadata() ipMetadata.ecn = .ect0let context = NWConnection.ContentContext(identifier: "ECN", metadata: [ ipMetadata ])connection.send(content: datagram, contentContext: context, completion: .contentProcessed{..})

服务等级(网络队列优先级)

为整个连接更改服务等级

let parameters = NWParameters.tls parameters.serviceClass = .background

为每个 UDP 数据包更改服务等级

let ipMetadata = NWProtocolIP.Metadata() ipMetadata.serviceClass = .signalinglet context = NWConnection.ContentContext(identifier: "Signaling", metadata: [ ipMetadata ])connection.send(content: datagram, contentContext: context, completion: .contentProcessed{..})

快速连接(Fast Open Connections)

允许在连接上快速打开需要发送幂等数据

parameters.allowFastOpen = truelet connection = NWConnection(to: endpoint, using: parameters)connection.send(content: initialData, completion: .idempotent) connection.start(queue: myQueue)

可以手动启用 TCP Fast Open 以通过 TFO 运行 TLS

let tcpOptions = NWProtocolTCP.Options() tcpOptions.enableFastOpen = true

允许失效的 DNS 查询结果

主动使用失效的 DNS 查询结果

parameters.expiredDNSBehavior = .allowlet connection = NWConnection(to: endpoint, using: parameters)connection.start(queue: myQueue)

新的 DNS 查询会同步进行

处理网络连接的变动

开始连接

  • .waiting 状态暗示连接还未建立

  • 避免在网络连接开始前检查可用性

  • 在需要时在 NWParameters 限制连接类型

处理网络连接状态的变化

主要是两个状态,一个是 isViable  当前连接是否可用,一个是 betterPathAvailable  是否有更佳的连接路径。她们也都提供了相应的闭包来处理

// Handle connection viabilityconnection.viabilityUpdateHandler = { (isViable) in    if (!isViable) {        // Handle connection temporarily losing connectivity    } else {        // Handle connection return to connectivity    }}// Handle better pathsconnection.betterPathUpdateHandler = { (betterPathAvailable) in    if (betterPathAvailable) {        // Start a new connection if migration is possible    } else {        // Stop any attempts to migrate    }}

开始实践

应避免的做法

不应继续使用的接口

CoreFoundation 中 CFStream  绑定的相关方法及 CFSocket

Foundation  中与 NSStream  绑定、NSNetService  监听、NSSocketPort  以及 SystemConfiguration  中的 SCNetworkReachability

推荐的接口

当然是 URLSession  和 Network.framework

推荐阅读

使用 iOS 12 的 Network Framework 实现 netcat Core ML & Vision 入门教程 iOS 任务调度器:为 CPU 和内存减负