史上最全 TCP 协议,看完你就懂了

2,000 阅读18分钟

思考

  1. 描述一下 TCP 的三次握手,第一次握手的报文段里除了 SYN 标志位,还包含了什么?
  2. 描述一下 TCP 的四次挥手,为什么不能是三次挥手?第四次挥手客户端发出最后的确认报文段之后,就会马上断开连接吗?
  3. TCP 是一个包发送出去并得到确认后,才可以发送下一个包,还是无需等待确认就可以发送下一个包?
  4. TCP 和 UDP 的区别是什么?
  5. TCP 是怎么保证可靠数据传输的?
  6. TCP 是如何进行流量控制的?
  7. TCP 是如何进行拥塞控制的?
  8. TCP 的慢启动是什么?快启动又是什么?

简介

TCP 协议一种面向连接的、可靠的、基于字节流的传输层通信协议,位于 OSI 模型中的第四层传输层UDP 协议是另一个重要的协议,提供不可靠传输。 不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换,所以由传输层 TCP 协议提供可靠连接,QUIC 协议通过基于 UDP 协议实现可靠连接。

packetModel.jpeg

TCP协议的运行可划分为三个阶段:连接创建(connection establishment)、数据传送(data transfer)和连接终止(connection termination)。

TCP 与 UDP 的区别

tcpUdp.png

TCP 数据包结构

packet.png

  • 【端口】:负责区分网络流

  • 【序列号码】:在连接建立时由计算机计算出的初始值,通过 SYN 包传给对端主机,每发送一次新的数据包,就累加一次该序列号的大小。用来解决网络包乱序问题;

  • 【确认号码】:指下次期望收到的数据的序列号,发送端收到这个确认应答以后可以确认确认应答号-1的数据包已经被正常接收。主要用来解决不丢包的问题;

  • 【标志字段】

    • 【URG】:为1表示高优先级数据包,紧急指针字段有效。
    • 【ACK】:为1表示确认号字段有效。
    • 【PSH】:为1表示是带有PUSH标志的数据,指示接收方应该尽快将这个报文段交给应用层而不用等待缓冲区装满。
    • 【RST】:为1表示出现严重差错。可能需要重新创建TCP连接。还可以用于拒绝非法的报文段和拒绝连接请求。
    • 【SYN】:为1表示这是连接请求或是连接接受请求,用于创建连接和使顺序号同步
    • 【FIN】:为1表示发送方没有数据要传输了,要求释放连接。
  • 【窗口大小】:表示从确认号开始,本报文的发送方可以接收的字节数,即接收窗口大小。用于流量控制。

  • 【校验和】:对整个的TCP报文段,包括TCP头部和TCP数据,以16位字进行计算所得。这是一个强制性的字段。

三次握手(连接创建)

一对终端同时初始化一个它们之间的连接是可能的。但通常是由一端(服务器端)打开一个套接字(socket)然后监听来自另一方(客户端)的连接,这就是通常所指的被动打开(passive open)。服务器端被被动打开以后,客户端就能开始创建主动打开(active open)。

服务器端执行了 listen 函数后,就在服务器上创建起两个队列:

  • SYN 队列:存放完成了二次握手的结果。
  • ACCEPT 队列:存放完成了三次握手的结果。

established.png

三次握手过程

  • (1)客户端(通过执行 connect 函数)向服务器端发送一个 SYN 包,请求一个主动打开。该包携带客户端为这个连接请求而设定的随机数 A 作为消息序列号。 以下是 tcpdump 抓包截图,其中 [S] 表示数据包中的 SYN 标志位置为 1,seq 表示随机数序号,win 表示窗口大小

tcpdump1.png

  • (2)服务器端收到一个合法的 SYN 包后,回送一个 SYN/ACK。ACK 的确认码应为 A+1,SYN/ACK 包本身携带一个随机产生的序号 B。 以下是 tcpdump 抓包截图,其中 [S.] 表示数据包中的 SYN 和 ACK 标志位置为 1,seq 表示随机数序号,win 表示窗口大小

