作为客户端开发者,如果你的 App 中有图片上传功能,而且某天测试人员拿着手机告诉你图片总是上传不上去,或者进度条走的很慢,你的第一反应很有可能是「网络不好?」。网络到底是个什么概念,网速为什么会不好,如何预估当前网速是否合理,分析这类问题,背后需要建立全面且广阔的技术视野。
无论是上行数据通道(数据从客户端发往服务器)还是下行通道(数据从服务器发回客户端),一次完整的网络行为(比如 HTTP 请求)涉及到的硬件设备有很多,影响面也不会止于通讯的两端。当我们尝试去分析是否因为网络问题导致 App 行为异常的时候,需要将各方面可能的因素纳入考量,一步一步抽丝剥茧直达真相。
Debug 的时候,所有游戏的参与者都值得被怀疑。
iPhone
测试人员手里的 iPhone 设备是第一个值得怀疑的对象,作为网络数据的发送方或者接收方,iPhone 有太多的方式去影响网络行为了,比如:
测试人员在 iPhone 的 Setting 中关闭了 App 的 Push,导致 Push 收不到。
测试人员将 iPhone 设置了夜间模式,导致锁屏收不到 Push。
测试人员将 iPhone 的开发者模式打开,并设置 100% 丢包,所有请求失败。
测试人员将 iPhone 连接了另一个不可用的 WIFI。
测试人员将 iPhone 的自动同步日期功能关闭,并将手机设置成 10 年前的某个时间,导致所有的 HTTPS 请求证书校验失败,请求无法成功。
测试人员。。。
作为网络行为的第一环节,iPhone 设备就有很多的可能性导致请求出现问题,优秀的测试人员会竭尽所能去探索 App 边际的异常行为,工程师如何去快速定位排除各类异常 case,只能靠年复一年的工程实践和经验积累。
路由器
大多数时候,网络包离开 iPhone 后下一个目的地可以大致分为两类,如果是 2G/3G/4G ,走较近的基站,如果是 WIFI,则走房间里的路由器。
这两类场景需要分开测试。
这几年随着国内 4G 的普及,4G 的覆盖面已经相当之广,其稳定性甚至已慢慢超过电信、网通的 WIFI 线路,我相信并不是只有我一个人会偶尔在 WIFI 不稳定的时候切换成 4G 看视频。
另一个原因是由于 Cellular Network 和 WIFI Network 运营商不同,有时候网络行为会存在差异。比如有些运营商会在 HTTP 请求中插入自己的代码,有些运营商对于 TCP 长连接设置较短的 NAT 超时时间,导致心跳失效。
路由器可能引起的网络问题有两类,一是信号干扰,二是路由器本身携带的安全机制。现在大部分写字楼的办公环境下,都能一次性搜出十多个 WIFI 信号,如果外部的 WIFI 信号碰巧和你路由器在使用同一频道,即使信号不强,也会造成数据的拥挤,进而导致「网速不好」的感知。另外是路由器的安全机制,现在很多给企业用的路由器都自带一键屏蔽 QQ,微信等 App 的安全功能,或者针对通讯协议进行屏蔽,比如丢弃所有除 53 端口的 UDP 包,直接导致一些走 UDP 的 VOIP 和 VPN 软件不可用。
公网
网络包离开网关,进入公网后,其网络行为将变得难以预测,公网代表的是互联网的基础设施,就像是高速公路。一辆货车在计划好路线,预估好车速之后,也无法准确的计算抵达目的地所耗费的时间,因为路上会有意料之外的拥堵,事故或者计划变更。
我们能做的是,理解网络包传输的整个流程,理清所有可能的影响因子,再借助网络工具分析,尽可能的接近网络传输的真相。
iOS 开发工程师绝大多数时候都是在和 TCP 打交道, 我们对于网络请求的分析是否准确取决于我们对于协议的理解程度。大多数请求(比如 HTTP 上传图片文件)都是从客户端发起,之后服务器响应,客户端 App 更多的是充当发送方的角色。TCP 连接的传输效率(主观上对于网速的感知)受制于 Flow Control 和 Congestion Control,有很明显的流量特征,大致分为以下几个阶段。
阶段一:三次握手。
阶段二:慢启动。
阶段三:到达最大发送窗口,遇到丢包,发送窗口减小。
阶段四:n 个 RTT 之后,发送窗口进一步达到最大值。
阶段三与阶段四之间往复。
上述几步是简化抽象的描述,深入了解 TCP 流控和拥塞控制的细节后,才能对这几个环节或者抓包的流量变化有更加准确的了解。
在设计 TCP 的早期,协议的设计者就认为互联网的基础设施相对于使用者来说是不够用的(总体带宽不够,或者理解成自来水管道容量不够)。因此为了避免管道出现拥塞,TCP 在刚开始建立连接的时候,发送方会通过 Slow Start 的方式去试探管道的容量,以避免初期大流量所导致的整个互联网管道拥塞。从 Slow Start 开始探测容量到发送方逐步到达最大发送速率,中间有一套流程机制严格控制。
发送方当前能够发送的数据包(能够注入管道的流量)是由 Send Window (Send Buffer,以 Bytes 为单位)控制的。而 Send Window 又受到以下几个因素影响:
Congestion Window (cwnd),cwnd 的概念引入是为了推测或者感知当前管道容量的大小,判断是否出现了拥塞。如果有一个包被成功 ack,那么可以推测管道容量够用,那么我们可以增大 cwnd 的大小,如果一个包发送超时,那么很有可能管道出现了拥塞,那么我们需要减小 cwnd 的大小。显然 Send Window 的大小不应该超过 cwnd 的值,因为如果出现拥塞,发送再多的包也只会加重 Congestion。可以说 cwnd 的值决定了发送方发送数据流量的变化。
cwnd 在连接的初期的变化有两种,第一种是指数级增大(2->4->8),第二种是加法增大(8->9->10)。第一种方式即为 slow start,cwnd 有一个较小的初始值(这个值也有个计算公式,感兴趣的可以查看 rfc 5681),但每一次 RTT 之后获得一次指数级增大的机会,这样可以在三次握手之后快速的达到一个较大的 cwnd 值。第二种方式称之为 congestion avoidence,是为了避免 cwnd 增长过快所带来的流量压力。两种方式的分水岭是 ssthresh(slow start threshold) 值:
- if cwnd < ssthresh,则采用 slow start。
- if cwnd > ssthresh,则采用 congestion avoidence。
- if cwnd == ssthresh,则采用 slow start 或者 congestion avoidence。
slow start 和 congestion avoidence 是连接初期 cwnd 的变化方式,连接稳定之后,如果出现丢包(RTO,或者连续收到三次重复 Ack),则认为可能出现拥塞,cwnd 值会减小。此时根据 TCP 版本的不同,cwnd 变化的方式也会不同,实际上 cwnd 值的控制存在非常多的版本,这里以 TCP Reno 的实现为例,cwnd 的值会减半,之后进入 congestion avoidence,再次以 RTT 为时间单位逐步增大 cwnd 的值。整个流程我们可以用下图表示:
Receive window (rwnd),rwnd 是另一个会影响 send window 大小的值,它表示的是接收方的接收窗口大小,在接收方的 ack 包里会带回 receive window 的大小。显然,send window 的大小不应该超过 rwnd 的值,否则则超出了接收方处理包的能力。
send window 的大小总是取 cwnd 和 rwnd 中的较小值,这样发送方在发包的时候,既考虑了网络的拥塞情况,也不会超过接收方的接收能力。
理解了上述的 TCP 拥塞控制过程,我们可以得出几个明显的结论。
结论一:高延迟会影响 TCP 的传输效率。 TCP 在前期的 Slow Start 和中期丢包导致的发送窗口减小,都需要接收方 Ack 来逐步增大发送窗口,每几次 RTT 之后,发送方有一次机会去增大发送窗口,如果两地的物理距离长,延迟高,那么 RTT 值也大,自然 TCP 的传输效率也会受到影响。延迟越高,TCP 的传输效率或者说吞吐量(Throughput)也会越低。这也是为什么,即使在不拥挤的时段,出口带宽足够的情况下,访问国外网站或者下载文件,网速也很难达到国内服务器的水准。
结论二:丢包率会严重影响 TCP 的传输效率。TCP Reno 的实现在确认丢包的情况下,会将发送方的发送窗口大小减半,只要有一个包丢失就会发生减半,这种策略本身是站在整个互联网的维度去保证整体效率,但对单条链路来说,十分影响传输效率,后续发送窗口的恢复需要更多健康的数据包发送。即使是 2% 的丢包率,也可以导致 TCP 传输效率严重下降。作为测试可以在 iPhone 的「设置-开发者」里面设置 2% 的丢包率,再上传图片文件,和正常情况下丢包对比下网速体验。
这里值得一提的是对于丢包的检测,发送方在两种场景下会认为发生了丢包。第一种是发送方的 RTO (Retransimission Timeout)被触发,发送方在一定的时间内如果没有收到对方的 Ack,则认为发生了丢包。第二种是发送方收到了三次对方重复的 Ack,这种情况表明接收方收到了乱序的包,比如先收到 Packet 1,Packet 2,再是 Packet 4,Packet 5,Packet 6,此时接收方会重复三次 Ack Packet 2,接收方收到之后,即认为 Packet 3 被丢失了。两种场景的丢包,对于发送窗口的影响也有差别,RTO 触发会将发送窗口置为 1 MSS,3 个重复 Ack 则导致窗口减半( TCP Reno 的做法,其他算法又不一样)。
了解这些技术细节的意义,在于抓包的时候能更准确的感知流量的变化是否正常。
服务器
客户端请求数据包的最后一站是服务器,一旦抵达服务器,我们能够分析的数据就只能依赖于服务器返回的 return code 了,或者是借助抓包工具分析服务器的回包行为。
结束语
学习网络协议的意义在于能准确分析可能出现的网络问题,网络行为的参与者多,环节多,客户端一旦遇到异常,需要一步步分析定位,接近问题的真相。不熟悉网络抓包的同学,可以参考我博客上之前写的几篇关于 tcpdump,wireshark,mitimproxy 的介绍文章。