计网 | 一文解析TCP协议所有知识点

零、TCP 简介

第一部分先为大家介绍一下 TCP 的主要概念,并讲解一下 TCP 的三个重要特性——

1. 面向连接;2. 基于字节流;3. 可靠性。

关于网络分层的概念实在是老生常谈了,下图就是两种经典的分层模型,可以看到 TCP 在网络分层中的位置。

        本文重点对 TCP 进行介绍,从图中可以看到 TCP 位于传输层,而且构建于网络层的 IP 协议之上,对于 TCP 最常见的介绍就是 “TCP 是一种面向连接的、可靠的、基于字节流的传输层通信协议”,那这三个形容词究竟是什么意思呢?

        1 面向连接

        面向连接意味着两个使用 TCP 的应用 (通常是一个客户端和一个服务器) 在彼此交换数据之前必须先建立一个 TCP 连接。这一过程与打电话很相似,先拨号响铃,等对方应答后再说明是谁。详细的三次握手、四次挥手过程将在第二部分——连接管理部分进行介绍。

        2 基于字节流

        TCP 连接双方的数据交换格式是以字节 (byte,1byte = 8 bit)构成的有序但无结构的字节流。TCP 不在字节流中插入记录标识符,这被称为字节流服务(byte stream service)。

        如果一方的应用程序先传 10 字节,又传 20 字节,再传 50 字节 ,连接的另一方将无法了解发方每次发送了多少字节。收方可以分 4 次接收这 80 个字节,每次接收 20 字节。一端将字节流放到 TCP 连接上,同样的字节流将出现在 TCP 连接的另一端。另外,TCP 对字节流的内容不作任何解释,TCP 无法知道传输的数据字节流是二进制数据,还是 ASCI I 字符。

        如果觉得上面这段话比较抽象的话,可以拿 TCP 的字节流和 UDP 的报文 (message) 进行比较(UDP:User Datagram Protocol,用户数据报协议,和 TCP 同为传输层的协议,后面会提供两者的全面对比)。

        TCP 的字节流类似于自来水,连接双方都有缓冲区,可以类比成蓄水池,发送方的发送频率和每次的发送量没有固定要求,接收方也可以自由决定自己的接收频率和每次的接收量,只要把所有的数据接收完毕即可。而 UDP 的报文则类似于瓶装水 (比如农夫山泉),发送方发送一瓶,接收方就要相应地接收一瓶。

        下图描述了 TCP 连接中数据的传输过程以及 TCP 在整个过程中所扮演的角色。

        按照图中的流程,比如我们在浏览B站,在 TCP 连接建立之后,客户端的应用层协议可以向 TCP 发送无特殊格式的字节流,TCP 会将这些字节打包成报文段(segment),报文段大小视情况而定,这些报文段会被网络层的 IP 封装成 IP 数据报(IP Datagram),然后经过网络传输给服务器,而接下来服务器的操作相当于客户端的逆操作,先从 IP 数据报中拆分出 TCP 报文段,再把 TCP 报文段还原成字节流并发送给上层的应用层协议。服务器向客户端发送数据的流程也是一样的,发送方和接收方的角色互换即可。

        3 可靠性

        我们都知道 TCP 是具有可靠性的通信协议,它主要通过以下方式确保可靠性,这里先了解一下可靠性的原理,其中细节部分后文会讲:

  • 合理的数据大小:TCP 发送的数据并不是固定的大小,而是会根据实际情况调整报文段的大小。
  • 检验和:发送端按照特定算法计算出 TCP 报文段的检验和并存储在 TCP 首部中的对应字段上,接收端在接收时会以同样的方式计算校验和,如果不一致,说明报文段出现错误,会将其丢弃。
  • 序号与确认序号:对乱序的数据进行排序后发给应用层,并丢弃重复的数据。
  • 超时重传机制:当 TCP 发出一个报文段后,它会启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段,后面会细讲这个机制。
  • 连接管理:也就是三次握手和四次挥手,连接的可靠性是整体可靠性的前提,本文第二部分将会详细介绍连接管理的内容。
  • 流量控制:TCP 双方都有固定大小的缓冲区,流量控制的原理是利用滑动窗口控制数据发送速度,避免缓冲区溢出导致数据丢失。
  • 拥塞控制:TCP 利用慢启动和拥塞避免等算法实现了拥塞控制。

        4 TCP 和 UDP 的区别

 


一、TCP报文结构

在学习TCP控制报文协议之前,必须要先了解TCP的报文结构。在TCP连接中传输的单元都是一个个相同结构的TCP报文段,每个报文段都有首部信息,这些首部信息能告诉它从哪里来,到哪里去,同时TCP底层也通过首部来控制传输流量,网络拥塞状况,保证可靠性、准确性等。