tcpdump2.png

  • (3)客户端收到 SYN/ACK 包后,发送一个 ACK 包,ACK 的确认码则为 B+1。此时客户端进入 ESTABLISHED 状态。 服务端接收到 ACK 包后也进入 ESTABLISHED 状态。 以下是 tcpdump 抓包截图,其中 [.] 表示数据包中的 ACK 标志位置为 1,win 表示窗口大小 tcpdump3.png

服务器建立连接超时时间 服务器端如果在一定时间内没有收到的客户端的 ACK 包会重发 SYN-ACK。 在Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻倍,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s才知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,TCP才会断开这个连接。

为什么要发送随机数序号 选择一个随机数作为序号的初值,防止 TCP 序号预测攻击。

TCP 序号预测攻击 在TCP序列预测攻击中,黑客试图预测要发送的下一个数据包的序列。 如果可以成功猜测序列号,那么该方可以插入具有相同序列号的数据包,该数据包来自相同的IP地址,并成功地将欺诈性的数据包提交给接收方。

为什么要三次连接,二次连接不行吗?

  • (1)避免历史连接 为了防止已失效的连接(connect)请求报文段传送到了服务端,因而产生错误。 网络环境是比较复杂的,不一定能保证我们先发送的数据包就一定能再我们期望的时间内送达,它可能半路 滞留了。也有可能超时后再抵达服务端,那么这时的TCP就会产生以下的一种情况:

rst.png

如果一个报文超时后没有得到响应,客户端可能再次发送新的 SYN 请求,这时旧的 SYN 请求会比新的 SYN 请求先达到服务器。如果此时没有第三次连接来确认此连接是否是历史连接,那么双方可能会建立两个连接,造成数据混乱。如果是三次连接的话,客户端就有机会再去确认或者中止掉错误的连接,**防止历史连接初始化了连接**
  • (2)同步双方初始序列号 TCP协议通信的可靠性,是靠着“序列号”所维持的,每次通信后,都会回一个ACK报文,ACK中的确认应答号都是靠着之前报文的序列号 + 1实现的,这样做有几个好处:

    • 接收方可以去除重复的数据报;
    • 接收方可以根据数据包的序列号按序接收;
    • 可以标识发送出去的数据包中, 哪些是已经被对方收到的;

    当客户端发送携带初始序列号的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送初始序列号给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步

    四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了“三次握手”。

    两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。

  • (3)避免资源浪费 假如我们现在是两次握手,如果有网络阻塞等原因造成旧的连接SYN请求还没抵达服务端,就已经达到了超时时间,那么客户端就会再次发送请求SYN报文,之后如果两次请求都能达到服务端的话,由于服务端现在只有两次握手,无法确定当前的SYN就是客户端想要的连接,只能一收到 SYN 就返回一个 ACK 报文再建立起一个连接,这么做就会造成资源的浪费。看下图所示:

timeout.png

  • 小结,不能使用两次握手和四次握手的原因: 两次握手:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号; 四次握手:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

如何保证数据传输可靠性

  • 使用序号,对收到的TCP报文段进行排序以及检测重复的数据
  • 使用校验和检测报文段的错误,即无错传输
  • 使用确认和计时器来检测和纠正丢包或延时
  • 流量控制(Flow control)
  • 拥塞控制(Congestion control)
  • 丢失包的重传

数据传输

translate.png 过程

  1. 客户端发送报文,把 PSH、ACK 标志位置为 1,seq 2067389750:2067391496,length长度:1746
  2. 服务端收到数据,发送 ACK 应答报文,ack 为 2067391496
  3. 客户端发送报文,把 PSH、ACK 标志位置为 1,seq 2352054211:2352055957,length长度:1746
  4. 服务端收到数据,发送 ACK 应答报文,ack 为 2352055957

校验和

将TCP报文段的头部和数据部分的和计算出来,再对其求反码(一的补码),就得到了校验和,然后将结果装入报文中传输。TCP校验和也包括了96位的伪头部,其中有源地址、目的地址、协议以及TCP的长度。这可以避免报文被错误地路由。

流量控制(Flow control)

