网络协议

详解网络分层和网络协议工作原理
帅旋
关注
充电
IT宅站长,技术博主,架构师,全网id:arthinking。

TCP协议 | 三次握手,四次挥手,流量控制,拥塞控制,重传机制

发布于 2020-07-26 | 更新于 2024-05-16

TCP是我们平时用到最多的协议,特别是做web开发的时候,或者互联网后端开发,真的是时时刻刻都会用到,这里我会展开来讲。《TCP/IP详解-卷1:协议》一书中花了6章来讲解TCP的各种功能,单单是从TCP/IP协议栈的名称就可以看出,TCP协议的分量有多重了。为此,面试官张口就聊TCP咋的咋的。

img

与UDP不同,TCP做了很多功能的封装与实现。

先来简单介绍下TCP协议

TCP给应用程序提供给了一种与UDP完全不同的服务。

TCP是面向连接的可靠的服务面向连接指TCP的两个应用程序必须在它们可交换数据之前,通过相互联系来建立一个TCP连接;

TCP提供了一种字节流抽象概念给应用程序:TCP不会自动插入记录标志或者消息边界,这意味着TCP没有限制应用程序的写范围。发送端分两次发10字节和30字节,接收端可能会以两个20字节的方式读入。

我们还是先来看看TCP数据报的格式吧,这个可比UDP复杂多了,但是也是设计的恰到好处的。

1、TCP数据报格式

image-20211024134023823

如上图,头部深黄色部分为TCP特有的重点字段,后面TCP相关功能基本都是靠这些特有的字段来实现的。

  • 源端口号和目的端口号:同UDP一样,主要用于区分数据应该转发给哪个应用;
  • 序号:这个序号是为了解决乱序问题,32位无符号数,到达2^32-1后再重新从0开始;
  • 确认号:确认已经接收到了哪里,该确认序号表示该确认号的发送方期望接收的下一个序列号。该字段只有在ACK位字段被启用的情况下才有效,所以也成为ACK号或者ACK段;
  • 状态位:该状态位会让TCP连接双方的状态发生流转,常见的状态为,后面讲建立连接和断开连接的时候会用到:
    • ACK:回复状态,启用该状态的情况下,确认号有效,连接建立之后一般都是启用状态;
    • SYN:发起一个连接;
    • RST:重置连接,连接去表,经常是因为错误导致;
    • FIN:结束连接,表示该报文的发送方已经结束想对方发送数据;
    • CWR:拥塞窗口减小,发送方降低发送速率;
    • ECE:ECN回显,发送方接收到了一个更早的拥塞通告;
    • URG:紧急,表示紧急指针字段有效,很少用到;
    • PSH:推送,表示接收方应该尽快给应用程序传送这个数据——没有被可靠的实现或用到;
  • 窗口大小:流量的窗口大小,用于流量控制,通信双方各声明一个窗口,这个大小表明了自己当前的处理能力;
  • 校验和:覆盖了TCP的头部和数据,以及伪头部数据(与UDP使用的相似的伪头部进行计算);
  • 紧急指针:只有在URG位启用的时候才有效;
  • 选项:如最大段大小等其他的可选项;
  • 数据:TCP数据报的数据内容。

2、TCP特点

TCP基于以上数据报的各种字段,实现了以下功能:

  • 数据的顺序传输;
  • 丢包重传,保证可靠;
  • 连接维护;
  • 流量控制,保证稳定;
  • 拥塞控制,及时调整,最大程度保证传输正常进行。

3、连接管理

我们首先来看看连接是如何建立的,这里就涉及到TCP的三次握手了。

3.1、TCP三次握手

三次握手流程如下:

image-20211023130118106

可以发现,为了实现可靠连接,双方都需要发起建立连接。具体流程如下:

  • 第一次握手:主动连接方发送一个SYN报文段指明自己想要连接的端口号,以及客户端消息的初始化序列化ISN©;
  • 第二次握手:服务器接收到消息后,也发送自己的SYN报文,包含了服务端的初始化序列号ISN(s),并设置确认号ack=客户端序列号+1;
  • 第三次握手:客户端应答服务器的SYN,将服务端的序列号+1作为ack返回给服务端。