图的上半部分显示 TCP 报文段被封装在 IP 数据报中,图的下半部分则显示了 TCP 报文段和 TCP 首部的结构,TCP 首部的固定数据有20字节,加上选项部分最大可达60字节,而有效数据部分则是被打包的应用层数据。下面介绍一下 TCP 首部的结构:

  • 端口号 (Source Port and Destination Port)
    • 每个 TCP 报文段都包含源端和目的端的端口号,用于寻找发送端和接收端应用进程。这两个值加上 IP 首部中的源端 IP 地址和目的端 IP 地址就可以确定一个唯一的 TCP 连接。
  • 占用16位的源端口号和目的端口号,标识了发送方和接收方的进程标识;
  • 序号 (Sequence Number)
    • 32位的序号字段表示数据报文的编号,应用层要发送一段应用数据,当传送到传输层后,如果应用数据大小超过MSS(最大报文段长度)时,就会对应用层数据字节流从头到尾按顺序进行编号,然后再切分为一个个MSS大小的TCP报文段,每个报文段的序号字段就是该分段的第一个字节的序号;
    • 这个字段的主要作用是用于将失序的数据重新排列。TCP 会隐式地对字节流中的每个字节进行编号,而 TCP 报文段的序号被设置为其数据部分的第一个字节的编号。序号是 32 bit 的无符号数,取值范围是0到 2^32 - 1。
  • 确认序号 (Acknowledgment Number)
    • 32位的确认序号字段表示希望对端发送的下一个报文段的序号。由于TCP是一种全双工的通讯协议,即发送方既发送数据也可能接收对端发送的数据,如源主机发送0-1000编号的字节段给目的主机,这时目的主机就会发送一个响应报文且确认序号为1001,表示希望收到下一个报文段编号为1001的数据,它和序号字段共同保证了TCP报文的可靠性;
    • 作用就是告诉发送方自己接收到了哪些数据,下一次数据从哪里开始发,因此,确认序号应当是上次已成功收到数据字节序号加 1。只有 ACK 标志为 1 时确认序号字段才有效。
  • 首部长度 (Header Length)
    • 4位的首部长度表示TCP报文首部的长度,该字段以4字节为单位。
    • TCP首部的长度是可变的,在特殊情况下选项字段会有值,也计入首部长度,但一般情况下选项都会为空,这样首部长度就是20个字节,因此该字段的数值就为5,用4位二进制表示就是0101;
  • 保留字段 (Reserved)
    • 占 6 位,未来可能有具体用途,目前默认值为0。可能以后TCP版本升级会被征用;
  • 控制位 (Control Bits):在三次握手和四次挥手中会经常看到 SYN、ACK 和 FIN 的身影,一共有 6 个标志位,它们表示的意义如下:

    • URG (Urgent Bit):值为 1 时,紧急指针生效;URG标识数据中需要被上层处理的紧急数据,而紧急数据部分是通过紧急指针字段来标识哪一段属于紧急数据;
    • ACK (Acknowledgment Bit):值为 1 时,确认序号生效,和确认序号结合使用
    • PSH (Push Bit):接收方应尽快将这个报文段交给应用层
    • RST (Reset Bit):发送端遇到问题,想要重建连接
    • SYN (Synchronize Bit):同步序号,用于发起一个连接,TCP连接的建立
    • FIN (Finish Bit):发送端要求关闭连接
  • 窗口大小 (Window)
    • TCP的流量控制由连接的每一端通过声明的窗口大小来提供,用于标识自己能接受的最大报文数,控制TCP连接的数据流量;
    • 窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端正期望接收的字节。窗口大小是一个 16 bit 字段,单位是字节, 因而窗口大小最大为 65535 字节。
  • 检验和 (Checksum)
    • 功能类似于数字签名,用于验证数据完整性,也就是确保数据未被修改。检验和覆盖了整个 TCP 报文段,包括 TCP 首部和 TCP 数据,发送端根据特定算法对整个报文段计算出一个检验和,接收端会进行计算并验证。
  • 紧急指针 (Urgent Pointer)
    • 当 URG 控制位值为 1 时,此字段生效,紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。 TCP 的紧急方式是发送端向另一端发送紧急数据的一种方式。
  • 选项 (Options)
    • 这一部分是可选字段,也就是非必须字段,最常见的可选字段是“最长报文大小 (MSS,Maximum Segment Size)”。
  • 有效数据部分 (Data)
    • 这部分也不是必须的,比如在建立和关闭 TCP 连接的阶段,双方交换的报文段就只包含 TCP 首部。

 
       


二、TCP连接

基于TCP的应用层要进行通讯时,发送消息的两端要先建立可靠的TCP连接,然后才能在连接基础上收发消息。这里的TCP连接只是逻辑链路,是按照TCP的连接规则在传输层上假定的一个逻辑连接。在实际网络传输中,两端数据发送过程还要往下经过网络层和数据链路层,最终经过一层层的网络节点到达目的主机,但这是另一个知识点了,本文侧重分析TCP的连接细节。

1、Socket连接的建立

Socket是在应用和传输层之间的一个抽象层,它把传输层TCP的复杂连接操作抽象为几个步骤,方便应用层在实现点对点通讯时的连接操作。Socket分为SOCK_STREAMSOCK_DGRAMSOCK_RAW三种类型,SOCK_STREAM用于提供可靠的传输服务,只能读写TCP数据报,SOCK_DGRAM只能读写UDP数据报,SOCK_RAW可读写原始数据报即IP数据报。本文主要为了分析TCP协议,因此我们通过SOCK_STREAM来分析TCP的连接细节。