流量控制的目的是用来避免主机分组发送得过快而使接收方来不及完全收下,一般由接收方通告给发送方进行调控,使用滑动窗口协议实现流量控制。 发送方在发送报文时,会指出发送方可接收的字节数量(win=655356),发送方在没有新的确认报文的情况下最多发送 win 字节数量的报文

tcpdump1.png 接收方在发送报文时,会指出接收方可接收的字节数量(win=655356),接收方在没有新的确认报文的情况下最多发送 win 字节数量的报文

tcpdump2.png

最大分段大小 (MSS)是在单个分段中TCP愿意接受的数据的字节数最大值。流量控制过程如下:

reliableTranslate.png 当服务端 win=0 时,客户端停止发送数据,并开启定时器,等待定时器到期,尝试发送一个小的ZWP(Zero Window Probe)包,等待服务端的 ACK 带有 win 的确认报文,一般 ZWP 包会发送 3 次,如果3次过后 win 还是 0 的话,有的 TCP 实现就会发 RST 把链接断了

愚蠢窗口综合症:服务端以很小的增量来处理到来的数据,会发布一系列小的 ACK 确认报文,在 TCP 的数据包中发送很少数据,相对于 TCP 包头是很大的开销。解决这个问题,就要在window size足够大的时候响应: - 服务端使用 David D Clark 算法: 如果收到的数据导致 window size 小于某个值,服务端发送带有 win=0 的 ACK 确认报文, 阻止了客户端再发数据。等到服务端处理了一些数据后, window size 大于等于 MSS 或服务端的 buffer 有一半为空, 可以把window打开让发送端再发数据过来。 - 客户端使用Nagle算法来延时处理: 条件一:Window Size>=MSS 且 Data Size >=MSS; 条件二:等待时间或是超时200ms; 这两个条件有一个满足,才会发数据,否则就是在积累数据。 Nagle算法默认是打开的,所以对于一些需要小包场景的程序——比如像telnet或ssh这样的交互性程序,需要关闭这个算法。

拥塞控制(Congestion control)

拥塞控制是发送方根据网络的承载情况控制分组的发送量,以获取高性能又能避免拥塞崩溃。 四种相互影响的拥塞控制算法:慢开始、拥塞避免、快速重传、快速恢复。 慢启动:慢启动初始启动时设置拥塞窗口值(cwnd)为1、2、4或10个MSS,慢启动是指每次TCP接收窗口收到确认时都会增长,增加的大小就是已确认段的数目,这种情况一直保持到要么没有收到一些段,要么窗口大小到达预先定义的阈值。 快速重传:TCP发送方每发送一个分段都会启动一个超时计时器,如果没能在特定时间内接收到相应分段的确认,发送方就假设这个分段在网络上丢失了,需要重发。快速重传的机制:如果假设重复阈值为3,当发送方收到4次相同确认号的分段确认(第1次收到确认期望序列号,加3次重复的期望序列号确认)时,则可以认为继续发送更高序列号的分段将会被接受方丢弃,而且会无法有序送达。发送方应该忽略超时计时器的等待重发,立即重发重复分段确认中确认号对应序列号的分段。

快速恢复:如果收到三次重复确认,则进入快速重传,只将拥塞窗口减半来跳过慢启动阶段,将慢启动阈值设为当前新的拥塞窗口值,进入“快速恢复”阶段。

拥塞控制算法

  • 当主机收到一条新确认,此时可以增加单次发送量。若当前单次发送量小于倍增阈值,则单次发送量加倍(乘以2),即指数增长;否则单次发送量加1,即线性增长。
  • 当主机收到三条重复的确认——单次发送量减半,倍增阈值等于单次发送量。
  • 当主机探测到超时——倍增阈值=单次发送量÷2,单次发送量=1。

丢失包的重传

  • 基于重复累计确认的重传

