WWDC Quick Look 💓 By SwiftGGTeam
Reduce network delays for your app

Reduce network delays for your app

观看原视频

Highlight

Apple 在 iOS 15 和 macOS Monterey 提供网络响应度测试、HTTP/3、TCP Fast Open、Multipath TCP 和后台服务类型,让开发者减少网络往返次数并降低用户等待时间。

核心内容

一个天气应用打开后显示加载指示器。代码很快,接口也不大。用户仍然要等一两秒。

开发者常先看带宽。家庭网络已经从几兆升级到千兆,视频会议和网页加载仍会卡。Stuart Cheshire 在开场给出原因:空闲 ping 时间不代表真实使用体验。网络被使用时,缓冲区会排队,20ms 的空闲往返延迟可能变成 600ms。

这类问题叫缓冲膨胀(bufferbloat)。包没有更快到达,只是在网络设备的大缓冲区里排队。带宽提升不能消除排队时间。

Apple 的做法分两层。

第一层是测量。iOS 15 的开发者设置加入 Responsiveness 测试,macOS 提供 networkQuality 命令。它们测的是工作状态下的响应度,用 RPM(Round-trips Per Minute,每分钟往返次数)表示。数字越高,网络越能快速完成往返。

第二层是减少往返。应用不能改变光速,也不能控制用户家里的路由器。但应用可以减少建立连接和拿到首字节所需的网络往返次数。Session 重点讲了 HTTP/3 与 QUIC、TCP Fast Open、TLS 1.3、Multipath TCP,以及如何把不紧急的流量标记为后台。

从等待首字节开始优化

如果应用当前使用 TCP + TLS 1.2,用户拿到首字节可能需要 4 次网络往返。往返延迟升到 600ms 时,首字节等待接近 2.4 秒。

HTTP/3、QUIC、TLS 1.3 和 TCP Fast Open 的价值在这里:它们减少握手阶段的往返。对于已经使用 URLSession 的应用,iOS 15 和 macOS Monterey 默认启用 HTTP/3。服务器支持后,客户端可以自动受益。

网络切换也会产生等待

用户从 Wi-Fi 走到户外,设备切到蜂窝网络。普通 TCP 连接需要重新建立。Multipath TCP(多路径 TCP)允许一个 TCP 连接在网络切换时继续存在,并让系统自动选择更快路径。

这对聊天、视频会议、远程控制这类交互应用很直接:用户移动时,连接不需要从头握手。

后台下载不该堵住前台请求

很多应用会预取图片、音频或文件。预取本身合理,但如果它把网络队列塞满,用户点击个人主页时,真正需要立即显示的数据会排在队尾。

iOS 15 和 macOS Monterey 改进了后台服务类型的拥塞控制。开发者把预取、批量同步和非紧急任务标记为后台后,系统可以让前台流量先通过,同时让后台传输接近原来的完成时间。

详细内容

测量工作状态下的网络响应度

00:36)iOS 15 的开发者设置中,Networking 区域新增 Responsiveness 测试。它会生成网络流量,测量工作状态下的响应度。macOS 上对应的命令行工具叫 networkQuality

networkQuality

关键点:

  • networkQuality 在 macOS 上运行网络响应度测试。
  • 测试关注工作状态下的往返能力,区别于空闲 ping。
  • 结果用 RPM 表示,RPM 越高,网络响应越快。
  • 这个指标适合用来复现用户抱怨“网速很快但应用很慢”的场景。

02:13)Session 给出一个具体数字:空闲往返时间可能是 20ms,工作状态下可能升到 600ms 或更高。应用等待时间由两项决定:一次往返要多久,以及应用需要多少次往返。

wait time = round-trip time × number of round trips

关键点:

  • round-trip time 是单次网络往返时间。
  • number of round trips 是应用协议完成一次操作需要的往返次数。
  • 开发者很难控制用户网络的基础 RTT。
  • 开发者可以通过协议和 API 选择减少往返次数。

HTTP/3 和 QUIC:URLSession 默认受益

08:15)iOS 15 和 macOS Monterey 默认启用 HTTP/3 和 QUIC。QUIC 是传输协议,建立连接比 TCP + TLS 更快,还能减少队头阻塞(head-of-line blocking,同一连接内丢包导致其他数据等待)。

let session = URLSession.shared
let (data, response) = try await session.data(from: url)

关键点:

  • URLSession.shared 使用系统提供的 URLSession 配置。
  • session.data(from:) 发起 HTTP 请求。
  • Session 明确说明:如果应用已经使用 URLSession,就已经准备好使用 HTTP/3。
  • 是否走 HTTP/3 还需要服务器端支持。

