Skip to content
文章目录

概述

计算机网络体系结构中的物理层、数据链路层以及网络层它们共同解决了将主机通过异构网络互联起来所面临的问题,实现了主机与主机的通信。

但实际上在计算机网络中进行通信的真正实体是位于通信两端主机中的进程。 如何为运行在不同主机上的应用进程提供直接的通信服务是运输层的任务,运输层协议又称为端到端的协议。

运输层向高层用户屏蔽了下面网络核心的细节,它使应用进程看见的就好像是在两个运输层实体之间有一条端到端的逻辑通信信道。

TCP 的全称是 Transmission Control Protocol ,它被称为是⼀种 ⾯向连接(connection-oriented) 的协议,这是因为⼀个应⽤程序开始向另⼀个应⽤程序发送数据之前,这两个进程必须先进⾏ 握⼿ ,握⼿是⼀个逻辑连接,并不是两个主机之间进⾏真实的握⼿。

TCP 基本认识

什么是 TCP ?

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。 计算机网络.png

  • 面向连接:一定是「一对一」才能连接,不能像 UDP 协议 可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
  • 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
  • 字节流:消息是「没有边界」的,所以无论我们消息有多大都可以进行传输。并且消息是「有序的」,当「前一个」消息没有收到的时候,即使它先收到了后面的字节已经收到,那么也不能扔给应用层去处理,同时对「重复」的报文会自动丢弃。

⼀旦主机 A 和主机 B 建⽴了连接,那么进⾏通信的应⽤程序只使⽤这个虚拟的通信线路发送和接收数据就可以保证数据的传输,TCP 协议负责控制连接的建⽴、断开、保持等⼯作。

TCP 连接是 全双⼯服务(full-duplex service) 的,全双⼯是什么意思?全双⼯指的是主机 A 与另外⼀个主机B 存在⼀条 TCP 连接,那么应⽤程数据就可以从主机 B 流向主机 A 的同时,也从主机 A 流向主机 B。

TCP 会将数据临时存储到连接的发送缓存(send buffer) 中,这个 send buffer 是三次握⼿之间设置的缓存之⼀,然后 TCP 在合适的时间将发送缓存中的数据发送到⽬标主机的接收缓存中,实际上,每⼀端都会有发送缓存和接收缓存,如下所示 计算机网络.png 主机之间的发送是以 报⽂段(segment) 进⾏的,那么什么是 Segement 呢?

TCP 会将要传输的数据流分为多个 块(chunk) ,然后向每个 chunk 中添加 TCP 标头,这样就形成了⼀个 TCP段也就是报⽂段。每⼀个报⽂段可以传输的⻓度是有限的,不能超过 **最⼤数据⻓度(Maximum Segment Size) ,俗称 MSS **。在报⽂段向下传输的过程中,会经过链路层,链路层有⼀个 Maximum Transmission Unit ,最⼤传输单元 MTU, 即数据链路层上所能通过最⼤数据包的⼤⼩,最⼤传输单元通常与通信接⼝有关。

那么 MSS 和 MTU 有啥区别呢?

因为计算机⽹络是分层考虑的,这个很重要,不同层的称呼不⼀样,对于传输层来说,称为报⽂段⽽对⽹络层来说就叫做 IP 数据包,所以,MTU 可以认为是⽹络层能够传输的最⼤ IP 数据包,⽽ MSS(Maximum segmentsize)可以认为是传输层的概念,也就是 TCP 数据包每次能够传输的最⼤量。

TCP报文段结构

我们先来看看 TCP 头的格式 计算机网络.png序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决不丢包的问题。控制位:

  • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。
  • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
  • SYC:该位为 1 时,表示希望建立连,并在其「序列号」的字段进行序列号初始值的设定。
  • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位置为 1 的 TCP 段。

TCP 的各种功能和特点都是通过 TCP 报⽂结构来体现的

什么是UDP?

UDP 不提供复杂的控制机制,利用 IP 提供面向「无连接」的通信服务。