总结一下:客户端与服务端利用SYN报文交换彼此的初始化序列号。在我们熟悉的Socket编程中,三次握手在执行connect的时候触发。

其中的ACK应答和递增的序列化是可靠性的保证。

为什么是三次握手,而不是两次或者四次?

如果是两次:

image-20211023124313638

客户端请求建立连接,服务端收到了请求,并且做出了响应,很明显,服务器没法知道这个响应究竟有没有被接收,也许可能客户端迟迟收不到SYN响应,于是结束了请求。这个时候再传消息网络层就会收到一个ICMP目的不可达的差错报文。

同理:客户端的SYN请求如果迟迟没有服务器的响应,那么也会重发SYN,最终如果服务端可能收到两个SYN,客户端想要建立一个连接,但是服务器收到两个SYN之后,建立了两个连接(当然,实际上的三次握手服务端是会判断客户端的请求序列号的,发现是同一个序列号,并不会建立多个连接,这也说明序列号的重要性)。

为什么不需要四次呢?因为如果服务端和客户端双方都发起SYN,并且收到ACK之后,就都知道对方接受了自己的请求了,已经没有必要再继续确认下去了。

为什么UDP端口号为65535个?

在TCP、UDP协议的开头,会分别有16位来存储源端口号和目标端口号,所以端口个数是2^16-1=65535个。

3.2、TCP四次挥手

接下来我们看看连接关闭的流程,连接的任何一方都可以发起关闭操作,此外,也支持双方同时关闭连接。在传统的情况下,负责发起关闭连接请求的通常是客户端。

这个流程又被称为四次挥手:

image-20211023125730916

  • 连接的主动关闭者发送一个FIN段请求关闭连接,携带了Seq=K,指明接收方希望看到的自己的当前序列号;携带了ack=L,指明自己想要接受到的下一个消息的序号。这个时候,连接主动关闭者表明了自己已经没有数据要发送了,但是仍然可以接受被动关闭者发送的数据;
  • 连接的被动关闭者进行了ACK回应,ack为K+1,表明自己已经成功接收到了主动关闭者发送的FIN。但是自己还未准备好关闭,所以主动关闭者会进入FIN_WAIT_2等待状态;
  • 紧接着被动关闭者也发送了一个FIN端请求关闭连接,携带了Seq=L。告诉主动关闭者自己也准备好了关闭;
  • 最后连接的主动关闭者接收到了对方的FIN关闭请求,也回应了一个ACK,同样的ack=L+1,表明自己已经成功接收到了被动关闭者发送的FIN;

可以发现,因为TCP是全双工的,双方都要单独发起关闭请求,只有当连接双方都发起FIN关闭请求操作,并且得到确认之后,才完成一个完整的关闭操作,这也是被称为四次握手的原因。

信息发送期间的状态流转如上图所示。其中主动关闭者在CLOSED状态之前,有一个TIME_WAIT状态,那么问题来了:

为什么要有TIME_WAIT状态呢?

我们知道主动关闭者在应道对方的FIN请求,有可能对方是收不到的,如果收不到的情况下,那么对方就可能认为自己的FIN请求丢失了,需要重新发起FIN请求,所以主动关闭者需要有一个足够长的等待时间,让对方有重试的机会

等待时间是2MSL(Maximum Segment Lifetime,报文最大生存时间),这也是报文在网络上最大的生存时间,超过了这个时间就会被丢弃。RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。如果超过了这个时间,那么主动关闭者就会发送一个RST状态位的包,表示重置连接,这个时候被动关闭者就知道对方已经关闭了连接:

image-20211023124556465

如果主动关闭者不进行等待,会出现什么问题呢?如下:

image-20211023125212139

