网络协议复习笔记(八)传输层:TCP协议

TCP包头格式

网络协议复习笔记(八)传输层:TCP协议
两个端口号决定了数据发向哪个应用。包的序号是为了解决乱序的问题。确认序号是为了确认对方是否收到,如果没有收到就应该重新发送,直到送达。
状态位中,SYN 是发起一个连接,ACK 是回复,RST是重新连接,FIN是结束连接等。TCP 是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态变更。
TCP 要做流量控制,通信双方各声明一个窗口,标识自己当前能够的处理能力;还会做拥塞控制,对于真正的通路堵车不堵车,它无能为力,唯一能做的就是控制自己,也即控制发送的速度。

三次握手

TCP 的连接建立,我们常常称为三次握手,也常称为“请求 -> 应答 -> 应答之应答”的三个回合。
A:您好,我是 A。
B:您好 A,我是 B。
A:您好 B。
为什么要三次,而不是两次?按说两个人打招呼,一来一回就可以了啊?为了可靠,为什么不是四次?

假设这个通路是非常不可靠的,A 要发起一个连接,当发了第一个请求杳无音信的时候,会有很多的可能性,比如第一个请求包丢了,再如没有丢,但是绕了弯路,超时了,还有 B 没有响应,不想和我连接。

A 不能确认结果,于是再发,再发。终于,有一个请求包到了 B,但是请求包到了 B 的这个事情,目前 A 还是不知道的,A 还有可能再发。

B 收到了请求包,就知道了 A 的存在,并且知道 A 要和它建立连接。如果 B 不乐意建立连接,则 A 会重试一阵后放弃,连接建立失败,没有问题;如果 B 是乐意建立连接的,则会发送应答包给 A。

对于 B 来说,这个应答包也是一入网络深似海,不知道能不能到达 A。这个时候 B 自然不能认为连接是建立好了,因为应答包仍然会丢,会绕弯路,或者 A 已经挂了都有可能。因而两次握手肯定不行

B 发送的应答可能会发送多次,但是只要一次到达 A,A 就认为连接已经建立了,因为对于 A 来讲,他的消息有去有回。A 会给 B 发送应答之应答,而 B 也在等这个消息,才能确认连接的建立,只有等到了这个消息,对于 B 来讲,才算它的消息有去有回。按理来说,还应该有个应答之应答之应答,这样下去就没底了,所以四次握手是可以的

三次握手除了双方建立连接外,主要还是为了沟通一件事情,就是 TCP 包的序号的问题。A 要告诉 B,我这面发起的包的序号起始是从哪个号开始的,B 同样也要告诉 A,B 发起的包的序号起始是从哪个号开始的。为什么序号不能都从 1 开始呢?因为这样往往会出现冲突。

每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个 32 位的计数器,每 4ms 加一,如果计算一下,如果到重复,需要 4 个多小时,绕路的包就挂了,因为我们都知道 IP 包头里面有个TTL,也即生存时间。双方的建立连接的状态变化时序图如下,其中左边为客户端,右边为服务端。
网络协议复习笔记(八)传输层:TCP协议
一开始,客户端和服务端都处于 CLOSED状态。先是服务端主动监听某个端口,处于LISTEN状态。然后客户端主动发起连接SYN,之后处于SYN-SENT状态。服务端收到发起的连接,返回 SYN,并且ACK 客户端的 SYN,之后处于SYN-RCVD状态。客户端收到服务端发送的 SYNACK 之后,发送 ACKACK,之后处于 ESTABLISHED 状态,因为它一发一收成功了。服务端收到 ACKACK 之后,处于ESTABLISHED 状态,因为它也一发一收了。

四次挥手

A:B 啊,我不想玩了。
B:哦,你不想玩了啊,我知道了。

此时 B不能在ACK的时候直接关闭,B 还没做完自己的事情,还是可以发送数据的,所以称为半关闭的状态。

B:A 啊,好吧,我也不玩了,拜拜。
A:好的,拜拜。

这是和平分手的状况。其他异常状况,比如A 说“不玩了”,没有收到回复,则 A 会重新发送“不玩了”。但是这个回合结束之后,就有可能出现异常情况了,因为已经有一方率先撕破脸。

一种情况是,A 说完“不玩了”之后,直接跑路,是会有问题的,因为 B 还没有发起结束,而如果 A 跑路,B 就算发起结束,也得不到回答,B 就不知道该怎么办了。另一种情况是,A 说完“不玩了”,B 直接跑路,也是有问题的,因为 A 不知道 B 是还有事情要处理,还是过一会儿会发送结束。

所以设计了断开连接时的状态时序图如下
网络协议复习笔记(八)传输层:TCP协议
当 A 说“不玩了”,就进入 FIN_WAIT_1的状态,B 收到“A 不玩”的消息后,发送知道了,就进入CLOSE_WAIT的状态。