UDP 的全称是⽤户数据报协议(UDP,User Datagram Protocol) ,UDP 为应⽤程序提供了⼀种 ⽆需建⽴连接就可以发送封装的 IP 数据包的⽅法。如果应⽤程序开发⼈员选择的是 UDP ⽽不是 TCP 的话,那么该应⽤程序相当于就是和 IP 直接打交道的

UDP 协议真的非常简,头部只有 8 个字节( 64 位),UDP 的头部格式如下: 计算机网络.png

  • 目标和源端口:主要是告诉 UDP 协议应该把报文发给哪个进程。
  • 包长度:该字段保存了 UDP 首部的长度跟数据的长度之和。
  • 校验和:校验和是为了提供可靠的 UDP 首部和数据而设计。

TCP 和 UDP 区别

  1. 连接
  • TCP 是面向连接的传输层协议,传输数据前先要建立连接。
  • UDP 是不需要连接,即刻传输数据。
  1. 服务对象
  • TCP 是一对一的两点服务,即一条连接只有两个端点。
  • UDP 支持一对一、一对多、多对多的交互通信
  1. 可靠性
  • TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。
  • UDP 是尽最大努力交付,不保证可靠交付数据。
  1. 拥塞控制、流量控制
  • TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
  • UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
  1. 首部开销
  • TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。
  • UDP 首部只有 8 个字节,并且是固定不变的,开销较小。

TCP 和 UDP 应用场景: 由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:

  • FTP 文件传输
  • HTTP / HTTPS

由于 UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:

  • 包总量较少的通信,如 DNS 、SNMP 等
  • 视频、音频等多媒体通信
  • 广播通信

TCP 三次握手和四次挥手

TCP 连接建立

TCP 三次握手过程和状态变迁

TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手而进行的。计算机网络.png

  • 一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态

计算机网络.png

  • 客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。

计算机网络.png

  • 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。

计算机网络.png

  • 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态。
  • 服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。

从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题。 一旦完成三次握手,双方都处于 ESTABLISHED 状态,此致连接就已建立完成,客户端和服务端就可以相互发送数据了。

为什么是三次握手?不是两次、四次?

相信大家比较常回答的是:“因为三次握手才能保证双方具有接收和发送的能力。” 这回答是没问题,但这回答是片面的,并没有说出主要的原因。

接下来以三个方面分析三次握手的原因:

  • 三次握手才可以阻止历史重复连接的初始化(主要原因)
  • 三次握手才可以同步双方的初始序列号
  • 三次握手才可以避免资源浪费

原因一:避免历史连接 简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。 网络环境是错综复杂的,往往并不是如我们期望的一样,先发送的数据包,就先到达目标主机,可能会由于网络拥堵等乱七八糟的原因,会使得旧的数据包,先到达目标主机,那么这种情况下 TCP 三次握手是如何避免的呢? 计算机网络.png 客户端连续发送多次 SYN 建立连接的报文,在网络拥堵等情况下:

  • 一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端;
  • 那么此时服务端就会回一个 SYN + ACK 报文给客户端;
  • 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送 RST 报文给服务端,表示中止这一次连接。

如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接:

  • 如果是历史连接(序列号过期或超时),则第三次握手发送的报文是 RST 报文,以此中止历史连接;
  • 如果不是历史连接,则第三次发送的报文是 ACK 报文,通信双方就会成功建立连接;

所以, TCP 使用三次握手建立连接的最主要原因是防止历史连接初始化了连接。

原因二:同步双方初始序列号 TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:

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

可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。计算机网络.png 四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。 而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。

原因三:避免资源浪费 如果只有「两次握手」,当客户端的 SYN 请求连接在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK 确认信号,所以每收到一个 SYN 就只能先主动建立一个连接,这会造成什么情况呢?

如果客户端的 SYN 阻塞了,重复发送多次 SYN 报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。计算机网络.png 即两次握手会造成消息滞留情况下,服务器重复接受无用的连接请求 SYN 报文,而造成重复分配资源。

TCP 连接断开

TCP 四次挥手过程和状态变迁

天下没有不散的宴席,对于 TCP 连接也是这样, TCP 断开连接是通过四次挥手方式。 双方都可以主动断开连接,断开连接后主机中的「资源」将被释放。 计算机网络.png

  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。
  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
  • 服务器收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。