可以发现,由于端口复用,主动关闭者已经开启了另一个连接,这个时候被动关闭者还在重试发起FIN请求,导致新主动关闭者新的连接收到了很多没用的包。因为包是有序列号的,所以可以判断到不是本次连接该接收的包。为此,我们需要让主动关闭者进行等待,确保被动关闭者不会再发FIN请求了,再进行端口复用。

3.3、完整连接流程

完整连接流程如下:

image-20211023125530273

可以发现,每个TCP连接在正常的建立和关闭的基本开销是7个报文段,如果只是需要交换很少量的数据时,有写程序更愿意选择使用UDP协议。但是UDP会面临数据丢失,拥塞管理,流量控制等问题。

4、TCP状态机

介绍了三次握手和四次挥手,我们再看看看以下这个TCP状态机就清晰多了。

如果没有看过三次握手和四次挥手流程,不建议直接看这个状态机,真的是太复杂了…不过为了方便大家能够更直观的看出状态流转,我还是绘制了下,加了一些说明:

image-20211023130329016

5、数据传输

5.1、如何保证可靠传输:ACK+序列号

假设主机A通过TCP向主机B发送数据,当主机A的数据到达主机B时,主机B会发送一个确认应答消息ACK。主机A收到ACK之后,就知道自己的数据已经被对方接收了:

image-20211023130448988

如果主机一直没有收到ACK,一定时间之后,就会重发,因此,即使主机A的数据报没有发到主机B,或者主机B的ACK数据包丢失了,也有重传机制,确保双方最终可以通过重传确保能够正确收到消息:

image-20211023130719765

从上图也可以看出,主机A实际发了两次同样的数据给主机B,主机B可以通过序列号,判断是重复数据,然后就丢弃了,但是还是会发送一个ACK告诉主机A已经收到消息。

5.2、流量控制与窗口管理

在TCP头部中,为了实现流量控制,包括顺序问题与丢包问题,我们重点关注TCP头部的这三个字段:序列号,序列号与确认号:

image-20211023130813029

(注意:后面部分数据传输图中的发送方统一称为客户端或者发送端,接收方统一称为服务端或者接收端,实际的数据传输,可以是两台电脑之间,或者是两台服务器之间)

其中TCP头部的窗口字段表明自己的处理能力,代表着可用缓存空间的大小,以字节为单位。

接下来再看看滑动窗口。

TCP连接的每个端都可以收发数据,每个端的收发数据量是通过一组窗口结构来维护的。每个端都会包含一个发送窗口结构和接收窗口结构。

发送窗口结构

发送窗口结构如下图所示:

image-20211023131048138

其中:

  • SND.WND:提供窗口大小是由接收返回的ACK中的窗口大小字段控制的;
  • SND.UNA:记录窗口左边界的值;
  • SND.UNA + SND.WND:记录窗口右边界的值;
  • SND.NXT:记录下次发送的数据

所谓窗口,就是左右边界会根据情况进行调整的窗口,由主要三个动作:

  • 关闭:窗口左边界右移,当已发送的数据得到ACK的时候,就会进行关闭,提供窗口大小减小;
  • 打开:窗口左边界右移,当已确认的数据得到处理后,那么接收端可用缓存就会变大,这个时候通过打开操作让提供窗口大小变大;
  • 收缩:窗口右边界左移,使得提供窗口大小减小;

接收窗口结构

接收窗口与发送窗口结构类似,如下图:

image-20211023131018873

从滑动窗口看如何保证可靠传输:顺序与丢包问题

为了避免接收重复数据:接收到的数据包小于左边界,说明是已经确认过的,将把数据报丢弃;如果接收到的数据报序列号大于右边界,说明暂时超出了处理能力范围,也将会被丢弃。

为了保证已确认数据包的连续性,接收到的数据包的序列号与已确认 已接受部分连续的时候,才表示真正的已确认,左边界才可以右移。

image-20211023131159764

5.3、超时重传机制

基于计时器的重传超时机制(Retransmission Ttimeout, RTO)

TCP在发送数据的时候会设置一个重传计时器,如果计时器超时仍然没有收到ACK确认信息,那么会进行重传操作。