2、Socket的处理过程

Socket通讯是一个客户/服务端的结构,假设客户端向服务端发起一个Socket通讯连接。如下图所示,服务端要先调用socket()函数创建一个Socket,然后通过bind()绑定端口号和IP地址,端口号是为了客户端发送数据过来时能找到对应要处理的进程,而绑定IP是由于一台主机可能有多个网卡,需要选择指定的网口去接收。当服务端有了 IP 和端口号,就可以调用 listen 函数进行监听,这个时候客户端就可以发起连接了。在服务端等待的时候,客户端可以通过 connect 函数发起连接。先在参数中指明要连接的 IP 地址和端口号,然后开始发起三次握手。内核会给客户端分配一个临时的端口。一旦连接成功,服务端的 accept 就会返回另一个 Socket。

3、Socket服务端特性

此外,服务器要接受客户端连接,必须先创建一个用于连接客户端的Socket,该Socket绑定了端口号和IP地址在主机B运行起来,然后主机A会指定服务端的IP地址和端口号发起连接请求,客户端在发起连接时底层会自动为它创建一个本地IP地址的Socket,并为该Socket绑定一个随机端口号。而服务端的连接Socket收到连接请求并确认连接可用后会为该主机创建一个独有的通讯Socket用于收发数据报(这个过程就是上图 accept 函数的处理过程),如下图所示:

服务端的连接Socket相当于一家公司的前台服务,用来对接所有访客的到访,然后再根据客户的来访目的,为他分配一个接待员,即这里的通讯Socket,后续客户所有的需求就都由接待员来提供,即后续Socket两端的数据收发都由通讯Socket来完成。


我终于搞懂了TCP的三次握手和四次挥手(图片案例超详解)_辰兮要努力的博客-CSDN博客_tcp三次握手和四次挥手的全过程

4、三次握手

前面介绍的Socket连接中,有提到客户端调用connect()函数后会经过TCP三次握手的连接过程。这三次握手过程就是收发双方来回发送了三个TCP数据报后就确定了连接关系,可以开始发送数据了。那三次握手的规则是怎么样的呢?接下来为你详细分解:

  1. 首先,客户端和服务端原本互不相关,都处于连接关闭状态。现在客户端主动发起连接,就发送一个TCP报文段(称为SYN报文),SYN标志位置1,序号字段设为一个原始编号x,这个原始编号可以认为是一个32位计时器,每4微秒加1,这样是为了防止序号重复,该原始编号同时也是应用层数据包的起始编号;
  2. 服务端收到SYN报文后,接着会给客户端发送一个ACK置1、确认序号为x+1响应报文,客户端发送的SYN报文段会消耗一个序号,虽然SYN报文没有数据;与此同时,TCP是全双工通讯,服务端也会发送数据给客户端,因此响应报文也会将SYN置为1,序号设置为服务端的原始编号y,该编号同样是通过服务端的系统计时器得出的一个随机数;
  3. 客户端收到服务端的响应后,再对服务端的SYN发送一个ACK置1、确认序号为y+1的响应报文,而序号同样为x+1,这说明确认报文是没有消耗序号的。

自此,TCP连接就建立了。

 这是 TCP 建立连接的特殊情况,有时会出现两台机器同时执行主动打开的情况,不过概率非常小,这种情况大家仅作了解即可。在这种情况下就无所谓发送方和接收方了,双放都可以称为客户端和服务器,同时打开的过程如下:

同时打开的过程

如图所示,双方在同一时刻发送 SYN 报文段,并进入 SYN-SENT 状态,在收到 SYN 后,状态变为 SYN-RECEIVED,同时它们都再发送一个 SYN + ACK 的报文段,状态都变为 ESTABLISHED,连接成功建立。在此过程中双方一共交换了4个报文段,比三次握手多一个。


5、四次挥手

        建立一个连接需要三次握手,而终止一个连接要经过 4次挥手。这由 TCP 的半关闭( half-close) 造成的。既然一个 TCP 连接是全双工 (即数据在两个方向上能同时传递), 因此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向连接。当一端收到一个 FIN,它必须通知应用层另一端已经终止了数据传送。理论上客户端和服务器都可以发起主动关闭,但是更多的情况下是客户端主动发起。

  1. 客户端发送关闭连接的报文段,FIN 标志位1,请求关闭连接,并停止发送数据。序号字段 seq = x (等于之前发送的所有数据的最后一个字节的序号加一),然后客户端会进入 FIN-WAIT-1 状态,等待来自服务器的确认报文。
  2. 服务器收到 FIN 报文后,发回确认报文,ACK = 1, ack = x + 1,并带上自己的序号 seq = y,然后服务器就进入 CLOSE-WAIT 状态。服务器还会通知上层的应用程序对方已经释放连接,此时 TCP 处于半关闭状态,也就是说客户端已经没有数据要发送了,但是服务器还可以发送数据,客户端也还能够接收。
  3. 客户端收到服务器的 ACK 报文段后随即进入 FIN-WAIT-2 状态,此时还能收到来自服务器的数据,直到收到 FIN 报文段。
  4. 服务器发送完所有数据后,会向客户端发送 FIN 报文段,各字段值如图所示,随后服务器进入 LAST-ACK 状态,等待来自客户端的确认报文段。
  5. 客户端收到来自服务器的 FIN 报文段后,向服务器发送 ACK 报文,随后进入 TIME-WAIT 状态,等待 2MSL(2 * Maximum Segment Lifetime,两倍的报文段最大存活时间) ,这是任何报文段在被丢弃前能在网络中存在的最长时间,常用值有30秒、1分钟和2分钟。如无特殊情况,客户端会进入 CLOSED 状态。
  6. 服务器在接收到客户端的 ACK 报文后会随即进入 CLOSED 状态,由于没有等待时间,一般而言,服务器比客户端更早进入 CLOSED 状态。