你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。 这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。

为什么挥手需要四次?

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

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

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

为什么 TIME_WAIT 等待的时间是 2MSL?

MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 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。

2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时

在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。 其定义在 Linux 内核代码里的名称为 TCP_TIMEWAIT_LEN: #define TCP_TIMEWAIT_LEN (60HZ) / how long to wait to destroy TIME-WAIT state, about 60 seconds */ 如果要修改 TIME_WAIT 的时间长度,只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值,并重新编译 Linux 内核。

为什么需要 TIME_WAIT 状态?

主动发起关闭连接的一方,才会有 TIME-WAIT 状态。 需要 TIME-WAIT 状态,主要是两个原因:

  • 防止具有相同「四元组」的「旧」数据包被收到;
  • 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭;

原因一:防止旧连接的数据包 假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢? 计算机网络.png

  • 如上图黄色框框服务端在关闭连接之前发送的 SEQ = 301 报文,被网络延迟了。
  • 这时有相同端口的 TCP 连接被复用后,被延迟的 SEQ = 301 抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题。

所以,TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。 原因二:保证连接正确关闭 TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。 假设 TIME-WAIT 没有等待时间或时间过短,断开连接会造成什么问题呢? 计算机网络.png

  • 如上图红色框框客户端四次挥手的最后一个 ACK 报文如果在网络中被丢失了,此时如果客户端 TIME-WAIT 过短或没有,则就直接进入了 CLOSE 状态了,那么服务端则会一直处在 LASE-ACK 状态。
  • 当客户端发起建立连接的 SYN 请求报文后,服务端会发送 RST 报文给客户端,连接建立的过程就会被终止。

如果 TIME-WAIT 等待足够长的情况就会遇到两种情况:

  • 服务端正常收到四次挥手的最后一个 ACK 报文,则服务端正常关闭连接。
  • 服务端没有收到四次挥手的最后一个 ACK 报文时,则会重发 FIN 关闭连接报文并等待新的 ACK 报文。

所以客户端在 TIME-WAIT 状态等待 2MSL 时间后,就可以保证双方的连接都可以正常的关闭。

TCP可靠性传输

相信大家都知道 TCP 是一个可靠传输的协议,那它是如何保证可靠的呢?

为了实现可靠性传输,需要考虑很多事情,例如数据的破坏、丢包、重复以及分片顺序混乱等问题。如不能解决这些问题,也就无从谈起可靠传输。

那么,TCP 是通过序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现可靠性传输的。 今天,将重点介绍 TCP 的重传机制、滑动窗口、流量控制、拥塞控制。

流量控制

发送方不能无脑的发数据给接收方,要考虑接收方处理能力。

如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。

为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。 TCP 通过使⽤⼀个 接收窗⼝(receive window) 的变量来提供流量控制。接收窗⼝会给发送⽅⼀个指示到底还有多少可⽤的缓存空间。发送端会根据接收端的实际接受能⼒来控制发送的数据量。

接收端主机向发送端主机通知⾃⼰可以接收数据的⼤⼩,发送端会发送不超过这个限度的数据,这个⼤⼩限度就是窗⼝⼤⼩,还记得 TCP 的⾸部么,有⼀个接收窗⼝,我们上⾯聊的时候说这个字段⽤于流量控制。它⽤于指示接收⽅能够/愿意接收的字节数量。

发送端主机会定期发送⼀个 窗⼝探测包 ,这个包⽤于探测接收端主机是否还能够接受数据,当接收端的缓冲区⼀旦⾯临数据溢出的⻛险时,窗⼝⼤⼩的值也随之被设置为⼀个更⼩的值通知发送端,从⽽控制数据发送量。

下⾯是⼀个流量控制示意图 计算机网络.png 发送端主机根据接收端主机的窗⼝⼤⼩进⾏流量控制。由此也可以防⽌发送端主机⼀次发送过⼤数据导致接收端主机⽆法处理。