如下图是超时重传的演示说明例子:

image-20211024135811176

对于接收方来说,1,2,3都已经接收并且发送ACK了,3的ACK丢失了。

**ACK丢失的场景:**过了一段时间,3的计时器发现超时了,于是会触发超时重传。但是这个时候接收方发现3是在已接受已确认区域,于是会丢弃3,并反馈一个ACK;

**数据丢失的场景:**4和5的数据传输丢失了,计数器发现超时,也会进行超时重传,保证4和5可以传给接收方,并拿到ACK反馈。

关于重传时间间隔

ICMP端口不可达案例中,采用UDP的TFTP客户端使用简单且低效的超时重传策略:设置足够大的超时间隔,每5秒进行一次重传;

而TCP的基于计时器的重传策略是如果发生重试,可以有两种处理方式:

  • 一种是基于拥塞控制机制,减小发送窗口大小;
  • 另一种是超时时间间隔会一直加倍。

关于重传时间

重传时间需要讲到自适应重传算法,一种计算重传时间的算法,大致流程:

TCP通过采样RTT的时间,进行加权平均,算出一个值,最终得到一个估计的重传时间。

1
2
3
初始值:原始值
测量之后:RTO = RTTs + 4*RTTd
(RTTs:加权平均值,RTTd:偏差值)

因为网络是不断变化的,所以重传时间也会处于变动状态。

基于反馈信息的快速重传机制

快速重传机制是这样的:当接收方接收到一个序列号大于下一个所期望的报文段的时候,就会检测到数据流中间丢失的间隔,然后发送冗余的ACK,向发送者索要确实的间隔。当发送者收到一定数量的冗余的ACK(称为重复ACK的阈值或dupthresh)之后,就不等定时器过期了,直接重传丢失的 报文。

重复ACK的阈值通常为3,一些非标准化的实现可基于当前的失序程度动态调整。

如下例所示:发送方的4、5、6、7都已经发送出去了,但是接收方接收到了5、6、7,少了4,会在分别收到5、6、7的时候都发一个3的ACK,向发送方索要下一个数据4。这样发送方就收到到3个3的ACK了,于是就主动发起了4的重传,不等待重传计时器超时了:

image-20211023131327625

带选择确认的重传SACK

虽然重传保证了数据的到达,但是重传应该尽可能保证不重传以正确接收到的数据,而SACK信息能更快速的实现空缺填补并且减少不必要的重传。

随着选择确认选项的标准化[RFC2018],TCP接收端可以提供SACK的功能了,通过TCP头部的累计ACK号字段来描述其接收到的数据。

每当缓存存在失序数据时,接收端就可以生成SACK,代表着缓存接收状态地图,这样通过将缓存的接收状态地图发给发送方,发送方就很快可以知道是什么数据丢失并发起重传了。

这种重传机制下,窗口内的其他报文段也可以被接收确认,但只有在接收到等于窗口的左边界的序列号时,窗口才会前移。这样就减少了窗口内的不必要的重传。

5.4、流量控制

流量控制指的是通过控制发送方和接收方的窗口大小,以使得接收方缓存中已接受的数据处理不过来时,通过减小发送方的窗口大小,让接收方能有足够的时间来接收数据包;或者是接收方比较空闲时,尝试让发送方调大窗口大小,以加快传输,合理利用空闲的网络资源。

**流量控制主要是通过TCP头的窗口大小来调节的。**发送端收到接收端的通告窗口之后,得知接收端可接收的数据量。

下面举例来说明。

正常情况下,发送方左边界每关闭一格,右边界就打开一个,多一个可发送的单元:

image-20211024135927547

我们知道接收端接收并确认数据之后,会放到缓存中,等待应用程序处理,如果应用程序一直没有处理,最终会导致接收端没有更多空间来存储到达的数据了,如果应用程序一直没有处理数据,那么窗口右边界可能就不会打开了,最终接收的窗口大小变为0:

image-20211023131656905

