大多数浏览器和
Developer App 均支持流媒体播放。
-
Introducing Network.framework: A modern alternative to Sockets
The new Network.framework API gives you direct access to the same high-performance user-space networking stack used by URLSession. If you're considering using Berkeley Sockets in your app or library, learn what better options are available to you.
资源
- Implementing netcat with Network Framework
- Supporting IPv6 DNS64/NAT64 Networks
- URLSession
- WWDC2013 - What's New in Foundation Networking
- WWDC2014 - What's New in Foundation Networking
- WWDC2015 - Networking with NSURLSession
- 演示幻灯片 (PDF)
相关视频
WWDC21
WWDC20
-
下载
(Network.framework简介 套接字的现代替代品) (演讲715) 早上好
我叫Josh Graessley 我很高兴今天早上能来到这里 向你介绍 Network.framework
Network.framework 是套接字的现代替代品 今天我们将讨论现代化传输API 这将提供一些背景知识以便你理解 Network.framework是什么 以及它如何结合到系统中 以及你的app是否应该使用它
我们将通过引导你完成 第一次连接来向你介绍其API 我们将讨论如何使用此API 来真正优化你的数据传输 并提供远超套接字所能提供的性能
我们将讨论这个API 如何帮助你应对 一些复杂的移动性挑战 最后我们将提供一些 关于你如何参与进来 并开始使用的信息
首先… 我想花一点时间谈论 现代化传输API 当我说传输API时 我在谈论这样的API 即它可以让你在网络上两个端点之间 发送和接收任意数据 这是一个相当广泛的定义 有很多API可能属于此类别
也许其中应用最广泛的就是套接字 我们使用套接字已经有30多年了 而且我认为 说套接字改变了世界并不夸张 但世界一直在变化 其结果是 使用套接字为今天的互联网 编写app真的很难 在三个主要领域中 若想很好地使用套接字 非常困难
第一个是建立连接 有很多原因导致使用套接字 来建立连接非常困难 首先 套接字连接到地址 所以大多数时候你需要有一个主机名 并且你必须将该主机名 解析为一个地址 当你这样做时 你通常会得到多个地址 你会得到一些IPv4地址 一些IPv6地址 现在你遇到了这个挑战 你应该尝试连接哪个地址 以什么顺序 你在尝试下一个之前等待多久 你可以花费数年时间来完善这一点 我知道 因为我们就是这样做的
一旦你解决了双协议栈主机问题 你还会遇到更多其它问题 有些网络使用一种 叫做代理自动配置或PAC的东西 在这些网络上 你会得到一个JavaScript 并且你必须将一个URL 传递给JavaScript 然后JavaScript 将运行并返回一结果 其中说明你可以直接连接 或是你必须使用这个SOCKS代理 或者那个HTTP连接代理 这样你的app就必须 支持SOCKS代理 和HTTP连接代理 这真的很难做好 最困难的事情 是你可能没有其中一个网络以供测试 所以你可能会收到 一位客户的错误报告 他们可能会抱怨它在他们的环境下 无法正常工作 你可能想要添加代码来解决问题 但… 尽管你修改了代码 你却无法测试它 你最终必须构建整个环境 来重现其所在的相同环境 这是一个真正的挑战 所以用套接字连接真的很难
使用套接字的第二个挑战 是数据传输 使用套接字传输数据非常困难 这有很多原因
主要的问题是读写模型本身 如果你使用阻塞套接字 那很简单 但是你要绑定一个线程 而在等待读取或写入数据时 绑定一个线程 真的不是一个好主意 你可以切换到非阻塞 但最终你会遇到更多其它挑战 当你使用非阻塞时 你可能告诉内核 我想要100个字节 而内核可能会回答说 我现在只有10个字节 你为何不稍后再回来呢 你必须建立一个状态机 以跟踪你读取的字节数 与你想要读取的字节数 这个工作量可能会很大 而且要想使其表现良好 可能是一个真正的挑战
最重要的是 你真的不应该直接读写套接字 因为你应该使用安全传输层协议 即TLS之类的东西
套接字不支持TLS 所以你可能正在使用其它一些库 它们会为你处理TLS 并替你读取和写入套接字 或者你也可以编写 该库和套接字之间的胶水代码 而且你必须弄清楚如何预先放入 那些复杂的连接逻辑 并让所有这些正常工作
这里有很多东西可能非常困难
最后 使用套接字对移动性 带来极大的挑战 这有多种原因
我认为这很大程度上归结于 当套接字刚出现时 许多设备需要不止一个人来移动它们 而且它们只用一根网线连接 并且它们只有一个静态IP地址 一切都很稳定和简单 而今天我们的口袋里 装有这些功能强大的设备 其可能同时开启多射频 并且其中一些正在从一个网络 迁移到另一个网络 而你的app必须很好地处理 所有这些转换 来为你的客户提供无缝体验
套接字无法帮你解决此问题 你可以使用路由套接字 但这非常非常困难 我们认为传输API可以做得更好
幸运的是 在我们的平台上 作为app开发者 你在URLSession中 有一个很棒的API URLSession 为你处理所有这些问题 它真正专注于HTTP 但它也有流任务 它能为你提供 对TCP和TLS连接的原始访问
现在你可能正在看这幅图 并且如果你没有通过查看 WWDC app中的描述而作弊 你可能认为URLSession 是基于与你自己使用的 相同的原语所构建的
但事实并非如此 URLSession建立在我们称之为 Network.framework的基础之上 URLSession 关注的是所有的HTTP内容 它将很多传输功能委托给 Network.framework
我们研发 Network.framework 已经有许多年了 在支持URLSession方面 我们学到了很多东西 并且我们把很多这些经验 带到了IETF 我们的许多工程师经常参加IETF 并与其它公司的工程师见面 他们讨论了许多 我们在传输服务工作组中 学到的东西 在这些讨论中 我们得到了一些很好的反馈 我们研究这些反馈 并基于此 改进 Network.framework 我们很高兴地宣布 今年你的app 现在可以直接利用这个库
我们知道人们喜欢套接字的原因之一 是它给予人们 对几乎所有东西的精细控制 而他们真的不想失去这点 因此我们在开发 Network.framework时 我们希望确保默认情况下 它能做正确的事情 而套接字不会这样做 但同时它也给了你 套接字的所有功能 并且它是以一种渐进的方式 即你转动的旋钮越多 它变得越复杂 它为你提供所需的所有功能 但你不必为复杂性付出代价 除非你真的需要它
Network.framework 具有令人难以置信的智能连接搭建过程 它能处理双协议栈用例 它能处理仅IPv6网络 它能处理PAC 它也能处理代理 它会… 帮助你连接在不使用它的情况下 很难处理的网络 它具有令人难以置信的 优化数据传输路径 其性能要远远超过 套接字可以达到的效果 稍后Tommy会介绍一下这点
它支持内置安全性 它默认支持TLS和DTLS 它使用起来非常简单
它对移动性有很好的支持 它提供有关网络更改的通知 其与你的app正在建立的连接相关
它可以在iOS、macOS 和tvOS上 作为带有自动引用计数的 C API使用 它很容易在Objective C中使用 且它还有令人难以置信的Swift API
我想把话筒交给 Tommy Pauly 他会引导你完成你的第一次连接 谢谢
好的 大家好 我叫Tommy Pauly 我在Apple的网络团队工作 我相信很多人都很高兴能够看到 如何开始在你的app中 使用Network.framework
开始学习并立即尝试的最好方式 就是尝试建立你的第一个连接 你将从本地设备建立连接 到你的服务器 或本地网络上的其它对等设备
但你可能在想 哪种连接适合使用 Network.framework呢 有什么用例? 让我们首先探讨一些可能正在使用 套接字的app 它们若能在以后利用 Network.framework 将会大有裨益
我要强调的第一种 是游戏app 游戏app通常使用UDP 在两台设备间实时发送 关于游戏状态的数据
它们非常重视优化延迟 并确保没有任何滞后 或任何被丢弃的东西 如果你有这样的app 你会爱上 Network.framework 因为它允许你真正优化UDP 发送和接收比以往更快 以及具有尽可能短的延迟时间
另一种能够得益于 Network.framework的 app类别 是直播app 直播app 通常在app中结合使用UDP 和TCP 但这里的关键点是 它正在实时地生成数据 如果你有新的视频帧或音频帧 你需要确保它们步调一致 而且你不会在设备或网络上 造成很多缓冲
Network.framework中 用于读写的异步模型 可以完美的应用于 确保你能够减少这种缓冲
我要强调的最后一个用例 是邮件和消息app 这些app将使用更传统的协议 即TCP上的TLS 然而优雅地处理网络转换 对于像这样的app来说非常重要
通常 如果你有一个消息传递app 你的用户会在他走出大楼时 使用你的app 给他的朋友发消息让他们知道 他正在路上 你想确保你能够处理这种转变 从大楼内的Wi-Fi网络 切换到他即将进入的蜂窝网络 并且你没有用很长时间来将消息 送到他的朋友那里
这些只是可能会使用 像这样的低级网络的 三种类型的app 还有许多其它类型的app 可以利用它 所以如果你有一个这些类型的app 或当前使用套接字的其它一些用例 我邀请你继续跟随我 并了解你的app可以如何受益
首先我想专注于最后一个用例 最简单的邮件和消息app用例 并看下他们如何建立连接
当你想建立与服务器的连接时 假设这是一个邮件连接 即带有 TLS安全功能的iMap协议 你从主机名 mail.example.com开始 你有一个要连接的端口 即端口993 并且你希望使用TLS 以及TCP协议
那么传统的套接字是如何做的呢 这样的事情开始了 你得到该主机名 你会调用一些DNS API 来解析该主机名 若它是getaddrinfo() 你将得到一个或多个地址 你必须决定要先连接哪一个 你将调用socket() 附带适当的地址族
你将设置一系列套接字选项 假设你希望使用非阻塞套接字 像Josh之前提到的那样
调用connect()启动TCP 然后等待可写事件 而且这是在你使用TLS 做任何事情之前 那又是一大堆其它问题
这在Network.framework中 看起来如何呢? 我们希望它看起来很熟悉 但更简单一些 所以你做的第一件事 就是创建一个连接对象 连接对象基于两件事 第一个是端点 它定义了你要访问的目标 这可能是像以前那样的IP地址 但通常 就像在这个例子中 我们有一个主机名和一个端口 所以我们的端点 可以只包含主机名和端口 这也可能是 我想连接的Bonjour服务
接下来是参数 参数定义了我想要使用的协议 TLS、DTLS、UDP、TCP 它定义了我想要用的协议选项 以及我想用来连接的路径 即我是想使用任何东西连接呢 还是只想使用Wi-Fi呢?
一旦你配置好连接后 你只需要调用start() 来开始连接 然后等待连接进入就绪状态 这就是为了建立完整TLS连接 你需要做的所有事情 而且我认为你会喜欢 这在Swift中的样子
这就是你所要做的 你首先导入Network模块
然后你创建一个 NWConnection对象 所以一个NWConnection 无论是在Swift还是C中 都是读取和写入数据的基本对象
在这个例子中 我们可以方便地使用主机和端口 来初始化你的端点 所以我给它我的主机名 mail.example.com 以及端口 在这个例子中 这是一个众所周知的端口 即iMaps 所以我可以把它放在Swift中 非常简单 但我也可以在那里输入 任何其它数值文字
然后我通过传入参数来定义 我想要使用的协议 由于这是客户端连接 我只想要默认 TLS和TCP参数 只需要输入.tls 就这么简单 现在我有了一个完整的TLS连接
我做的下一件事就是设置 stateUpdateHandler 来处理我的连接可能经历的所有切换 你要处理的第一个 并且是最重要的状态 就是就绪状态 就绪意味着你的app已准备好 在此连接上读取和写入数据 连接已经完全建立 如果你使用的是TCP和TLS 这意味着TLS握手已完成
我们也会告知你等待状态 去年在URLSession中 我们引入了waitsForConnectivity 而NWConnection的 等待状态与之完全相同 而且默认情况下它始终处于启用状态 因此当你创建连接并启动它时 如果没有可用的网络 我们不会失败 我们只是告诉你 我们正在等待网络变为可用 我们会给你一个有用的原因代码 但你不必再做任何其它事情 来自己处理网络切换 移动性是此API的重要组成部分
如果出现严重错误 我们也会通知你 假设我们必须从服务器重置 或TLS失败了 我们会将其作为失败事件告知你
一旦你设置好了这些 只需调用start() 并提供你希望接收回调的调度队列
我想深入探讨一下 当调用start()时会发生什么 实际发生了什么
NWConnection的 内部结构 是一个小状态机 当我们从设置状态开始 并调用start()时 我们进入准备状态 (连接生命周期) 准备状态 它不仅仅是在TCP套接字上 调用connect() 对于TCP套接字 这会将SYN数据包发送到 想要连接的服务器
但当你在NWConnection上 调用start()时 它实际上处理了 Josh之前提到的所有事情
它会评估你所在的网络 并尝试为你提供最快的连接 我想继续深入讨论一下 所以这就是我们所说的 智能连接搭建
当你调用start()时 我们所做的第一件事是 我们获取你的端点 然后我们评估当前可用的 所有网络是什么 在这个例子中 我们有Wi-Fi和蜂窝网络 通常我们更喜欢Wi-Fi网络 因为它对用户来说成本更低 所以我们先看看那个
然后我们检查一下 该网络上是否有任何特殊配置 有VPN吗?有代理吗? 我们将为你评估 在这个例子中 假设有一个配置了 自动配置文件的代理 如果代理不适用于你的连接 它也可以直连 所以我们将评估这两个选项 我们将检查是否需要使用代理 继续并连接到它 在那里创建一个TCP连接
但如果我们不需要它 我们将替你执行DNS 直接连接 取回所有回复的DNS IP地址 并连接到它们 一个接一个地让它们并行执行 我们比较它们的速度 来为你提供最快的连接
然后 如果Wi-Fi出现问题 假设Wi-Fi信号质量非常糟糕 因为你离开了大楼
我们实际上可以利用 称为Wi-Fi协助的功能 并无缝回退到蜂窝网络 在那里进行DNS解析 并一个接一个地尝试连接 所以这样你的连接搭建非常有弹性 处理VPN 为你处理代理 并为你提供可能的最佳连接
现在 当然 你可能不想尝试所有这些选项 你可能希望限制连接搭建的行为 所以我们有许多不同的旋钮 和控件让你这样做 我今天要强调其中的三个
首先 你可能不想使用昂贵的网络 比如蜂窝网络 因为此连接仅适合使用Wi-Fi 所以在连接的参数中 具有控制你所使用接口的选项 所以如果你不想使用蜂窝 只需将cellular添加到 prohibitedInterfaceTypes中
实际上将一般的 昂贵网络都禁止掉甚至要更好 因为这也会屏蔽掉 比如说 Mac上个人热点
另一种限制连接搭建的方法是 明确指定你想要使用的 IP地址系列 假设你非常喜欢IPv6 因为它更快 而且它是未来的趋势 你根本不想在连接上使用IPv4 为此你可以使用参数 找到其中针对IP的选项 并且在这里你可以看到 该选项 与你在当前套接字中的 套接字选项相类似 你还可以明确指定要使用的IP版本 这会影响你的连接以及DNS解析
最后 你可能不想 在你的连接上使用代理 也许你的连接不适合 使用SOCKS代理 在这种情况下 你也可以简单地禁止使用代理
这就是准备状态中所发生的事情
我之前提到过 事情可能会出错 在尝试建立连接时 你可能没有可用的网络 我们在进入准备状态之后所做的是 如果我们发现没有好的选择 DNS失败 没有网络 也许你处于飞行模式 我们将进入等待状态 并让你知道其原因 并且每次网络发生变化时 我们都会回到准备状态 这时系统认为 “现在你的连接说不定 能建立成功了呢” 我们将为你处理所有这些事情 并且每次我们重新尝试时都会通知你
最终你会有希望成功建立连接 此时我们将进入就绪状态
就像我之前提到的那样 就绪状态是你的连接完全建立的时候 此时已准备好了你的协议栈中 一直到上层的协议 例如TLS 此时你可以读和写 这也是我们对你所经历的网络切换 进行回调的地方 因此如果你的连接已建立 然后你更改了网络 我们会为你提供相应的更新 以便你可以优雅地处理移动性 我们稍后再继续讨论这个问题
如果连接出错 无论是在连接建立期间 还是已经连接后 你会得到一个错误并进入失败状态
然后一旦你用完了连接 假设你已经把它关闭了 或者你从另一端收到了关闭请求 并且你想要使连接无效 你可以调用cancel() 我们就会进入取消状态 这必定是我们将传递给 你的对象的最后事件 以便你可以清理所使用的任何内存 并继续进行下一步
就是这样 这是对 Network.framework中连接对象的 基本生命周期的概述 为了向你展示如何使用它 来构建一个简单的app 我想邀请Eric上台
谢谢Tommy 我是Eric Kinnear 我也来自Apple的网络团队 我非常高兴能与你一起 使用Network.framework 构建一个示例app 我们将使用Tommy先前提到的 视频直播用例 构建一个可以在一台设备上 使用来自摄像头的输入 并通过网络发送 以便在另一台设备上显示的app
由于我们将持续生成实时视频帧 我们将使用UDP通过网络 发送这些数据包 那我们该怎么做呢?
首先… 我们需要与相机进行捕捉会话 以便我们可以从图像传感器 接收视频帧 为了这个例子 我们不打算使用 任何视频编解码器 或其它压缩算法 我们只是从相机中取出原始字节 通过网络发送它们 并在另一端进行显示
为此我们需要将这些帧分割成 我们可以用UDP数据 发送的较小块
当然 要想在网络上发送 这些UDP数据包 我们需要一个连接
对于另一台设备 我们需要一个监听器 它可以接收传入的连接 并从网络上读取数据包 此后我们只是逆转了之前的过程 重新组装视频帧并将其发送到显示器 以便我们能在屏幕上看到它们
为了简单起见 我们已经抽象出了相机 和显示功能 这样我们就可以专注于使用 Network.framework的部分
这里有一个我们尚未涉及的内容 它就是监听器 所以我们现在花点时间来了解它
监听器功能 由NWListener类提供 你可以使用与你用于配置连接的 相同的参数对象来创建它
设置一个监听器来发布 一项Bonjour服务非常简单 在这个例子中 我们将使用_camera._udp
当监听器收到新连接时 它会将该连接传递给你提供的 newConnectionHandler块中 这是你执行针对该连接的 所选配置的机会 然后你需要调用start() 来让这个连接知道是时候开始了
同样你也需在你的监听器上 调用start() 也像连接一样 你需要提供一个调度队列 即你希望在哪里调度这些回调
这就是监听器 如果你仔细想想 我们刚实现了 相当于在UDP套接字上 调用listen()的效果 除了listen() 无法在UDP套接字上工作
我们已准备好在Xcode中 构建app了 这是我们的app 我们这里有一堆文件 它们已经处理了相机和显示功能 所以我们将只关注 UDPClient类 和UDPServer类 UDPClient 将负责创建与另一端的连接 并发送帧 同样的 服务器负责创建监听器 接受传入的连接 从该连接中读取数据 并将其发送到屏幕 让我们从客户端开始
我的客户端类有个接收一个名字的 初始化函数 它是描述我们要连接的 Bonjour服务名称的字符串
我通过调用 NWConnection 并传入服务端点 来创建我的连接 使用提供给我的名称 并将_camera._udp 作为类型
我们还传递了默认的UDP参数
正如Tommy所说我们可以使用 stateUpdateHandler
来检查就绪状态和失败状态 这里 当我们的连接就绪 我们将调用sendInitialFrame() 我们稍后再实现它
因为我们使用的是UDP 并且没有其它握手过程 我们将一些数据通过网络发送到 其它设备 并在我们开始生成大量视频帧 并将其放到网络中之前 等待它的回应
我们需要记住在我们的连接上 调用start() 并且传入上面创建的队列
让我们实现 sendInitialFrame()
这里我们使用 文字字节“hello” 创建一个数据对象 要在连接上发送内容 我们可以调用 connection.send() 并提供该数据对象作为内容
我们提供了一个完成处理程序 以便检查发送过程中 可能遇到的错误
由于我们预期该内容可以 立即得到回应 我们马上调用 connection.receive() 来从连接中读取传入的数据 在该完成处理程序中 我们验证该内容是否存在 如果存在 我们让app的其它部分知道 我们已经连接上了 它应该调出相机硬件 并开始生成帧
生成这些帧时 app的其余部分知道 在UDPClient类上调用send()
并向其传递一个表示 我们想要发送的视频帧的 数据对象数组
由于我们要在很短的时间内 进行大量发送操作 我们将在一个我们传入 connection.batch 的块内做这件事
在这个块中 我们将遍历该 数据对象数组的每一帧 并将每个数据对象都传递给 connection.send() 与上面类似 我们使用完成处理程序来检查 发送时遇到的任何错误 就是这样 我们已经有了UDPClient类 并且我们已经准备好了
让我们来看看服务器
在服务器端 我们需要一个监听器 它可以接收传入的连接 我们需要回应 我们刚从客户端发出的握手请求 我们需要从网络上读取数据 以便我们可以将其推送到显示器 从监听器开始
我们只是使用默认的UDP参数 创建一个NWListener
如果我愿意的话 这也是 使用这些参数告诉监听器 监听特定的本地端口的时机 但由于我们用Bonjour服务 我们不需要这样做
为了设置该服务 我将listener的 服务属性设置为 _camera._udp类型的 服务对象 注意我没有在此处传递名称 因为我希望系统为我提供 默认的设备名称
我还为serviceRegistration UpdateHandler 提供了一个块 它将在系统发布的 端点集发生改变时被调用 这里 我对添加端点的情况感兴趣 如果它是服务类型 我将告诉app的其余部分 它所发布的名称 即我要求系统提供的默认设备名称 以便我可以在UI中显示它 并让我的用户在其它地方输入它
我要在监听器上设置一个新的 连接处理程序 它在每次监听器收到新的 传入连接时都会被调用
我可以对这些连接进行一些配置 但这里使用默认配置就够了 因此我只需调用connection.start() 并传入一个队列
我在这里通知app的其余部分 我已收到传入连接 以便它可以开始预热显示管道 并准备好显示视频帧
我也会调用自身receive() 我稍后再实现它 来开始从网络中读取该数据 并将其发送到显示管道
就像连接一样 监听器也有
stateUpdateHandler 我将用它来检查就绪状态和失败状态
别忘了启动我的监听器 为此我调用 listener.start() 并传入我们在上面创建的队列
现在我准备好了监听器 我只需要从网络上读取数据 并实现该receive函数
我们在这里首先调用 connection.receive() 并传入完成处理程序 当数据进入该连接时 我们将检查我们是否尚未连接 如果我们没有连接 这可能是客户端通过发送 所启动的握手过程
我们只需调用 connection.send() 并将相同的内容传回去 以便它回显给客户端 然后我们记录下连接已经建立 在此后的 所有receive()回调中 我们将告诉app的其余部分 我们收到了这个帧 它应该将其发送到显示管道 以便我们能在屏幕上看到它
最后 如果没有错误 我们再次调用receive() 以便我们接收后续帧 并将其发送到显示器 并将这些独立的图像拼接成视频
就是这样 我们有了UDPClient 也有了UDPServer 让我们试一试
我将在我的手机上运行客户端 并在Mac上运行服务器 以便我们可以在大屏幕上看到它
这里 服务器刚刚启动 我们看到它正作为 Demo Mac进行公布 这是我让系统其余部分 给我这个名称的地方 在我的手机上 如果我点击连接
一瞬间 我就可以通过UDP Live看到 通过网络流式传输的视频帧
我们刚才看到了我能够多么快地 启动一个UDPClient 并连接到Bonjour服务 它可以发送握手 等待其被处理 获取来自相机的视频帧 并通过网络发送它们
服务器端启动了 一个Bonjour监听器 它公布了一项服务 并接收传入的连接 然后对握手进行回应 并将它们全部发送到显示器 以便我们可以看到它们 现在为了更详细的介绍 优化该数据传输过程 我想邀请Tommy回到舞台上
谢谢 Eric 这是一个非常酷的演示 它很容易实现 现在我们已经介绍了基础知识 我们知道如何建立出站连接 如何接收入站连接 但Network.framework的 真正关键部分 也是这里的杀手级特性 是其优化性能的方式 我们将超越套接字所能够做的事情
我想先谈谈 你在app中与网络连接 进行交互的 最基本的方式 即发送和接收数据 这些调用非常简单 但是关于你如何处理 发送和接收的细微差别 将对app的响应能力 以及设备和网络上正在进行多少缓冲 有着巨大影响
所以我想介绍的第一个例子 是当我们在app中 发送数据时 就像Eric刚向你展示的那样 一些正在直播的东西 一些动态生成数据的东西 但在这个例子中 让我们谈谈 使用TCP流来发送的情况 即可以在网络上恢复的TCP流 它具有可以发送的特定时间窗口 我们该如何处理该情况呢
这是一个发送单帧的函数 这是你的app生成的某些数据帧
你在连接上发送它的方式 是通过调用 connection.send() 并传入该数据 如果你习惯在连接上 使用套接字发送数据 你要么使用阻塞套接字 在这种情况下 如果你要发送100个字节的数据 如果发送缓冲区中没有空间 它实际上会阻塞你的线程 并等待网络连接消耗掉这些数据
或者 如果你使用的是非阻塞套接字
该发送过程实际上可能 不会发送你的全部数据 它会说 “哦 我先只发送50个字节 过一段时间再发送剩下的50字节” 这要求你和你的app需要处理很多 关于你实际发送了多少数据的状态
所以NWConnection 的好处在于 你可以一次发送所有数据 你不必担心这些问题 并且它不会阻塞任何东西
然而你必须处理 如果连接正在恢复该怎么办 因为我们不想不必要地 将大量数据 发送到此连接 如果你想要一个响应性强的 实时数据流的话
这里的关键 是我们提供给你的回调块 它被称为 contentProcessed 每当网络栈消耗你的数据时 我们都会调用它 这并不意味着必须发送数据 或另一方进行确认 它完全等同于阻塞套接字的调用 返回的情况 或非阻塞套接字 能够消耗你发送的 所有字节的情况
在此完成处理程序中 你可以检查两件事 首先 你可以检查错误 如果有错误 这意味着在我们尝试发送你的数据时 出现了问题 这通常表示整体连接失败
然后 如果没有错误 这是一个绝佳的机会 来查看你的app中 是否有更多数据要生成 所以如果你正在生成实时数据帧 你应该发送它并从视频流中 获取另一帧 因为现在正是你将下一个数据包 放入队列的时机 这使你可以一步一步地发送所有数据 正如你在这里看到的 我们实际上使用此异步发送回调 形成了一个循环 从而持续从我们的连接中消耗数据 并非常优雅地处理它
关于发送我想指出的另一件事 是Eric之前展示的 对UDP app很有效的技巧 这类app需要一次发送多个数据报
如果你需要发送一大堆很小的数据 或独立数据包
你可以使用我们添加的 称为connection.batch的东西 UDP套接字以前一次只能发送 一个数据包 这可能效率很低 因为如果我需要发送 100个UDP数据包 它们每个都是不同的系统调用 不同的副本 并且导致内核中大量的上下文切换
但如果你在那个块中调用batch 你想调用send或receive 多少次都可以 并且连接将停止处理任何数据 直到你完成此batch块 并尝试将所有这些数据报 作为单个批次传入系统 理想情况下 只有一个上下文切换到内核中 并最终从接口发送出去 这使你非常非常有效率
以上就是发送 接收与发送一样是异步的 而异步性质则会给你 能够让你调整app的背压
在这个例子中 我有一个基于TCP的协议 并且app想要读取某种类型的 记录格式非常常见
假设你的协议有一个10字节的头部 它告诉你有关你将要收到的 内容的一些信息 比如你将要接收的主体长度
因此你想首先读取该头部 然后读取你的其它内容 也许你的内容很长 假设有几兆字节
使用传统的套接字时 你可能会尝试读取10个字节 你可能得到10个字节或更少 你须继续读取 直到获得正好10个字节 从而读取头部信息 然后你还要读取几兆字节 你会读取一些数据 进行一大堆不同的read调用 并且需要在你的app 和堆栈之间来回切换
使用NWConnection的情况下 当你调用receive()时 你可以提供你想要接收的最小数据 和最大数据 所以你实际上可以指定 如果你想要接收正好10字节的话 因为那是你的协议所规定的 你可以说 “我想要最小10字节 最大10字节 即给我正好10个字节” 而我们只会在以下情况发生时 才对你进行回调 即要么从连接读取数据发生错误 要么我们正好读了10个字节 然后你就可以读取 头部中你需要的内容 比如长度 然后假设你想继续读取 几兆字节 你基本上只需做同样的事情 来读取你的主体 你只需传入 你想要从连接中读取的数量 这可以让你不必 在堆和app之间来回切换 这是通过在所有数据准备好时 只进行一次回调来做到的
所以这是一种优化交互的好方法
除了发送和接收 我还想强调一下 你的网络参数中的 几个高级选项 它们允许你配置连接 从而当你实际发送和接收数据时 具有合适的启动时间 以及网络上的行为 第一个 是我们在WWDC中多次讨论的内容 它是ECN 即显式拥塞通知
它为你提供了一种平滑连接的方法 即让终端主机知道 何时发生了网络拥塞 从而使我们可以很好地调整速度 最棒的是默认情况下 所有的TCP连接都会启用ECN 你无需做任何事情 但过去在基于UDP的协议上 使用ECN非常困难 所以我想在这里向你展示 如何做到这一点
你要做的第一件事是 创建一个ipMetadata对象 ECN由IP数据包中的标志控制 所以这个ipMetadata对象 允许你在每个数据包上设置各种标志 并且你可以把它包装成 一个context对象 它描述了你想与某次send调用 相关联的若干协议的所有选项 以及该特定消息的相对优先级
然后使用 此context作为额外参数 传入send调用中 就在你的content参数后面 所以现在当你发送它时 由此内容生成的任何数据包 都将包含你想要标记的所有标志 所以这很容易
而且你也可以在收到连接时 获取这些相同的标志 你将在收到的数据包中得到 与其关联同样的context对象 你将能够读出你想得到的 特定的低级标志
类似地 我们也有服务类 这是一个在URLSession中 也存在的属性 它定义你的流量的相对优先级 这会影响我们在发送数据时 流量在本地接口上排队的方式 及在Cisco Fastlane 网络上流量的工作方式
你可以通过使用参数对象中的 serviceClass参数 来将服务类标记为整个连接上的属性 在这个例子中 我们展示了 如何使用后台服务类 这是将你的连接标记为 较低优先级的好方法 我们不希望它妨碍用户交互式数据 所以如果你需要后台数据传输 我们真的很鼓励你 将它们标记为后台服务类
但你也可以对那些UDP连接 在每个数据包的基础上 标记服务类 假设你有一个连接 其中在同一个UDP流上包含 语音和信号数据
在这种情况下 你可以创建我之前介绍的 ipMetadata对象 在这里标记你的服务类 而不是之前的ECN标志 将其附加到context上 然后将其发送出去 这样你就在每个数据包的基础上 标记了优先级
你可以优化连接的另一种方式 是减少为了建立它们所需的往返次数 所以在这里我想强调 两种方法来做到这一点 第一种是在你的连接上启用快速打开 TCP快速打开允许你 在TCP发出的第一个数据包中 发送初始数据 即在SYN包中 这样你就不必等待整个握手过程完成 才开始发送你的app数据
为此 你的app 需要与连接达成协议 并说明你将提供此初始数据 以发送出去 为了启用它 你需要在参数上 标记allowFastOpen 然后创建连接 在你调用start()之前 你实际上可以调用send() 并将你的初始数据发送出去
现在我想指出 这里的完成处理程序 被替换为该数据为幂等的标志 幂等意味着可以安全地重新发送数据 因为初始数据可能会 通过网络重新发送 因此如果重新发送的话 你不希望它有任何副作用
然后你只需调用start() 当我们正在启动连接时 我们之前提到过的所有尝试 如果可以的话 我们将使用 该初始数据在TCP快速打开中发送
我还想指出使用TCP快速打开的 另一种方法 它不要求你的app发送自己的数据
如果你在TCP之上使用TLS 来自TLS的第一条消息 即客户端hello 实际上可以用作 TCP快速打开的初始数据 如果你只想启用此功能 而不提供自己的快速打开数据
只需找到针对TCP的选项 并在那里标记你想要 启用快速打开 它会在连接建立期间自动将 来自TLS的第一条消息发送出去
你还可以做另外一件事 来优化你的连接建立过程 并节省一次往返 这是Stuart 在前一个演讲中提到的 我们称之为乐观DNS 这允许你使用以前过期的DNS回复 其生存时间可能很短
并尝试连接到它们 同时我们并行执行一个 新的DNS查询 所以… 如果你之前收到的地址 虽然过期但却仍然有效 并且你将expiredDNSBehavior 标记为allow 当你调用start()时 我们将首先尝试连接这些地址 而不必等待新的DNS查询完成 这可以减少大量的连接建立时间 但如果你的服务器的确改变了地址 因为我们正在尝试 多种不同的连接选项 如果第一个不起作用 我们会优雅地等待 新的DNS查询返回 并再次尝试这些地址 所以这是一种非常简单的方法 如果它适合你的服务器配置 你就可以更快地建立连接
我想谈的关于性能的下一个领域 你实际上并不需要在app中 执行任何操作就可以得到 这是你在使用URLSession 或Network.framework连接时 自动得到的东西 这就是用户态网络 这是我们去年在WWDC上 介绍的内容 并在iOS和tvOS上都启用了它 我们在这里完全避开了套接字层 我们将传输栈移到你的app中 为了让你知道它是做什么的 我想先介绍一下通常的 传统栈模型是什么样的
假设你正在通过网络接收数据包 这里是Wi-Fi接口 该数据包先进入驱动程序 然后被发送到内核中的 TCP接收缓冲区
然后当你的app读取套接字时 将进行一次上下文切换 并将数据从内核复制到 你的app中 接着一般情况下如果你使用了TLS 它必须进行另一次转换来解密该数据 然后才能将其发送到app
那么当我们进行用户态网络时 这看起来如何呢?
你可以看到 主要的变化是我们将传输栈 即TCP和UDP 向上移到你的app中 这有什么好处呢? 现在当一个数据包从网络进入时 像以前一样先进入驱动程序 但我们将其移动到一块内存映射区域 你的app可以 自动从中获取这些数据包 不需要复制 也没有做额外的上下文切换 并自动开始处理数据包 这样我们需要做的唯一转换 就是我们为TLS无论如何 都要做的解密
这确实可以减少用于 发送和接收数据包的CPU时间 特别是对于UDP这类协议 你需要直接在你的app中使用它 来回发送大量数据包
为了说明这是如何工作的 及它可以产生的效果 我想给你看一个视频 它是使用Eric之前向你展示的 那个app拍摄的 并通过用户态网络 来演示UDP的性能
在这个例子中 我们将会有 两个视频同时播放
左侧的设备正在接收一个视频流 其来自… 使用套接字编写的app
右边的设备 将接收完全相同的视频流
其来自一个设备上的 一个app 该app使用 Network.framework编写 因此它可以利用用户态网络栈
在这个例子中 我们将视频以流式传输 它只是原始帧 并没有被压缩 它没很好质量或其它东西 但是有大量数据包来回传递 在此演示中 我们不会降低视频质量 无论是在我们遇到争用时 还是当我们无法足够快的 发送数据包时 或是不丢弃任何东西时 而是在必要时放慢速度 你的app可能在实际中不会这样做 但它突出了这两种栈之间的 性能区别 现在让我们看看吧
它们使用的数据完全相同 以它们各自尽可能快的速度 在网络上 发送完全相同的帧
我们看到右边的那个 很容易就超过了左边的那个
事实上 如果你看看其差异 我们仅在接收端就看到了 30%的开销削减
这是因为在比较套接字 和用户态网络时 我们看到它们发送和接收 UDP数据包 所需的CPU百分比存在巨大差异 这只是一个例子 这不会是每个app的样子 因为你将以不同方式进行压缩 你将会尝试提高连接的效率 但如果你有一个 生成实时数据的app 尤其是如果你使用UDP 来发送和接收大量数据包 我邀请你尝试在你的app中 使用Network.framework 并在工具软件中运行它 测量当你使用Network.framework 与套接字时 它们各自的CPU使用量的差异 我认为你会对所看到的结果感到满意
我们今天要讨论的最后一个话题 是解决网络移动性问题的方法 这是我们使用 Network.framework
尝试解决的关键领域 它的第一步 只是确保我们能够优雅地开始连接 我们已经提到了这一点 但我想稍微回顾一下 等待状态 是在你的连接刚建立时 处理网络切换的关键 当你正在做DNS或TCP时 它会指出当前无连接 或连接已更改
我们鼓励你避免使用 可达性之类的API 来在建立连接之前检查网络状态 这将导致竞争条件 并可能无法提供准确的信息 来描述连接中实际发生的事情
如果你需要确保你的连接 不会通过蜂窝网络建立 不要… 预先检查设备当前是否运行在 蜂窝网络上 因为那可能会改变 只需使用 NWParameters来限制 你要使用的接口类型
一旦你启动了你的连接 并处于就绪状态 我们会为你提供一系列事件 来让你知道网络何时发生变化
第一个被称为连接可行性 可行性 表示你的连接能够通过该接口 发送和接收数据 即它有一个有效的路由
为了演示这一点 若当设备连到Wi-Fi网络时 你开始建立连接
然后 你的用户走进电梯 他们失去了信号 此时我们将会给你一个事件 让你知道连接不再可用 那么这时你应该怎么做呢?
两件事 我们建议 如果这对你的app来说 合适的话 你可以让用户知道他们目前没有连接 如果他们试图发送和接收数据 现在这将无法正常工作
但是 你不一定要拆掉该连接 反正此时你也没有 可用的更好的接口 并且之前的Wi-Fi接口 可能会回来 通常 如果你走出电梯 并回到同一个Wi-Fi网络范围 你的连接立即可以在断开的地方恢复
我们给你的另一个事件 是更优路径通知
我们还是使用刚才的例子 即你通过Wi-Fi网络连接
假设你走出一幢建筑 现在失去了Wi-Fi信号 但你现在可以使用蜂窝网络 此时我们会让你知道两件事 首先 你的连接不再像之前一样可用 但我们也会告诉你 现在有一条更优的路径可用 如果你再次连接 你就能够使用蜂窝网络
这里的建议是 如果这对你的连接合适的话 尝试迁移到新连接 如果你能恢复刚才正在进行的工作
但只有在新连接完全就绪后 才应该关闭原始连接 同样 Wi-Fi网络可能会回来 或蜂窝网络连接可能会失败
我想在这里强调的最后一种情况 是用户最初通过蜂窝网络连接 然后用户走进建筑物 现在他们可以访问Wi-Fi网络 在这种情况下 你的连接 即原来的那个完全没问题 它仍然可用 但你现在也有更优的路径可用
同样的 在这种情况下 如果你可以迁移你的连接 这可能是尝试建立新连接并使用它 来发送数据的好时机 这将为用户节省流量账单
但请继续使用原始连接 直到你完全建立新的连接
我们来看下代码的样子 你可以在连接上设置一个 viabilityUpdateHandler 它会返回一个布尔值 来让你知道当时连接是否可用 以及一个betterPathUpdateHandler 它告诉你何时有更优的路径可用 或不再可用
现在 处理网络移动性的 更好解决方案 是我们前几年所谈到的 多路径连接 多路径TCP 如果你能够在服务器上 启用多路径TCP 并且在客户端启用参数中的 multipathServiceType 那么你的连接将随着网络的变换 而在它们之间自动迁移 这是一种非常棒的无缝体验 你的app不需要处理任何工作
这也是URLSession中 可用的相同服务类型
关于Network.framework 我想在此强调几点 若你在NWParameters中 限制可以使用的接口类型 这将适用于MPTCP 所以在使用多路径连接时 你仍然可以选择 不使用蜂窝网络 这样我们只会在可用的 不同Wi-Fi网络之间 进行无缝迁移
此外 之前我提到的 连接可行性处理程序 与多路径TCP略有不同 因为每当我们更改网络时 我们都会自动为你迁移 你的连接 只有在你完全没有可用的网络时 才是不可用的
因此在等待连接之间
可用性 更优路径 MPTCP 我们真的希望你的app中 所有这些使用 SCNetworkReachability 来手动检查网络更改的用例 都能被替换掉 然而我们的确意识到了 在有些情况下你仍然想知道 有哪些可用的网络 以及它什么时候改变
为此Network.framework提供了 一个新的名为NWPathMonitor的API
这个PathMonitor 并不监视可达性 或尝试预测给定主机的可达性 而只是让你知道 你的设备上接口的当前状态是什么 以及它们何时改变
它允许你遍历 你可以连接的所有接口 以备你想要在其中每个上面 都建立连接
并且只要这些网络发生变化 它就会通知你 这可能非常有用 如果你想要更新UI 来让用户知道他们是否已连接的话
如Stuart在上次演讲中提到的 可能有这样的场景 其中用户需要填写一个很长的表格 但他们可不想在填完的时候 才意识到他们没有连接 因此在这些情况下使用 只是等待连接的 NWPathMonitor是不够的 所以在所有这些情况下 我们真的希望看到人们 不再使用可达性 并比以往更优雅地处理网络转换
接下来 我想邀请Josh 回到舞台上来并告诉你如何参与 并开始使用 Network.framework
谢谢 Tommy 我们有一个很棒的新API 我们认为你会爱上它 我想谈谈为了立即开始使用它 你可以做的事情 但首先我要谈一些 我们希望你不要做的事情 以便我们可以真正利用这些新技术 比如用户态网络
若你使用的是macOS 并且有一个网络内核扩展 而且你正在该网络内核扩展中 执行某些用其它方法实现不了的操作 请立即与我们联系 我们会为你提供更好的选择 因为网络内核扩展不兼容 用户态网络
你需要注意 对于URLSession
代理自动配置将不再支持 FTP和文件URL 以后唯一支持的URL方案 将是HTTP和HTTPS
我们希望你停止使用 Core Foundation层 的许多API 它们最终会被弃用 虽然它们尚未标记为已弃用 它们是CFStreamCreatePairWith 开头的任何与套接字相关的东西 以及CFSocket 它们不能很好利用我们通过 Network.framework 引入的 许多关于连接建立的优化 并且也无法利用新的用户态网络 我们真的希望你能摆脱它们 以利用Network.framework 和URLSession带来的 强大的连接性改进 和性能改进
还有一些Foundation API 我们也希望你不再使用 如果你使用NSStream、NSNetService 或NSSocketPort这些API中的任意一种 请改为使用Network.framework 或URLSession 最后 如果你使用的是 SCNetworkReachability 我们觉得等待连接模型 是一个更好的模型 所以我们真的希望你改为使用它 对于那些等待连接并不合适的 少数情况 以后NWPathMonitor 是一个更好的解决方案
我们已经说了一些不希望你做的事情 现在我想专注于 我们真的希望你做的事情 从此以后 我们平台上的网络首选API 是URLSession 和Network.framework URLSession 主要针对HTTP 但流任务提供了非常简单的 TCP和TLS连接
如果你需要更先进的东西 Network.framework 为你提供了 对TCP、TLS UDP、DTLS的更好支持 它处理监听入站连接 以及出站连接 并且我们用PathMonitor 来处理一些移动性问题
下一步 我们真的希望看到你采用 Network.framework 和URLSession 你的客户将会赞赏 你建立的连接有多么可靠 并赞赏更好的性能所带来的 更长的电池续航时间
当你做这些事时 请关注于
你如何处理发送和接收 来真正优化其性能 并多花些时间来获得 对可行性和更优路径变化的支持 它对于提供无缝的网络体验 至关重要
现在我们知道Network.framework 还不支持UDP组播 所以若你正使用UDP组播 我们真的很想了解你的用例 以便以后我们可以将这些考虑在内
此外 如果你有任何其它问题 或增强请求 我们很乐意听取你的意见 请联系开发人员支持 或到访我们的实验室 午饭后两点我们有一个实验室 并在明天上午九点还有一个
更多详细信息 请参阅此URL
别忘了明天早上和午餐后的实验室 非常感谢 祝你们度过愉快的WWDC
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。