如上图所示,当主机 B 收到报⽂段 2000 - 2999 之后缓冲区已满,不得不暂时停⽌接收数据。然后主机 A 发送窗⼝探测包,窗⼝探测包⾮常⼩仅仅⼀个字节。然后主机 B 更新缓冲区接收窗⼝⼤⼩并发送窗⼝更新通知给主机A,然后主机 A 再继续发送报⽂段。

在上⾯的发送过程中,窗⼝更新通知可能会丢失,⼀旦丢失发送端就不会发送数据,所以窗⼝探测包会随机发送,以避免这种情况发⽣。

拥塞控制

前面的流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。 一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。 在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大…. 所以,TCP 不能忽略网络上发生的事,它被设计成一个无私的协议,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。 于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。 为了在「发送方」调节所要发送数据的量,定义了一个叫做「拥塞窗口」的概念。

什么是拥塞窗口?和发送窗口有什么关系呢? 拥塞窗口 cwnd是发送方维护的一个 的状态变量,它会根据网络的拥塞程度动态变化的。 我们在前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。 拥塞窗口 cwnd 变化的规则:

  • 只要网络中没有出现拥塞,cwnd 就会增大;
  • 但网络中出现了拥塞,cwnd 就减少;

那么怎么知道当前网络是否出现了拥塞呢? 其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了用拥塞。 在了解完 TCP 拥塞控制后,下⾯我们就该聊⼀下 TCP 的 拥塞控制算法(TCP congestion control algorithm) 了。TCP 拥塞控制算法主要包含三个部分: 慢启动、拥塞避免、快速恢复 ,下⾯我们依次来看⼀下

慢启动

当⼀条 TCP 开始建⽴连接时,cwnd 的值就会初始化为⼀个 MSS 的较⼩值。这就使得初始发送速率⼤概是 MSS/RTT 字节/秒 ,⽐如要传输 1000 字节的数据,RTT 为 200 ms ,那么得到的初始发送速率⼤概是 40 kb/s。实际情况下可⽤带宽要⽐这个 MSS/RTT ⼤得多,因此 TCP 想要找到最佳的发送速率,可以通过 慢启动(slow-start) 的⽅式,在慢启动的⽅式中,cwnd 的值会初始化为 1 个 MSS,并且每次传输报⽂确认后就会增加⼀个 MSS,cwnd 的值会变为 2 个 MSS,这两个报⽂段都传输成功后每个报⽂段 + 1,会变为 4 个 MSS,依此类推,每成功⼀次 cwnd 的值就会翻倍。如下图所示 计算机网络.png 发送速率不可能会⼀直增⻓,增⻓总有结束的时候,那么何时结束呢?慢启动通常会使⽤下⾯这⼏种⽅式结束发送速率的增⻓。

  • 如果在慢启动的发送过程出现丢包的情况,那么 TCP 会将发送⽅的 cwnd 设置为 1 并重新开始慢启动的过程,此时会引⼊⼀个 ssthresh(慢启动阈值) 的概念,它的初始值就是产⽣丢包的 cwnd 的值 / 2,即当检测到拥塞时,ssthresh 的值就是窗⼝值的⼀半。

  • 第⼆种⽅式是直接和 ssthresh 的值相关联,因为当检测到拥塞时,ssthresh 的值就是窗⼝值的⼀半,那么当cwnd > ssthresh 时,每次翻番都可能会出现丢包,所以最好的⽅式就是 cwnd 的值 = ssthresh ,这样 TCP就会转为拥塞控制模式,结束慢启动。

  • 慢启动结束的最后⼀种⽅式就是如果检测到 3 个冗余 ACK,TCP 就会执⾏⼀种快速重传并进⼊恢复状态。

拥塞避免