这个时候接收端就会发送一个零窗口通告(TCP ZeroWindow),告知发送端不要再发送数据了,我已经处理不过来了,于是发送方就暂停发送数据了,等待接收端的窗口更新(TCP Window Update)通知:

image-20211023132036401

这样,接收方就可以有时间来处理接收的数据了,等到有了足够多的缓存之后,于是会给发送端传输一个窗口更新通知。

为了避免由于窗口更新通知ACK丢失,到时双方陷入等待的僵局,在发送方停止发送数据之后,会采用一个持续计时器间歇性的查询接收端,给接收端发送窗口探测(TCP ZeroWindowProbe)请求,要求接收端返回TCP ZeroWindowProbeAck,看看是否窗口是否已经增加了:

image-20211023131939312

5.5、拥塞控制

前面我们讲到,可以通过滑动窗口大小来控制流量,从而为接收方缓解压力,避免不必要的丢包。

而拥塞控制,就需要用到拥塞窗口了。拥塞控制主要用于避免丢包和超时重传。

反映网络传输能力的变量称为拥塞窗口(congestion window),记为cwnd。

可以理解为**滑动窗口是为接收方服务的,而拥塞窗口是为整个网络通道服务的,拥塞窗口大小又会受制于接收方滑动窗口大小,并且会因为网络原因进行调整。**因为网络通道中的任何一个环节都有可能影响整体的传输效率。

5.5.1、发送实际可用窗口

那么我们发送端实际可用窗口应该是多少了,这里我们记实际可用窗口大小为W,那么W为接收端通知窗口awnd和拥塞窗口cwnd的较小者:

1
W = min(cwnd, awnd)

假设网络没有任何问题,并且带宽足够宽,数据包不会在传输过程遇到需要排队等待的情况下,这种理想状况下,也就是没有网络延迟,接收方收到一个数据包,立刻就ACK一个,立刻空出一个可传输单元,发送的实际可用窗口就是接受方的滑动窗口大小了,如下:

image-20211023132124524

理想是很美好的,但是实际网络情况是非常复杂的,TCP根本不知道里面会发生什么情况,也许W还没到达接收端滑动窗口大小,网络中就因为中间的瓶颈导致丢包了,那么更加会增加重传的频率。所以为了能减少丢包和超时重传,需要有一些动态发送端窗口大小的策略。

5.5.2、发送端窗口调整策略

虽然可以通过接收方的ACK得到对方的接收窗口大小,但是因为刚开始并不知道拥塞窗口是多少,所以只能以越来越快的速率不断发送数据,直到出现数据包丢失为止。

通常TCP在建立新连接的时候会执行慢启动,直到有包丢失,然执行拥塞避免算法进入稳定状态。

慢启动

初始窗口设为IW(Initial Window, IW),IW=SMSS(发送方的最大段大小)。

先发送初始窗口大小的数据,没有出现丢包,并且每收到一个ACK,慢启动算法就会以min(N,SMSS)来增加cwnd的值。可见这是指数性的增长。

直到出现了网络拥塞,出现丢包、超时重传,说明已经到达了慢启动的阈值ssthresh(slow start threshold),这个时候cwnd减少一半,并作为新的ssthresh。

避免拥塞

一旦达到慢启动的阈值之后,为了得到更多的传输资源而不影响其他连接的传输,TCP实现了拥塞避免算法。一旦确定慢启动阈值,TCP会进入拥塞避免阶段,这个时候cwnd每次的增长值近似于成功传输的数据段大小。也就是说由原来慢启动的指数增长,变为了线性增长。

References

本文作者: 帅旋

本文链接: https://www.itzhai.com/columns/network/transport-layer/tcp.html

版权声明: 版权归作者所有,未经许可不得转载,侵权必究!联系作者请加公众号。

×
IT宅

关注公众号及时获取网站内容更新。

请帅旋喝一杯咖啡

咖啡=电量,给帅旋充杯咖啡,他会满电写代码!

IT宅

关注公众号及时获取网站内容更新。