WWDC 2018:Network.framework 入门,现代化 Socket 编程的新选择

WWDC18 Session 715 Introducing Network.framework: A modern alternative to Sockets编程

现代化的传输 API

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

Socket 编程有不少须要解决的问题,最重要的 3 个大问题,以及更多的细节问题:安全

  • 创建链接
  • 数据传输
  • 链接的变更

当前,URLSession 底层就是使用 Network.framework 完成基础链接的。特意查了一下,相关私有 API 是从 iOS 9 开始存在的。markdown

在将来,Apple 但愿你可以将原来的 Socket API 所有替换为全新的 Network.framework。(iOS 又有人要了!)网络

Network.framework 的特色

  • 智能创建链接
  • 经优化的数据传输
  • 内建的安全加密
  • 无缝兼容移动网络
  • 原生 Swift 支持🔥🔥🔥

开始你的第一次链接

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

使用传统 Socket 创建链接

  1. 使用 getaddrinfo() 查询 DNS
  2. 使用正确的地址族去调用 socket()
  3. 使用 setsockopt() 设置 socket 选项
  4. 调用 connect() 开始 TCP 链接
  5. 等待直到一个可写入的事件回调

使用 Network.framework 创建链接

  1. 使用 NWEndPointNWParameters 建立链接
  2. 调用 connection.start()
  3. 等待链接进入 .ready 的状态

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

链接的生命周期

在链接设置完毕之后,就会进入 准备 状态。而针对移动设备复杂的网络状态,你须要更加智能的创建链接。app

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

在准备完毕之后,链接可能进入 等待就绪失败 状态。固然在你取消链接时也会进入 取消 状态。socket

案例:流式视频传输

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

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

最佳的数据传输方式

数据的发送与接收

单帧发送

// Send a single frame
func 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 batch
connection.batch {
    for datagram in datagramArray {
        connection.send(content: datagramArray, completion: .contentProcessed { (error) in
            // Handle error in sending
        }
    })
}
复制代码

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

// Read one header from the connection
func 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 length
func readBody(_ connection: NWConnection, bodyLength: Int) { ... }
复制代码

高级选项

显式拥塞通知(Explicit Congestion Notification)

在全部 TCP 链接中 ECN 是默认开启的。

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

let ipMetadata = NWProtocolIP.Metadata() 
ipMetadata.ecn = .ect0
let 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 = .signaling
let context = NWConnection.ContentContext(identifier: "Signaling", metadata: [ ipMetadata ])
connection.send(content: datagram, contentContext: context, completion: .contentProcessed{..})
复制代码

快速链接(Fast Open Connections)

容许在链接上快速打开须要发送幂等数据

parameters.allowFastOpen = true
let 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 = .allow
let connection = NWConnection(to: endpoint, using: parameters)
connection.start(queue: myQueue)
复制代码

新的 DNS 查询会同步进行

处理网络链接的变更

开始链接

  • .waiting 状态暗示链接还未创建
  • 避免在网络链接开始前检查可用性
  • 在须要时在 NWParameters 限制链接类型

处理网络链接状态的变化

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

// Handle connection viability
connection.viabilityUpdateHandler = { (isViable) in
    if (!isViable) {
        // Handle connection temporarily losing connectivity
    } else {
        // Handle connection return to connectivity
    }
}

// Handle better paths
connection.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。

查看更多 WWDC 18 相关文章请前往 老司机x知识小集xSwiftGG WWDC 18 专题目录