TCP 相关知识
TCP/IP 协议占据了互联网通信的一大半江山,特别像 TCP 这种保障端到端的可靠传输更是相当重要,关于它的实现也很复杂,今天介绍下关于 TCP 的相关重要知识。
我们先来看下 TCP 的头格式:
我们看到有一个源端口
、目的端口
,这 2 个元素再加上 IP 层的源地址
和目的地址
,就可以用来表示 TCP 的某个连接了,相当于数据库里的唯一标识。所以当我们发起一个 TCP 连接时,会发现如果存在相同的端口在运行时,操作系统是不允许的,只有这样才能保证唯一连接的存在,避免数据接收混乱。
在上图中,我们会看到以下几个元素,它们是 TCP 协议对几个重要问题的保障:
序列号(seq)
:数据包的序号,通过序号来确认包的连续性,解决包的乱序问题。响应号(ack)
:针对上面字段的确认序号,比如服务器接收到客户端的请求,里面包含了 seq 序号,此时服务器响应回去时,会进行 ack = seq + 1 的字段设置,表示已接收到的累积数据。Window
:滑动窗口使用,用来反馈接收方接下来能处理的包大小,防止双方对数据包的处理能力不对等,主要解决了流控问题。TCP Flag
:TCP 包的类型,用来辅助 TCP 的阶段处理,比如 SYN 表示建立连接,FIN 表示关闭连接。
TCP 状态流转
协议之所以会存在,就在于双方需要互相配合协作,以确定哪个阶段该做哪些事情。在 TCP 协议总体划分为建立连接
、数据传输
、连接断开
这三个过程。这些阶段将会涉及对应状态的流转:
(图片地址)
TCP 的三次握手、四次挥手
而在上面的状态流转中,最重要,也是经常会提起的关于连接建立的三次握手
以及连接断开的四次挥手
。
从上面的流程图中,我们会发现对于 Client 端来讲,经历了一个请求-确认过程;对于 Server 端来讲,也经历了一个请求-确认过程。这就相当于双方都已经互相确认过眼神了。所以,三次握手对于连接的建立是刚刚好的。
如果我们只进行 2 次握手就建立连接,那么对于 Server 端来讲太容易建立起连接了,基本是有客户端过来,那么 Server 就要建立起连接了。这种情况就会导致连接成本太低,Server 端很容超负载。
至于在关闭连接时需要四次挥手,主要是因为 TCP 是全双工
的,存在了两个方向的数据发送与接收,所以需要在两个方向都进行关闭流程。否则一方关闭了,另一方还在傻傻等待,只能等待异常超时结束了。
TIME_WAIT
在四次挥手中,有一个状态是 TIME_WAIT
,它是主动关闭者在最后会进行的动作,是一个定时设置,在 2*MSL(MSL 表示一个包在网络环境中的生存时间,一般为 2 分钟, Linux 里为 30s)时间过后就会真正的 CLOSED。
之所以不立即关闭,主要为了让被动关闭方能有足够的时间接收到最后的 Ack 包,如果没有接收到,被动方就会重新发送 Fin 包,重新触发主动方发送最后的 Ack 包。这样的话,就能尽量保证被动关闭方尽快关闭连接了,毕竟主动关闭方需要承担起主要责任,所以会有 TIME_WAIT 的等待了。
另外一个原因也是怕当前连接立马释放,有一定概率会立马被使用到,就有可能产生包的混乱问题了。
TCP 重传
TCP 发送的包都需要接收方进行一个 Ack
包的响应,如果在一定时间内没有响应的话,那么发送方就会认为包未能正确到达,需要进行重传动作。这就是 TCP 的重传机制。
TCP 里的重传机制会有一个超时的判断,这个超时时间并不是很准确,或者说并不是很标准,毕竟不同的网络环境,包的到达情况都会是不一样的。
所以 TCP 会使用一个采样时间,先记录了正常情况下一个数据包从发送到响应确认这么一来一回的时间,即所谓的 RTT
(Round Trip Time) 时间,根据这个时间进行一些公式计算,得到了超时时间的值:RTO
(Retransmission TimeOut)
对于重传机制,还有另外一种触发机制。上面的情况属于发送方去探知发送情况,也有另一种情况是接收方能探知的。比如发送方发送了 1, 2, 3 的包,但实际上接收方只接收到 1 和 3,一直没能收到 2 这个包,那此时接收方就会连续响应三个 关于 2 的 ack 包。
当发送方收到这么一个连续的 3 个 ack 包后,就知道需要重传 2 了,此时就不需要等到 2 的超时未确认触发,可以提前的重传 2 这个包了。
TCP 滑动窗口
重传机制使得数据包能够可靠的传输,然而如果接收方数据处理能力有限,而发送方未能感应到这种情况,不停的发送数据包,则会增加接收方的压力,还会导致网络拥塞的发生。
为此,TCP 采用滑动窗口
进行了流量的控制,所谓的滑动窗口即在发送方和接收方各自维护了一个窗口,在这个窗口里将会维护对应的数据包,以感知当前的数据处理情况。
在接收方这边的窗口称之为接收窗口
,它具体表示当前所能接收的数据包大小,计算公式为:当前最大可接收缓冲区大小 - 当前已接收的大小,在连接建好的开始一般为 65535 字节。
在计算出可接收大小后,接收方就会将此值设置在 TCP 头部里的 Window 字段,然后响应回发送方,发送方也就知道了当前所能允许发送的数据包大小了。
在发送方这边的窗口称之为发送窗口
,按正常逻辑来讲,发送窗口维护的是即将要发送的数据,即根据刚刚反馈回来的接收窗口大小计算出的发送数据。
但由于一个数据包的发送需要有一个 ACK 响应才算完整流程,所以对于这些“已发送未响应”的数据也应该纳入到发送窗口的管理,并且只有真的 ACK 响应回来,才能继续下个数据包的准备发送。
需要注意的是,如果发送方接收到的 Window 大小为 0,则表示当前的接收方已经无能力处理新的包了,此时发送方就不会再下发数据了,直到接收方发送一个窗口通告
,才继续数据的发送。
但此时需要考虑一种情况,就是接收方由于网络问题没能将窗口通告送达发送方,那此时发送方就会一直干等着了.所以对于发送方来讲,会启动窗口探知动作,让接收方 ACK 它当前的接收窗口大小,如果超过 3 次的探知动作,则直接断开连接了。
TCP 的拥塞控制
在一个复杂的网络环境中,数据包的传输不仅仅只涉及到端到端的,还可能会出现很多网络问题,导致包的堆积,影响了整体的传输速度。所以,TCP 协议需要将网络的阻塞情况考虑进来,避免加剧。这就是 TCP 的拥塞控制。
为此,TCP 协议抽象出了拥塞窗口
(cwnd)的概念,它会根据当前的网络拥塞程度进行动态的调整。由于加入了拥塞情况的考虑,前面我们提到过的发送窗口则不能仅仅只考虑接收窗口这个因素了,需要进行 min(拥塞窗口,接收窗口)
的选择了。
由此可见,拥塞窗口的计算很重要,它将决定了数据包的发送大小。而关于拥塞窗口的计算,它将在几个场景里会涉及到,下面我们一一来分析。
MTU 和 MSS
在分析拥塞窗口的具体场景之前,我们先来看看拥塞窗口的基本单位:MSS。MSS 表示 网络传输数据的最大值,如果 MSS 加上包头大小,则表示网络传输最大报文:MTU 了。
在 Internet 这种互联网中,一般 MTU 定义为 576 字节,减去 TCP、IP 的包头 40 字节,则可以得到 MSS = 536 字节的值;而在以太网这种局域网里,一般 MTU 会大点:1500 字节,MSS 为 1460 字节。
慢启动
当连接建立完毕,开始传输数据时,TCP 协议规定不能一开始就发送大尺寸的数据包,这样避免了网络环境有问题时,新加入的连接加剧了拥塞状况。所以,对于新加入的连接而言,需要一点一点的增大数据量,这就是所谓的慢启动
。
其中,慢启动涉及的拥塞窗口计算过程如下:
- 刚开始建立好连接时,拥塞窗口 = 1
- 每当接收到一个 ACK 包时,拥塞窗口 = 拥塞窗口 + 1,此时呈线性增加。
- 每当经过一个 RTT,拥塞窗口 = 拥塞窗口 * 2,此时呈指数上升趋势。
拥塞避免
从慢启动的算法来看,每经过一个 RTT 后,拥塞窗口 的增长速度将会变得很厉害,如果没有进行限制的话,那么很快就会占满带宽了。因此, TCP 协议使用了一个叫慢启动门限(ssthresh)的变量(一般取 65535 字节)。当 cwnd 超过该限制后,就会进入所谓的拥塞避免
阶段了。
在拥塞避免阶段,拥塞窗口的计算过程如下:
- 每接收到一个 ACK 包时,拥塞窗口 = 拥塞窗口 + 1/拥塞窗口
- 每当经过一个 RTT,拥塞窗口 = 拥塞窗口 + 1
从上面的算法可以看出,进入拥塞避免阶段后,数据包的发送大小将呈线性增加了。通过这样的方式,使得 TCP 的传输在前期很快,然后再慢慢降下来,达到网络最佳值。
拥塞发生
前面都是在避免拥塞的发生去动态调整拥塞窗口(cwnd)的,然而按照线性增长的趋势,始终会导致网络拥塞的。当发送拥塞(一般只要丢包,需要重传数据包就认为发送了拥塞)时,TCP 协议该如何处理呢?此处也算是 TCP 协议比较复杂的地方,因为它在不断的改进,也衍生出了很多版本,下面我们来看看这些不同版本的区别和处理吧。
Tahoe 版本
Tahoe 版本是 TCP 的最早版本,当它发现需要进行重传动作,即触发了 RTO 超时或发送方收到三个重复 ACK 包时,此时会进行的动作为:
- sshthresh = cwnd /2
- cwnd 重置为 1
- 重回慢启动阶段
Reno 版本
Reno 版本进行的动作为:
- sshthresh = cwnd /2
- cwnd = sshthresh + 3 * MSS (将 3 个重复 ACK 考虑进去)
- 进入快速恢复阶段
其中,快速恢复阶段的步骤如下:
- 当重传的包发出去后,收到了重传包的 ACK 后,cwnd = cwnd + 1
- 当收到新的数据包的 ACK 后,此时快速恢复过程已结束,则 cwnd = sshthresh,然后重回拥塞避免阶段
NewReno 版本
NewReno 是对 Reno 的改进,主要是优化了快速恢复阶段,在 Reno 版本中,所考虑的都是一个包的丢失情况。然而,在实际情况中,一次数据窗口的发送,是有可能出现很多数据包丢失情况的。
这样的话,就会触发多次的 cwnd 和 ssthresh 减半动作,一旦 cwnd 降到小于 3 时,即发送窗口会出现小于 3 的情形,此时将再也触发不了 3 次快速重传动作了,只能依赖 RTO 超时,而一般 RTO 的值是比较大(太小会经常触发重传)的,此时整个传输速度将会大大降低。
所以 NewReno 会在收到所有数据包的确认后才结束快速恢复阶段,这样 cwnd 和 sshthresh 就不会轻易被降低了。
NewReno 主要是使用了一个 recover 变量,作为当前数据窗口中,可能丢包的最大序号。即如果有丢包情况产生,并且大于当前的 recover 值,则会更新该值。
当收到接收方的 ack 后,会进行 ack_seq 的判断,如果 ack_seq > recover,此时就可以结束快速恢复阶段了;如果 ack_seq < recover,则意味着多包丢失,还不能结束快速恢复阶段。通过这样的控制,来提高了整个的吞吐量。
Nagle 算法
下面我们来看看 TCP 里的其他经典算法:Nagle。Nagle 算法是一种通过减少小数据包的发送来提高 TCP 效率的机制。它会把多个小数据包合并到一个片段,并且等待满足一定条件后,再一起发送过去。具体的触发条件是:
- 如果包长度达到 MSS,则允许发送
- 如果包含 FIN,则允许发送
- 如果设置了 TCP_NODELAY,则允许发送
- 未设置 TCP_CORK 选项时,若所有发出去的小数据包(包长度小于 MSS)均被确认,则允许发送
当上述条件都未满足,但发生了超时(一般为 200ms),则立即发送。
对于 TCP 协议来讲,默认会启用 Nagle 算法,降低网络负载,减少网络拥塞,提高网络吞吐。
Delay Ack
Delay Ack 指延迟发送 Ack。在 TCP 的确认机制里,可以在通信过程中不对每一个 TCP 数据包进行单独的 ACK 包响应,而是在传输数据时,顺便把 ACK 信息随数据包一起发送,这样可以提高利用率。
如果在一定时间内(一般 40 ms)没有数据包要发送,那么就会单独的进行 ACK 包响应。这个过程就被称为 Delay Ack 了。
粘包与拆包
TCP 是面向字节流的传输,它会根据接收方的包处理能力以及当前网络的拥塞情况来一部分一部分的加载数据发送,再加上有 Nagle 这种整合小数据包的算法存在。所以对于接收方来讲,接收到的数据有可能是粘合在一起的,也有可能是被拆分开的,即所谓的粘包和拆包。
对于粘包和拆包现象,常用的解决方案有:
- 在包的首部添加当前要传输的数据包的长度,让接收方根据长度去切割。
- 将数据包封装为固定长度,不够的补 0,让接收方按固定长度解析。
- 人为的给数据包添加边界,比如在数据包结尾添加特殊字符,当解析到特殊字符时,接收方就认为读取到了一个完整有意义的数据段了。