A 收到“B 说知道了”,就进入FIN_WAIT_2的状态,如果这个时候 B 直接跑路,则 A 将永远在这个状态。TCP 协议里面并没有对这个状态的处理,但是 Linux可以调整tcp_fin_timeout这个参数,设置一个超时时间。

如果 B 没有跑路,发送了“B 也不玩了”的请求到达 A 时,A 发送“知道 B 也不玩了”的 ACK 后,从 FIN_WAIT_2状态结束,按说 A 可以跑路了,但是最后的这个 ACK万一 B 收不到呢?则 B 会重新发一个“B 不玩了”,这个时候 A 已经跑路了的话,B 就再也收不到ACK 了,因而 TCP 协议要求 A 最后等待一段时间 TIME_WAIT,这个时间要足够长,长到如果 B 没收到ACK的话,“B 说不玩了”会重发的,A 会重新发一个ACK 并且足够时间到达 B。

A 直接跑路还有一个问题是,A 的端口就直接空出来了,但是 B 不知道,B 原来发过的很多包很可能还在路上,如果 A 的端口被一个新的应用占用了,这个新的应用会收到上个连接中 B 发过来的包,虽然***是重新生成的,但是这里要上一个双保险,防止产生混乱,因而也需要等足够长的时间,等到原来 B 发送的所有的包都挂掉,再空出端口来。

MSLMaximum Segment Lifetime,即报文最大生存时间。是任何报文在网络上存在的最长时间。等待的时间设为2MSL,超过这个时间报文将被丢弃,协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。

TCP状态机

网络协议复习笔记(八)传输层:TCP协议
加粗的实线是客户端 A 的状态变迁,加粗的虚线是服务端 B 的状态变迁。

实现细节

为了保证顺序性,每一个包都有一个 ID。在建立连接的时候,会商定起始的 ID 是什么,然后按照 ID 一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的 ID,表示都收到了,这种模式称为累计确认或者累计应答

TCP 也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存里是按照包的 ID 一个个排列,根据处理的情况分成四个部分:

  1. 发送了并且已经确认的
  2. 发送了并且尚未确认的
  3. 没有发送,但是已经等待发送的
  4. 没有发送,并且暂时还不会发送的

在 TCP 里,接收端会给发送端报一个窗口的大小,叫 Advertised window。这个窗口的大小应该等于上面的第二部分加上第三部分,就是已经交代了没做完的加上马上要交代的。超过这个窗口的,接收端做不过来,就不能发送了。

发送端需要保持如下的数据结构
网络协议复习笔记(八)传输层:TCP协议

  • LastByteAcked:第一部分和第二部分的分界线
  • LastByteSent:第二部分和第三部分的分界线
  • LastByteAcked + AdvertisedWindow:第三部分和第四部分的分界线

接收端要保持如下结构:
网络协议复习笔记(八)传输层:TCP协议

  • NextByteExpected: 是第一部分和第二部分的分界线
  • MaxRcvBuffer:最大缓存的量
  • LastByteRead 之后是已经接收了,但是还没被应用层读取的

从上面两张图可以看到,在发送端来看,1、2、3 已经发送并确认;4、5、6、7、8、9 都是发送了还没确认;10、11、12 是还没发出的;13、14、15 是接收方没有空间,不准备发的。
在接收端来看,1、2、3、4、5 是已经完成ACK,但是没读取的;6、7 是等待接收的;8、9 是已经接收,但是没有 ACK 的。

当前状态如下:

  • 1、2、3 没有问题,双方达成了一致
  • 4、5 接收方说ACK 了,但是发送方还没收到,有可能丢了,有可能在路上
  • 6、7、8、9 肯定都发了,但是 8、9 已经到了,但是 6、7 没到,出现了乱序,缓存着但是没办法 ACK

确认与重发机制

假设 4 的确认到了,5 的 ACK丢了,6、7 的数据包丢了。

一种方法就是超时重试,也即对每一个发送了,但是没有 ACK 的包,都有设一个定时器,超过了一定的时间,就重新尝试。
估计往返时间,需要 TCP 通过采样往返时间 RTT 的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断地变化。除了采样 RTT,还要采样 RTT波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法Adaptive Retransmission Algorithm)。

如果过一段时间,5、6、7 都超时了,就会重新发送。接收方发现 5 原来接收过,于是丢弃 5;6 收到了,发送 ACK,要求下一个是 7,7 不幸又丢了。当 7 再次超时的时候,有需要重传的时候,TCP 的策略是超时间隔加倍每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送

流量控制

在对于包的确认中,同时会携带一个窗口的大小。