同时关闭

之前在介绍 TCP 建立连接的时候会有一种特殊情况,那就是同时打开,与之对应地, TCP 关闭时也会有一种特殊情况,那就是同时关闭,这种情况仅作了解即可,流程图如下:

 

同时关闭过程

这种情况下,双方应用层同时发出关闭命令,这将导致双方各发送一个 FIN,两端均从 ESTABLISHED 变为 FIN_WAIT_1,两个 FIN 经过网络传送后分别到达另一端。收到 FIN 后,状态由 FIN_WAIT_1 变迁到 CLOSING,并发送最后的 ACK,当收到最后的 ACK 时,为确保对方也收到 ACK,状态变化为 TIME_WAIT,并等待 2MSL 时间,如果一切正常,随后会进入 CLOSED 状态。


三、TCP的可靠性

我们都知道TCP是一个可靠的通讯协议,它能保证数据的有序性、不丢失和无差错。那这些特性是如何保证的呢?前面我们已经有了解过序号和确认序号的使用了,就是这两个字段保证TCP的这些特性。应用层将要发送的数据不断地发送到Socket中,行程的数据流就不断地交给TCP处理,由于字节流本身是有顺序的,TCP会先将这些字节流编号,而起始编号是以ISN作为初始序号(ISN是根据系统计时器生成,防止和其他TCP连接重复)。编号后再将字节流切割成一个个MSS大小的TCP报文段,每个报文段的首字节编号作为此TCP报文首部的序号字段。

1、累积确认

而对于客户端发送的每一个TCP报文段,服务端都要对应发送一个确认报文段,如果在一定时间内没有收到服务端的确认报文,客户端就会认为这个报文可能丢失了,就会重发,这样就保证了不丢失的特性。假设客户端发送0-999字节的报文段到服务端,服务端就会发送一个确认序号为1000的确认报文段,代表服务端接下来希望接收1000编号开始的报文段。但这样发送一个报文响应一个报文,一来一回的传送方式效率低下,真实的场景可能是客户端连续发送了多个TCP报文段,服务端只发送一个确认报文,就能确认所有客户端发来的报文,这叫做累积确认。如,客户端发送了0-999、1000-1999和2000-2999三个报文段,服务端连续收到了三个TCP报文段后,只发送一个确认序号为3000的报文段就能确认以上的三个报文。

2、保证数据报有序性

客户端发送了多个报文后,每个报文都是独立地往底层传输,可能每个报文经过的网络路由都不一样,因此三个按照顺序发送的报文,可能会不按顺序到达服务端。那服务端是怎么解决的呢?它是通过只确认最后一个按序到达的TCP报文来解决的,比如,服务端刚发送完确认序号为3000的ACK报文段后,接着收到了一个4000-4999的报文段,这时由于还有一个3000-3999的报文段未收到,因此服务端不会发送确认序号为5000的ACK报文,而是先将4000-4999的报文段存放到接收缓存中,然后还是继续发送确认序号为3000的ACK报文段。等到失序的报文段收到后再发送一个确认序号5000的ACK来累积确认所有已收到的报文。


四、TCP的流量控制

TCP连接建立后,每一侧的主机都为该连接建立了一个接收缓存。当连接的一端收到有序、正确的报文段后就将数据存放到接收缓存中,然后应用程序再从缓存中读取数据,被读取过数据就会从缓存中删除。但有时候应用程序处理速度较慢时,就会导致接收缓存占满空间,这时发送方继续发送报文时就会因为缓存溢出而被丢弃,因此TCP需要提供一个流量控制服务来防止缓存溢出的情况。那TCP是如何实现流量控制呢?

TCP通过在每次双方收发数据时,都给对方通告一个窗口大小,该值是接收方可用缓存的大小,用来告诉发送方我最多只能接收这么多数据。该窗口大小是通过TCP报文段结构中的16位窗口大小字段来传给对方的。由于TCP是全双工协议,两端主机都会各自维护一个接收窗口大小的变量rwnd,这个变量是发给对端的,同时自己也会维护一个发送窗口大小,这个窗口大小是控制自己发送数据的快慢。下图是关于接收窗口的计算:

1、滑动窗口

不管是流量控制还是等下要分析的拥塞控制,其最终实施方案都是通过控制发送窗口的大小来实现的。TCP协议规定发送窗口的大小是取拥塞窗口和接收窗口的较小者。但现在我们先忽略拥塞情况,假设发送窗口大小只受接收端接收窗口的影响,且发TCP连接数据传输是单向的,发送端只发送数据,接收端只接收数据,如图所示是主机A的发送窗口随着主机B响应报文通报的窗口大小而动态滑动的示意图:

主机A和主机B建立TCP连接后,主机B通报其接收窗口大小为400字节,因此主机A将初始发送窗口设置为400,通过图左边的蓝色背景代表发送主机A的发送窗口大小。这时主机A将seq为0和100的两个报文段发送出去,随即在左边的缓存中用深黄色标注出已发送待确认的报文。之后从主机B收到响应报文,ack为200代表主机B对序号为200以前的两个报文段进行累积确认,同时接收窗口rwnd减小为300,这时有可能是主机B虽然已收到两个确认报文,都将两个报文存入接收缓存,但应用层只读取了一个,还有一个占用了缓存导致接收窗口减小了100;主机A收到响应报文后,发现0-99和100-199的报文段已经确认,于是将蓝色窗口向右移动到200-299报文段的左边缘,由于此时接收窗口减小为300了,因此右边缘向左移动一个报文段的位置。这时包含在蓝色窗口内的是200-299、300-399和400-499三个可发送未发的报文。这时主机A就可以同时将三个报文段都发送出去,并将其标注为已发送未确认的深黄色。再次收到ACK报文时,确认了400以前的200-299、300-399报文,同时rwnd减小到100,窗口还是按照规则滑动,变成只剩一个换色报文段窗口。

 2、零窗口和持续计时器

在主机B发送最后一个响应报文后,通报接收窗口为0,这时主机A的发送窗口也随着变为0,这时就是零窗口状态,这时主机A就不能继续发送数据了,只能等主机B将接收窗口更新为大于0后才能继续发送,但如果接收方发送更新窗口的确认报文在中途丢失了,这时双方就处于假死状态。因此当窗口为0时,客户端会启动持续计时器,在计时器触发时主动发送零窗口探测报文段,服务端收到探测报文后就会同步发送一个响应报文并更新窗口大小,但也有可能探测报文也会丢失,还是回到原来的问题了。因此零窗口探测报文也有一个重传计时器,为了避免该探测报文段也丢失的情况。

五、TCP的拥塞控制

对网络中某一资源的需求超过了该资源所能提供的可用部分,网络性能就要变坏,这种情况称为拥塞,若出现拥塞而不进行控制,整个网络的吞吐量将随输入负载的增大而下降。因此发送端引入了一个拥塞窗口变量cwnd来动态了解网络的拥塞情况,前面滑动窗口大小rwnd是怕数据把接收窗口塞满,而拥塞窗口cwnd是怕数据把网络塞满,因此需要根据两个值来决定发送窗口的大小。

那怎么判断发送多少数据能把整个网络塞满呢?其实,相对于网络层来讲,TCP是上层协议压根就不知道底下的网络状态,而发送端在发送数据,就像往一个瓶子里倒水,不能一下子往里面灌进太多水,这样会溅出来,同样拥塞控制也是使发送的数据不出现丢包、超时重发前提下,尽量发挥带宽作用、尽快地发送数据。

因此,TCP设计了几个四个算法用来控制拥塞窗口慢开始拥塞避免快重传快恢复,我们通过一个例子来说明四个算法。首先,假设接收方有足够大的接收缓存,因而发送窗口仅受拥塞窗口影响。

  • 发送方维护一个拥塞窗口cwnd的变量,其值取决于网络的拥塞情况,且动态变化;
    • 拥塞窗口cwnd的维护原则:只要网络没有出现拥塞,cwnd就再增大一些,如果出现拥塞时,cwnd就再减小一些;
    • 出现拥塞的判断依据:没有按时收到应当到达的确认报文(即出现超时重传的情况);
  • 发送方将拥塞窗口作为发送窗口swnd,即swnd = cwnd;
  • 发送方维护一个慢开始门限的变量ssthresh的状态变量:
    • 当swnd < ssthresh时,使用慢开始算法;
    • 当swnd > ssthresh时,停止使用慢开始算法而改用拥塞避免算法;
    • 当swnd = ssthresh时,既可以使用慢开始算法也可以使用拥塞避免算法;

1、慢开始

慢开始的算法是先设置发送窗口大小cwnd为1(1代表一个MSS大小报文段),发送完报文后,在不发生超时重发情况下每收到一个TCP报文段的确认后,cwnd就加1,即确认了几个报文就加几。刚开始发送1个报文段,当收到这个报文段的确认后,cwnd就加1变为2,这时就可以同时发送两个报文段,当收到这两个报文段的累计确认后,cwnd就加2变为4,继续同时发送四个报文出去,当收到4个报文段的累计确认后,swnd就加4变为8了。由此可知,慢开始算法是一个指数增长的关系,cwnd增长得很快,之所以叫慢开始其实是指一开始的发送窗口比较小。由于发送窗口是指数方式增长,迟早会把整个网络带宽塞满,那什么时候会结束这种指数增长呢?一般会有两种情况:

当有一个发送的报文出现超时未收到响应时,就代表出现了拥塞了。这时就将swnd重新设置为1,同时将慢开始门限变量设为出现拥塞时cwnd的一半,即ssthresh=cwnd/2,随后继续进行慢开始算法。结束慢启动算法的第二种情况是与慢开始门限ssthresh有关了,当cwnd增大到超过或等于ssthresh时,就要停止慢开始算法而改用拥塞避免算法。

2、拥塞避免

当使用拥塞避免算法时,此时的拥塞窗口刚好是使用慢开始出现拥塞时的一半,这个时候可能举例出现拥塞也不远了。因此,就不能像慢开始一样快速地增加拥塞窗口大小了,而是每次收到一个数据报文的确认时,就将cwnd增加1/cwnd。假如现在的cwnd为8,于是就同时将8个TCP报文段发送出去,当收到所有8个报文段的累计确认时,就将cwnd增加8 * (1/8),即增加1个报文段为9,然后继续发送9个报文段出去,又收到9个报文段的累计确认时,又将cwnd增加1变为10,由此可知,拥塞避免算法是以线性方式增长的。那什么时候结束这种线性方式增长呢?和慢开始算法碰到拥塞情况时一样,当出现拥塞时,就将即ssthresh设为原来的一半,cwnd重新设置为1,又重新开始执行慢开始算法,然后算法流程还是和原来的一样重复执行。

3、快重传

在使用慢开始和拥塞避免算法时,当出现数据报超时时,就认为是出现了网络拥塞。但这并不是绝对的,当网络还是很流畅时,传输过程中有个别数据报由于各种原因导致丢失了,这时发送方就发现此数据报超时未确认,如果因此就认为是网络拥塞导致的,就会误用拥塞算法而将cwnd减小为1,这无疑就降低了传输效率了。因此,就出现了快重传算法来解决这种个别数据报丢失的情况。

快重传的原则是发送端在发现个别报文丢失时,就应该尽快重传,而不是等到超时了再重传。那发送端怎么知道是个别报文丢失呢?如下图所示,A主机和B主机已建立连接情况下,A主机先发送一个数据报文0,正常收到报文,接着发送第二个报文100,该报文在传输工程中丢失了,主机B就收不到报文100的数据,接着主机A继续发送其他报文200,该报文在主机B正常收到,但主机B发现报文100还没收到,先收到报文200,于是将报文200存放到接收缓存,继续发送确认序号为100的ACK报文,代表希望从主机A收到报文100。接着主机A继续发送报文300和400,两个报文都正常到达主机B,主机B由于设置不使用经受时延的确认,对每个收到的报文都要及时发送ACK报文,也就连续一共发送了三次确认序号为100的响应报文。当客户端连续收到三个连续的相同确认序号的报文端时,就知道这个报文可能中间丢失了,于是就要重发报文100,当服务端收到这个空缺的报文段后,就已收到的失序报文一起累计确认。

4、快恢复

当发送方连续收到三个重复的ACK报文时,就知道现在只是个别报文端丢失了,于是不启用慢开始算法,而改用快恢复算法。快恢复算法一般有两种实现方案:

  • 发送方将慢开始门限ssthresh值和拥塞窗口cwnd都调整为当前窗口的一半,然后开始执行拥塞避免算法;
  • 将快恢复开始时的拥塞窗口值再增大一些,即等于新的ssthresh+3,这样做有几个原因:
    • 既然发送方收到三个重复的ACK报文,就表明有三个数据报文段已经离开了网络;
    • 这三个报文段不再消耗网络资源而是停留在接收方的接收缓存中;
    • 可见现在网络中不是堆积了报文而是减少了三个报文段,因此拥塞窗口可以适当再扩大些。

5、经受时延的确认

TCP为了减少TCP数据单元的交互,增强效率。在收到TCP连接远程一端的数据报时,不会马上发送确认报文,而是等待系统的定时器,在定时器一个200ms周期内,如果本地端有要发送的数据,则连同确认报文一起发送出去,如果没有的话在200ms的触发时机才会发送单独的确认报文。这种现象也被称为数据捎带确认,它相当于TCP的一个功能,可开启也可被关闭,如前文介绍的快重传算法就没有启动该功能。

6、Nagle算法

Nagle算法为了避免发送小的数据包,规定同一个时间段内只能有一个未被确认的数据分组发送出去,在一个数据包发送出去还没收到确认报文之前,所有待发送的数据报都要在缓存区等待,等上一个数据报收到确认后,再将缓冲区内积累的所有数据当做一个数据分组发送出去(前提是不能超过MSS)。这样能提高端对端的数据传输效率,假设发送端每次发送的数据只有一个字节,如果没有启用Nagle算法的话,每次发送的一个字节就作为一个数据包发送出去,每个数据包就会有41字节(20字节的IP首部和20字节的TCP首部,加上一个字节的业务数据),这样负荷数据就增加了40倍。