08:47)如果应用自己实现应用层协议,可以使用 Network framework(网络框架)的 QUIC 支持。演讲说明做法是创建带 QUIC 参数的 NWConnection,并设置 TLS Application-Layer Protocol Negotiation(ALPN,应用层协议协商)。

let connection = NWConnection(
    host: "example.com",
    port: 443,
    using: .quic(alpn: ["myproto"])
)

connection.start(queue: myQueue)

关键点:

  • NWConnection 表示一条 Network framework 连接。
  • hostport 指向服务器。
  • .quic(alpn: ["myproto"]) 使用 QUIC 参数,并通过 ALPN 声明应用协议。
  • start(queue:) 启动连接。
  • 服务器也必须支持对应的 QUIC 协议和 ALPN。

TCP Fast Open:把首次数据放进握手

09:28)TCP 仍适合一部分应用。使用 TCP 时,TCP Fast Open 可以把应用的首次数据和 TCP 握手一起发送,省掉一个完整往返。

/* Allow fast open on the connection parameters */
parameters.allowFastOpen = true

let connection = NWConnection(to: endpoint, using: parameters)

/* Call send with idempotent initial data before starting the connection */
connection.send(content: initialData, completion: .idempotent)
connection.start(queue: myQueue)

关键点:

  • parameters.allowFastOpen = true 允许这条连接使用 TCP Fast Open。
  • NWConnection(to:using:) 用这些参数创建连接。
  • connection.send(content:completion:)start 之前提交首次数据。
  • .idempotent 声明这段首次数据是幂等的,可以安全重放。
  • connection.start(queue:) 启动连接,系统可以把首次数据随握手发出。

10:14)幂等(idempotent)是关键限制。HTTP GET 请求通常可以重放;购买手机这类会产生交易的请求不能随握手发送。网络可能重传首次数据,非幂等操作会导致重复执行。

GET / HTTP/1.1
Host: developer.apple.com

关键点:

  • GET 请求获取资源。
  • Host 指定目标主机。
  • 演讲用访问 developer.apple.com 的 GET 说明幂等请求。
  • 如果确认请求重放不会产生额外效果,才适合放进 TCP Fast Open 的首次数据。

11:01)如果应用基于 Sockets,可以调用 connectx 并使用对应标志。

connectx(fd, ..., CONNECT_DATA_IDEMPOTENT | CONNECT_RESUME_ON_READ_WRITE, ...); // delay SYN
write(fd, ...); // SYN goes out with first data segment

关键点:

  • connectx 发起 socket 连接。
  • CONNECT_DATA_IDEMPOTENT 声明首次数据可以安全重放。
  • CONNECT_RESUME_ON_READ_WRITE 让连接在读写时继续。
  • write 写入首次数据。
  • 注释说明 SYN 会和第一个数据段一起发出。

TLS 1.3:默认减少一次握手往返

12:46)TLS 1.3 相比 TLS 1.2 移除一个完整握手往返,并提供更强的安全性。Session 说明它从 iOS 13.4 起已经默认用于 URLSession 和 NWConnection。

let session = URLSession.shared
let connection = NWConnection(to: endpoint, using: .tls)

关键点:

  • URLSession.shared 使用系统 TLS 栈。
  • NWConnection(to:using: .tls) 创建 TLS 连接。
  • TLS 1.3 默认启用,无需给 URLSession 或 NWConnection 额外开关。
  • TLS 1.3 定义 early data,可以把幂等请求随 TLS 握手发送,再省一次往返。

Multipath TCP:网络切换时保留连接

13:35)Multipath TCP 允许一个 TCP 连接在设备从一个网络切到另一个网络时继续存在。低延迟场景使用 interactive 模式,系统会自动为数据包选择更快路径。

// Multipath TCP
// Save multiple round-trips when switching networks

// On URLSessionConfiguration
let configuration = URLSessionConfiguration.default
configuration.multipathServiceType = .interactive

// On NWParameters
let parameters = NWParameters.tcp
parameters.multipathServiceType = .interactive

关键点:

  • URLSessionConfiguration.default 创建默认 URLSession 配置。
  • configuration.multipathServiceType = .interactive 为 URLSession 开启交互型 Multipath TCP。
  • NWParameters.tcp 创建 TCP 参数。
  • parameters.multipathServiceType = .interactive 为 NWConnection 开启交互型 Multipath TCP。
  • .interactive 适合低延迟流量,系统会处理路径选择。

把非紧急流量标记为后台