当 TCP 进⼊拥塞控制状态后,cwnd 的值就等于拥塞时值的⼀半,也就是 ssthresh 的值。所以,⽆法每次报⽂段到达后都将 cwnd 的值再翻倍。⽽是采⽤了⼀种相对 保守 的⽅式,每次传输完成后只将 cwnd 的值增加 ⼀个MSS ,⽐如收到了 10 个报⽂段的确认,但是 cwnd 的值只增加⼀个 MSS。这是⼀种线性增⻓模式,它也会有增⻓逾值,它的增⻓逾值和慢启动⼀样,如果出现丢包,那么 cwnd 的值就是⼀个 MSS,ssthresh 的值就等于cwnd 的⼀半;或者是收到 3 个冗余的 ACK 响应也能停⽌ MSS 增⻓。如果 TCP 将 cwnd 的值减半后,仍然会收到 3 个冗余 ACK,那么就会将 ssthresh 的值记录为 cwnd 值的⼀半,进⼊ 快速恢复 状态。

快速恢复

在快速恢复中,对于使 TCP 进⼊快速恢复状态缺失的报⽂段,对于每个收到的冗余 ACK,cwnd 的值都会增加⼀个 MSS 。当对丢失报⽂段的⼀个 ACK 到达时,TCP 在降低 cwnd 后进⼊拥塞避免状态。如果在拥塞控制状态后出现超时,那么就会迁移到慢启动状态,cwnd 的值被设置为 1 个 MSS,ssthresh 的值设置为 cwnd 的⼀半。

重传机制

TCP 实现可靠传输的方式之一,是通过序列号与确认应答。 在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。 计算机网络.png 但在错综复杂的网络,并不一定能如上图那么顺利能正常的数据传输,万一数据在传输过程中丢失了呢? 所以 TCP 针对数据包丢失的情况,会用重传机制解决。 接下来说说常见的重传机制:

  • 超时重传
  • 快速重传
  • SACK
  • D-SACK

超时重传

重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传。 TCP 会在以下两种情况发生超时重传:

  • 数据包丢失
  • 确认应答丢失

计算机网络.png

如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。 也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。 超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢? 于是就可以用「快速重传」机制来解决超时重发的时间等待。

快速重传

TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。 快速重传机制,是如何工作的呢?其实很简单,一图胜千言。 计算机网络.png 在上图,发送方发出了 1,2,3,4,5 份数据:

  • 第一份 Seq1 先送到了,于是就 Ack 回 2;
  • 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;
  • 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;
  • 发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。
  • 最后,接收到收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。

所以,快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。 快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。 比如对于上面的例子,是重传 Seq2 呢?还是重传 Seq2、Seq3、Seq4、Seq5 呢?因为发送端并不清楚这连续的三个 Ack 2 是谁传回来的。

根据 TCP 不同的实现,以上两种情况都是有可能的。可见,这是一把双刃剑。

为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 方法。

SACK 方法

还有一种实现重传机制的方式叫:SACK( Selective Acknowledgment 选择性确认)。 这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。 如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。 计算机网络.png 如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。

Duplicate SACK

Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。 下面举例两个栗子,来说明 D-SACK 的作用。

栗子一号:ACK 丢包计算机网络.png

  • 「接收方」发给「发送方」的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499)
  • 于是「接收方」发现数据是重复收到的,于是回了一个 SACK = 3000~3500,告诉「发送方」 3000~3500 的数据早已被接收了,因为 ACK 都到了 4000 了,已经意味着 4000 之前的所有数据都已收到,所以这个 SACK 就代表着 D-SACK。
  • 这样「发送方」就知道了,数据没有丢,是「接收方」的 ACK 确认报文丢了。

栗子二号:网络延时计算机网络.png

  • 数据包(1000~1499) 被网络延迟了,导致「发送方」没有收到 Ack 1500 的确认报文。
  • 而后面报文到达的三个相同的 ACK 确认报文,就触发了快速重传机制,但是在重传后,被延迟的数据包(1000~1499)又到了「接收方」;
  • 所以「接收方」回了一个 SACK=1000~1500,因为 ACK 已经到了 3000,所以这个 SACK 是 D-SACK,表示收到了重复的包。
  • 这样发送方就知道快速重传触发的原因不是发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延迟了。

可见,D-SACK 有这么几个好处:

  1. 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
  2. 可以知道是不是「发送方」的数据包被网络延迟了;
  3. 可以知道网络中是不是把「发送方」的数据包给复制了;

在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能(Linux 2.4 后默认打开)。 计算机网络.png