Nagle算法虽然能提高发送效率,因为其他数据包都要等到确认到达时才能发送出去,这对于经常发送细小数据包的应用层业务很有好处的,比如Rlogin这种命令式交互的应用,而对于数据实时性要求比较高的业务,如射击类游戏这显示是不合适的,因为Nagle算法也增加了延迟,这时就需要禁用Nagle算法来提供传输速率。Nagle算法在大部分系统是默认开启的,但也是可以关闭的,如Linux提供了TCP_NODELAY的选项来禁用Nagle算法。

 


 

六、TCP超时重传

在拥塞控制的介绍中,已经了解到一个TCP报文发送出去后,如果在一定时间内没有收到确认报文段,就会导致发送端超时重传。那这个超时时间是怎么定的呢?其实计算TCP超时重传时间是个很复杂的过程,我们先通过一个图了解一下RTT往返时间和超时时间的关系:

这里,RTT是指TCP数据报文的从发送数据报到接收响应报文的往返时间,RTO是指TCP报文超时重传时间。如上图所示,当网络不出现拥塞时,TCP报文的往返时间为RTT,假如超时重传时间RTO定得远小于RTT,就会出现不必要的重传,而如果将RTO定得远大于RTT,当出现拥塞时就会等待过长时间才重发报文,降低效率。因此,RTO就应该设置得比RTT大一点才更合理。但是,网络环境很复杂,在一个TCP连接中,可能上一个报文段的RTT小一些,下一个报文段就会因为网络问题而变得很大,那怎么通过RTT来计算RTO的值?

通过上面的分析,显然不能直接使用某一次的RTT来计算超时重传时间RTO。因此,TCP利用每次测量得到的RTT样本,来计算加权平均往返时间RTTs(又称平滑往返时间),再RTTs来计算RTO,它们计算公式如下:

RTTs1 = RTT1 (第一个报文段的RTT作为RTTs)

RTTs = (1 - α) × 旧的RTTs + α × 新的RTT样本 (RFC6298标准推荐α值为0.125)

RTO = RTTs + 4 × RTTd

这里我们看到了一个新的变量RTTd,这个叫做RTT偏差的加权平均,计算公式如下;

RTTd1 = RTT1 ÷ 2 (第一个报文段的RTTd值)

新的RTTd = (1 - β) × 旧的RTTd + β × (RTTs - 新的RTT样本) (RFC6298标准推荐β值为0.25)

Karn算法

由于网络环境的复杂性,当主机A在时间T1将TCP报文段发送出去后由于网络拥塞导致超时,因此在时间T2重发,之后在时间T3收到了响应报文,那这时RTT的值T3-T2还是T3-T1呢?这时会有两种情况,如果第一次报文段在发送过程中丢失了,而第二次重发后正常到达主机B并收到响应报文,因此RTT=T3-T2,第二种情况是第一次报文段正常到达主机B了,并发回了响应报文,但响应报文发回主机A时发生延迟了,等第二次重发报文后发送端才收到此确认报文,这时RTT就应该为T3-T1。但客户端主机A这种情况是无法得知的。

因此,针对出现超时重传时无法推测往返时间RTT的问题,Karn提出了一个算法:在计算加权平均往返时间RTTs时,只要报文段重传了,就不采用其往返时间RTT样本。也就是出现重传时,不重新计算RTTs,进行RTO也不会重新计算

但这又引发了另一个问题,假设网络突然拥塞,一个报文段的往返时间RTT突然增大很多,这就导致了该报文段超时重传,之后的很多报文段都是保持这样大的RTT,于是都超时重传了。但根据Karn算法,超时重传的报文不计入更新RTO,就会导致RTO比实际情况偏小,大量的报文段都反复被重发。因此,Karn算法有了更新,方法是:报文段每重传一次,就把超时重传时间RTO增大一些,普遍的做法就将RTO取值为旧RTO的2倍


七、常见面试题

常见面试题1: 为什么TCP连接的时候是3次?2次不可以吗?

答:网上大多数资料对这个问题的回答只有简单的一句:防止已过期的连接请求报文突然又传送到服务器,因而产生错误,这既不够全面也不够具体。下面给出比较详细而全面的回答:

  1. 防止已过期的连接请求报文突然又传送到服务器,因而产生错误

    在双方两次握手即可建立连接的情况下,假设客户端发送 A 报文段请求建立连接,由于网络原因造成 A 暂时无法到达服务器,服务器接收不到请求报文段就不会返回确认报文段,客户端在长时间得不到应答的情况下重新发送请求报文段 B,这次 B 顺利到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,客户端在收到 确认报文后也进入 ESTABLISHED 状态,双方建立连接并传输数据,之后正常断开连接。此时姗姗来迟的 A 报文段才到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,但是已经进入 CLOSED 状态的客户端无法再接受确认报文段,更无法进入 ESTABLISHED 状态,这将导致服务器长时间单方面等待,造成资源浪费。

  2. 三次握手才能让双方均确认自己和对方的发送和接收能力都正常

    第一次握手:客户端只是发送处请求报文段,什么都无法确认,而服务器可以确认自己的接收能力和对方的发送能力正常;

    第二次握手:客户端可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;

    第三次握手:服务器可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;

    可见三次握手才能让双方都确认自己和对方的发送和接收能力全部正常,这样就可以愉快地进行通信了。

  3. 告知对方自己的初始序号值,并确认收到对方的初始序号值

    TCP 实现了可靠的数据传输,原因之一就是 TCP 报文段中维护了序号字段和确认序号字段,也就是图中的 seq 和 ack,通过这两个字段双方都可以知道在自己发出的数据中,哪些是已经被对方确认接收的。这两个字段的值会在初始序号值得基础递增,如果是两次握手,只有发起方的初始序号可以得到确认,而另一方的初始序号则得不到确认。