17:43)应用经常同时有两类流量:用户正在等待的前台请求,以及预取、批量同步、上传日志这类非紧急请求。如果非紧急请求填满瓶颈队列,用户点击后发出的请求会排在后面。

20:09)前台应用执行非用户发起的传输时,可以继续使用默认 URLSession,但在 URLRequest 上设置后台服务类型。Network framework 对应设置是 NWParameters.serviceClass

//Use default  URLSession, set background on URLRequest
var request = URLRequest(url: myurl)
request.networkServiceType = .background

//Set service class on parameters to apply to the  NWConnection
let parameters = NWParameters.tls
parameters.serviceClass = .background

关键点:

  • URLRequest(url:) 创建一次请求。
  • request.networkServiceType = .background 声明这次传输是后台性质。
  • NWParameters.tls 创建 TLS 连接参数。
  • parameters.serviceClass = .background 把后台服务类别应用到 NWConnection。
  • 系统可以据此保持网络队列较短,让前台数据更快送达。

20:37)如果应用启动长时间传输,并且希望应用挂起后继续执行,应使用后台 URLSession。对时间不敏感的任务,可以把 isDiscretionary 设为 true,让系统等待更合适的条件。

//Configure background URL Session

lazy var urlSession: URLSession = {
    let configuration = URLSessionConfiguration.background(withIdentifier: "MySession")
    configuration.isDiscretionary = true
    return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()

关键点:

  • URLSessionConfiguration.background(withIdentifier:) 创建后台会话配置。
  • "MySession" 是后台会话标识符。
  • configuration.isDiscretionary = true 允许系统选择合适时机执行传输。
  • URLSession(configuration:delegate:delegateQueue:) 用该配置创建会话。
  • 这个模式适合时间不敏感的下载、上传和同步任务。

服务器端也要减少发送端排队

21:17)延迟还可能出现在发送设备本身。历史上的网络栈会使用很大的发送缓冲区,包在进入网络前就可能排队几秒。Apple 已经在 2015 年为 URLSession 和 NWConnection 修复了这个问题。

setsockopt(fd, IPPROTO_TCP, TCP_NOTSENT_LOWAT, &value, sizeof(value));

关键点:

  • setsockopt 设置 socket 选项。
  • IPPROTO_TCP 表示设置 TCP 级别选项。
  • TCP_NOTSENT_LOWAT 是演讲提到的 TCP not sent low watermark socket option。
  • 该选项用于减少发送端尚未进入网络的数据排队。
  • Session 建议联系服务器运维人员确认服务器使用该选项。

核心启发

  1. 做什么:给 App 加一个“网络体验诊断”页面,提示用户在卡顿时运行系统网络响应度测试。 为什么值得做:Session 明确区分空闲 ping 和工作状态下响应度,RPM 更接近真实体验。 怎么开始:在帮助页说明 iOS Developer Settings 的 Responsiveness 测试和 macOS networkQuality 命令,把用户反馈从“网速慢”变成可记录的 RPM 数据。

  2. 做什么:为首屏接口启用 TCP Fast Open。 为什么值得做:首屏接口通常决定用户看到加载指示器的时间,Fast Open 能把幂等首次请求放进握手。 怎么开始:在 Network framework 连接参数上设置 parameters.allowFastOpen = true,并在 connection.start(queue:) 前用 .idempotent 发送只读请求数据。

  3. 做什么:让聊天、通话或协作连接支持网络切换。 为什么值得做:用户在 Wi-Fi 和蜂窝之间移动时,重新建连会引入多次往返。Multipath TCP 可以保留连接。 怎么开始:对 URLSession 设置 configuration.multipathServiceType = .interactive,或对 Network framework 设置 parameters.multipathServiceType = .interactive

  4. 做什么:把图片预取、离线包下载、日志上传标记为后台流量。 为什么值得做:非紧急流量填满队列后,会拖慢用户刚刚触发的前台请求。后台服务类型让系统控制队列长度。 怎么开始:对单次 URLRequest 设置 request.networkServiceType = .background;对时间不敏感的长任务使用 URLSessionConfiguration.background 并设置 isDiscretionary = true

  5. 做什么:建立一组网络延迟回归测试。 为什么值得做:开发机上的 5G、LTE 或快速 Wi-Fi 可能掩盖问题,Session 建议用 Network Link Conditioner 复现真实网络条件。 怎么开始:在 iOS Developer Settings 打开 Network Link Conditioner,在 macOS 从 Apple Developer 网站下载工具,用高 RTT、丢包、拥塞配置测试首屏、登录、搜索和上传路径。

关联 Session

评论

GitHub Issues · utterances