TCP 全称为 Transmission Control Protocol(传输控制协议),是一种面向连接的、可靠的、基于字节流的传输层通信协议,其中可靠性是相对于其他传输协议的优势点。TCP 为了确保数据传输的可靠性主要做了以下几点:
TCP 的传输基于字节流,记录起始序列号、是否发送、是否接收。本文从实战出发,使用 Wireshark 抓包工具来分析具体的请求。
TCP 每次发送数据,都有一个确认应答 ACK,表示已经收到了数据包。确认号表示下一个传送的起始号。
发送一个 http 请求,使用 Wireshake 抓取数据包,打开 Statistics -> Flow Graph,在弹出的页面上将 Flow type 修改成 TCP Flows,就能看到 TCP 的数据包请求:
上图中标记了三个地方,中间的的标记的发送确认,就表示数据发送和确认应答,len 表示字节长度。发送 1 ~ 218 的字节,确认应答返回了确认号 219。第二个发送确认也是类似原理,所不同的是,这个发送确认时接收端的发送确认。
发送端的数据包,一般都发送到接收端。但是在网络不好,或者信号比较差的情况,可能就无法正常发送到数据。
先介绍两个概念,RTT 和 RTO。
RTT Round-Trip Time 表示往返时间,表示网络一段到另一端所需要的时间,也就是数据包的往返时间,以 TCP 握手为例:
RTT 表示数据包从发送到收到确认应答的时间。
RTO Retransmission Timeout 表示超时重传时间。超过这个时间没有确认应答,就会重传报文段,这个时间根据 RTT 来设置的。
重传机制是 TCP 基本的错误恢复功能,常见的重传机制有两种:
超时重传,字面意思是,超时规定的时间没有收到确认消息,就会再次发送一个消息请求。TCP 发送方发送报文时,会设置一个定时器,如果在时间范围内没有收到接收方发来的 ACK 确认报文,发送方就会重传已经发送的报文段。
TCP 有两种超时重传的情况:
上面的 RTO 表示超时重传时间,RTO 的设定不能过大的或者过小:
设置一个适当的 RTO 才会让重传机制更加高效。超时时间 RTT 应该略大于往返时间 RTT。
如果超时重传的报文段又超时了该怎么办呢?,答案就是重传的超时时间加倍,也就是再次超时重传的超时时间会增加到之前的两倍。
如果超时重传的报文段又丢包呢?此时发送方会以 RTO 时间的 2、4、8倍的倍数尝试多次重传。
超时重传如果消息多次没有收到确认报文,超时的周期也比较长,有没有更加高效的方法减少超时重传的时间呢?就引出下面的要讲的快速重传。
快速重传不会等待超时时间到了再重传,发送方收到 3 次重复确认报文端,就不会等超时时间重试,而是直接重传报文。
连续发送的报文段,中间只要有一个丢失,后续返回的确认号都是相同,后面的报文段无论有没有返回,都会重传一遍,这种设置还是比较合理的。在一段时间内,如果网络状况不好,导致丢包情况,后续的报文段一般也会丢包。
但是重传丢包后面所有的包,也会造成网络传输的浪费。对于上面的例子,如果只想传输 seq2,其他有返回的确认包就不用重传。
TCP 有一种重传机制: SACK Selective Acknowledgment 选择性重传。
这种方式需要 TCP 报文段选项加一个 SACK 字段,使用查看 Wireshake SYN 包中 SACK Permitted:
发送包有返回确认应答,就会发送给发送方告知对应的数据被接收了,发送方就能记录哪些数据被接收了,哪些数据没有被接收。后面只会重传没有被接收的数据包,这就是选择性重传。
TCP 发送比较大的数据包,TCP 会一次性发送大的数据包给接收方?答案是不会的,需要考虑网络带宽,TCP 会将大的数据包拆分成多个大小适中的数据包,发送一个 http 请求,添加较大的参数,使用 Wireshake 抓取数据包:
数据包被拆分成五个小的数据包。
数据包被拆分成多个小的数据包之后,数据包发送都有返回一个确认序列号,每次发送一个新的包,都等待上一个包的 ACK 回来之后才能发送,这样一来一回的效率是很低的:
TCP 为了解决这个问题,引入窗口的概念,在窗口范围内的数据包,无需等待上一次 ACK 确认,可以直接发送数据包:
滑动窗口是 TCP 协议中的一种流量控制机制,用来控制发送方和接收方数据传输的速率,避免数据过多造成数据无法及时处理。
窗口的大小也就是 TCP 报文段的 windos 字段,表示的就是接收方目前能接收的缓冲区的剩余大小,发送端根据这个字段处理发送的数据。
发送窗口根据三个标准来划分:是否发送、是否收到 ACK、是否在接收方处理范围内,分成了四个部分:
四个部分组成:
如果发送方一直没有收到 ACK,数据不断的发送,很快可用窗口也被耗尽,这时发送方也不会继续发送数据了,这时发送端可用窗口为零的情况我们成为“零窗口”。
随着 ACK 的确认,窗口也会依次向右滑动,比如发送端的窗口中,比如 40 ~ 43 字节都收到了 ACK 确认,那么整个可用的窗口就会顺次往右移动。此时 53 ~ 57的数据也都能发送了。
接收端的滑动窗口相对发送的窗口要简单的多,主要分为三个部分:
但数据接收后,窗口也向右边滑动,给发生端的数据提供数据缓存。如果读取缓存的数据速度有变化时,接收端可能也会改变接收窗口的大小,以此来控制发送端的发送速度。这就是滑动窗口进行流量控制的一种机制。
网络中由于有大量的包传输,在固定带宽下处理不过来数据包的传输,可能会导致数据包阻塞,网络传输的速度下降,甚至会下降到 0 的情况。这就有点类似排队买东西,如果正常排队,速度虽然不快但处理速度比较稳定。但是如果一下涌来很多人口,就会处理不过来,导致堵死情况。
而 TCP 被设置成一个无私的协议,当遇到网络拥塞时,TCP 会减少自己发送数据包,这样网络拥塞会得到很大的缓解。
为了实现拥塞控制,首先在发送端定义一个拥塞窗口 CWND (congestion window),限制发送端发送数据最多没有收到 ACK 确认包的大小,超过拥塞窗口范围后,就不会继续发送数据了。
拥塞窗口会随着网络情况的变化动态的调用自身的大小,大体的变化规则是:如果没有出现拥塞,就扩大窗口大小,否则就缩小窗口的大小。
拥塞控制算法主要包含四个部分:
当一个新的TCP连接开始时,无法确定是否用拥塞发生,一开始不会发送大量的包,而是从最小的发送窗口开始,后续会采用倍增的方式增加窗口的大小,窗口大小从 1 开始,后续慢慢增大到 2、4、8 等。
指数增加速度会越来越快,窗口扩大的一定的程度,就会减慢增加的速度,改成线性增加,这时候就进入拥塞避免阶段。
慢启动和拥塞避免的临界点叫做慢启动门限 ssthresh (slow start threshold。
ssthresh 大小一般是 65535 字节。拥塞避免的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。就变成线性增长了。
拥塞避免将原来的指数增长改成了线性增长,虽然增长速度减慢,但 CWND 窗口还是在增长阶段。随着窗口进一步缓慢增加,网络还是会遇到阻塞的状态,会出现丢包的情况。就需要对丢包进行重传。
重传机制有两种:
当发生超时重传时,sshresh 和 cwnd 的值会发生如下变化:
cwnd 重置为1,表示直接进入慢启动状态。
上面的超时重传速度变化太快,而快速重传是一个相对温和的方案。如果我们连续 3 次收到同样序号的 ACK,包还能回传,说明这个时候可能只是碰到了部分丢包,网络阻塞还没有很严重,无需重置 cwnd。
此时 ssthresh 和 cwnd 变化如下:
并进入到快速恢复阶段。
快速恢复主要是将 cwnd 恢复到正常大小,上面说的 cwnd 设置成原来的一半,ssthresh 设置成 cwnd 的大小。
快速恢复算法如下:
TCP 提供基于字节流、可靠的数据传输,为了确保数据的可靠性,做了很多工作:
TCP 重传、滑动窗口、流量控制、拥塞控制
滑动窗口:TCP是如何进行流量控制和拥塞控制的