thirdMove.png 当发送方如果收到3次对同一个包的确认,就重传最后一个未被确认的包。

  • 超时重传 每次发送数据包都有 seq 号,接收端接收后会回复 ACK 确认。发送端在发送了某个数据包后,等待一定时间没有收到对应的 ACK 确认,就认为数据包丢失,会重传这个数据包。等待的一定时间称为超时时间。 超时时间算法
    • 先采样RTT,记下最近好几次的RTT值。
    • 做平滑计算 SRTT=( α*SRTT )+(( 1-α )*RTT ), α 取值在0.8 到 0.9之间
    • 计算RTO RTO=min(UBOUND,max(LBOUND,(β∗SRTT)), 其中 UBOUND是最大的timeout时间上限值,LBOUND是最小的timeout时间下限值,β值一般在1.3到2.0之间。

四次挥手(连接终止)

close.png TCP四次挥手的过程: 现在客户端与服务端都处在连接建立的状态,假设此时服务端想要关闭连接; 【第一个报文】:服务端发送一个 FIN 报文,用来关闭服务端到客户端的连接(也就是服务端告诉客户端:我不会再发送数据给你了),在 FIN 包之前发送出去的数据,如果没有收到对应的 ACK 报文,客户端依旧会重发这些数据,此时客户端还可以接受数据;

以下是 tcpdump 抓包截图,其中 [F.] 表示数据包中的 FIN、ACK 标志位置为 1,seq 表示随机数序号,win 表示窗口大小,实际情况中,FIN 报文可能和 ACK 报文一起发送

close1.png 【第二个报文】:客户端收到 FIN 报文后,发送一个 ACK 给对方,确认序号为收到序号 + 1,此时服务端进入 CLOSED_WAIT 状态。客户端接收到 ACK 报文后,进入 FIN_WAIT_2 状态;

以下是 tcpdump 抓包截图,其中 [.] 表示数据包中的 ACK 标志位置为 1,win 表示窗口大小

close2.png

【第三个报文】:客户端发送一个 FIN 报文,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了,接下来服务端进入 LAST_ACK 状态;

以下是 tcpdump 抓包截图,其中 [F.] 表示数据包中的 FIN、ACK 标志位置为 1,seq 表示随机数序号,win 表示窗口大小

close3.png

【第四个报文】:服务端收到 FIN 报文之后,发送一个 ACK 给服务端,确认应答号为收到序号 + 1,此时服务端进入 TIME_WAIT 状态;

以下是 tcpdump 抓包截图,其中 [.] 表示数据包中的 ACK 标志位置为 1,win 表示窗口大小

close4.png

为什么需要四次挥手 回顾下四次挥手双方发 FIN 包的过程,就能理解为什么需要四次了:

  • 关闭连接时,服务端向客户端发送 FIN 时,仅仅表示服务端不再发送数据了但是还能接收数据。
  • 客户端收到服务端的 FIN 报文时,先回一个 ACK 应答报文,而客户端可能还有数据需要处理和发送,等客户端不再发送数据时,才发送 FIN 报文给服务端来表示同意现在关闭连接。

从上面过程可知,客户端通常需要等待完成数据的发送和处理,所以客户端的 ACK 和 FIN 一般都会分开发送,从而比三次握手导致多了一次。

为什么需要 TIME_WAIT

  • (1)防止旧连接的数据包

closeOld.png 如果没有 TIME_WAIT 这段等待时间,由于网络延迟的消息,就有可能在下次建立连接的时候重新发送到接收端。

  • (2)保证连接的正确关闭

closeShort.png TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。 如果没有 TIME_WAIT 这段等待时间,那么客户端发送的 ACK 应答包在网络中丢失,由于 TIME_WAIT 过短或没有,客户端会进入 CLOSED 状态,而服务端却一直在 LAST_ACK 状态中等待,会导致下一次连接无法建立。 > 如果 TIME_WATI 时间足够长,那么上面这种情况就可以被解决: (1)服务端正常收到四次挥手的最后一个 ACK 报文,则服务端正常关闭连接。 (2)服务端没有收到四次挥手的最后一个 ACK 报文时,则会重发 FIN 关闭连接报文并等待新的 ACK 报文。

TIME_WAIT 为什么是 2 MSL

  • MSL 是 Maximum Segment Lifetime 的缩写,译为报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。RFC793定义了 MSL 为2分钟,Linux 设置成了30s。
  • TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。
  • MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。
  • TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。 比如如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会超时重发 FIN 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。
  • 2 MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME_WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2 MSL 时间将重新计时