常见面试题2: TCP 建立连接为什么要三次握手而不是四次?

答:相比上个问题而言,这个问题就简单多了。因为三次握手已经可以确认双方的发送接收能力正常,双方都知道彼此已经准备好,而且也可以完成对双方初始序号值得确认,也就无需再第四次握手了。

常见面试题3: 有一种网络攻击是利用了 TCP 建立连接机制的漏洞,你了解吗?这个问题怎么解决?

答:在三次握手过程中,服务器在收到了客户端的 SYN 报文段后,会分配并初始化连接变量和缓存,并向客户端发送 SYN + ACK 报文段,这相当于是打开了一个“半开连接 (half-open connection)”,会消耗服务器资源。如果客户端正常返回了 ACK 报文段,那么双方可以正常建立连接,否则,服务器在等待一分钟后会终止这个“半开连接”并回收资源。这样的机制为 SYN洪泛攻击 (SYN flood attack)提供了机会,这是一种经典的 DoS攻击 (Denial of Service,拒绝服务攻击),所谓的拒绝服务攻击就是通过进行攻击,使受害主机或网络不能提供良好的服务,从而间接达到攻击的目的。在 SYN 洪泛攻击中,攻击者发送大量的 SYN 报文段到服务器请求建立连接,但是却不进行第三次握手,这会导致服务器打开大量的半开连接,消耗大量的资源,最终无法进行正常的服务。

解决方法:SYN Cookies,现在大多数主流操作系统都有这种防御系统。SYN Cookies 是对 TCP 服务器端的三次握手做一些修改,专门用来防范 SYN 洪泛攻击的一种手段。它的原理是,在服务器接收到 SYN 报文段并返回 SYN + ACK 报文段时,不再打开一个半开连接,也不分配资源,而是根据这个 SYN 报文段的重要信息 (包括源和目的 IP 地址,端口号可一个秘密数),利用特定散列函数计算出一个 cookie 值。这个 cookie 作为将要返回的SYN + ACK 报文段的初始序列号(ISN)。当客户端返回一个 ACK 报文段时,服务器根据首部字段信息计算 cookie,与返回的确认序号(初始序列号 + 1)进行对比,如果相同,则是一个正常连接,然后分配资源并建立连接,否则拒绝建立连接。

常见面试题4: 为什么 TCP 关闭连接为什么要四次而不是三次?

答:服务器在收到客户端的 FIN 报文段后,可能还有一些数据要传输,所以不能马上关闭连接,但是会做出应答,返回 ACK 报文段,接下来可能会继续发送数据,在数据发送完后,服务器会向客户单发送 FIN 报文,表示数据已经发送完毕,请求关闭连接,然后客户端再做出应答,因此一共需要四次挥手。

 

常见面试题5: 客户端为什么需要在 TIME-WAIT 状态等待 2MSL 时间才能进入 CLOSED 状态?

答:按照常理,在网络正常的情况下,四个报文段发送完后,双方就可以关闭连接进入 CLOSED 状态了,但是网络并不总是可靠的,如果客户端发送的 ACK 报文段丢失,服务器在接收不到 ACK 的情况下会一直重发 FIN 报文段,这显然不是我们想要的。因此客户端为了确保服务器收到了 ACK,会设置一个定时器,并在 TIME-WAIT 状态等待 2MSL 的时间,如果在此期间又收到了来自服务器的 FIN 报文段,那么客户端会重新设置计时器并再次等待 2MSL 的时间,如果在这段时间内没有收到来自服务器的 FIN 报文,那就说明服务器已经成功收到了 ACK 报文,此时客户端就可以进入 CLOSED 状态了。

TIME_WAIT至少需要持续2MSL时长,这2个MSL中的第一个MSL是为了等自己发出去的最后一个ACK从网络中消失,而第二MSL是为了等在对端收到ACK之前的一刹那可能重传的FIN报文从网络中消失。

MSL:报文最长存活寿命


原文来自

tcp - 面试准备 TCP 知识,看这一篇就够了_个人文章 - SegmentFault 思否

一文解析TCP协议所有知识点 - 知乎

网络编程:TCP的三次握手与四次挥手 - 哔哩哔哩

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

西皮呦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值