先假设窗口不变的情况,窗口始终为 9。4 的确认来的时候,会右移一个,这个时候第 13 个包也可以发送了。
网络协议复习笔记(八)传输层:TCP协议
假设发送端发送过猛,会将第三部分的 10、11、12、13 全部发送完毕,之后就停止发送了,未发送可发送部分为 0。
网络协议复习笔记(八)传输层:TCP协议
对于包 5 的确认到达的时候,在客户端相当于窗口再滑动了一格,这个时候,才可以有更多的包可以发送了,例如第 14 个包才可以发送。
网络协议复习笔记(八)传输层:TCP协议
如果接收方实在处理的太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为 0,则发送方将暂时停止发送。假设一个极端情况,接收端的应用一直不读取缓存中的数据,当数据包 6 确认后,窗口大小就不能再是 9 了,就要缩小一个变为 8。
网络协议复习笔记(八)传输层:TCP协议
新的窗口 8 通过 6 的确认消息到达发送端的时候,会发现窗口没有平行右移,而是仅仅左面的边右移了,窗口的大小从 9 改成了 8。
网络协议复习笔记(八)传输层:TCP协议
如果接收端还是一直不处理数据,则随着确认的包越来越多,窗口越来越小,直到为 0。
网络协议复习笔记(八)传输层:TCP协议
发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。当接收方比较慢的时候,要防止低能窗口综合征,可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口。

拥塞控制

TCP 发送包常被比喻为往一个水管里面灌水,而 TCP 的拥塞控制就是在不堵塞,不丢包的情况下,尽量发挥带宽。
水管有粗细,网络有带宽,也即每秒钟能够发送多少数据;水管有长度,端到端有时延。在理想状态下,水管里面水的量 = 水管粗细 x 水管长度。对于到网络上,通道的容量 = 带宽 × 往返延迟
如果设置发送窗口,使得发送但未确认的包为为通道的容量,就能够撑满整个管道。
网络协议复习笔记(八)传输层:TCP协议
如图所示,假设往返时间为 8s,去 4s,回 4s,每秒发送一个包,每个包 1024byte。已经过去了 8s,则 8 个包都发出去了,其中前 4 个包已经到达接收端,但是 ACK 还没有返回,不能算发送成功。5-8 后四个包还在路上,还没被接收。这个时候,整个管道正好撑满,在发送端,已发送未确认的为 8 个包,正好等于带宽,也即每秒发送 1 个包,乘以来回时间 8s。

原来发送一个包,从一端到达另一端,假设一共经过四个设备,每个设备处理一个包时间耗费 1s,所以到达另一端需要耗费 4s,如果发送的更加快速,则单位时间内,会有更多的包到达这些中间设备,这些设备还是只能每秒处理一个包的话,多出来的包就会被丢弃。

可以想其他的办法,例如这个四个设备本来每秒处理一个包,但是我们在这些设备上加缓存,处理不过来的在队列里面排着,这样包就不会丢失,但是缺点是会增加时延,这个缓存的包,4s 肯定到达不了接收端了,如果时延达到一定程度,就会超时重传。

TCP 的拥塞控制主要来避免两种现象,包丢失超时重传。一旦出现了这些现象就说明,发送速度太快了,要慢一点。

如果通过漏斗往瓶子里灌水,就知道不能一桶水一下子倒进去,要一开始慢慢的倒,然后发现总能够倒进去,就可以越倒越快。这叫作慢启动

拥塞的一种表现形式是丢包,需要超时重传,这个时候,将 sshresh设为 cwnd/2,将 cwnd 设为 1,重新开始慢启动。但是这种方式太激进了,将一个高速的传输速度一下子停了下来,会造成网络卡顿。

快速重传算法可以使当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,cwnd 减半为 cwnd/2,然后 sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh + 3,也就是没有一夜回到*,而是还在比较高的值,呈线性增长。
网络协议复习笔记(八)传输层:TCP协议
蓝线为上述的重新慢启动,橙线为快速重传算法

然而仍然存在两个问题:

  1. 丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。
  2. 拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实 TCP 只要填满管道就可以了,不应该甚至连缓存也填满。

TCP BBR 拥塞算法找到了上述两个问题的平衡点,通过不断地加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡。
网络协议复习笔记(八)传输层:TCP协议

小结

  • 连接的建立是经过三次握手,断开的时候四次挥手
  • TCP 包头很复杂,但是主要关注五个问题,顺序问题丢包问题连接维护流量控制拥塞控制
  • 顺序问题丢包问题流量控制都是通过滑动窗口来解决的
  • 拥塞控制是通过拥塞窗口来解决的,相当于往管道里面倒水,快了容易溢出,慢了浪费带宽,要摸着石头过河,找到最优值