23

前言

TCP(Transmission Control Protocol,传输控制协议) 是计算机网络的的重要组成部分,也是网络编程的重要内容,还有我们平时接触最多的 HTTP 也是基于 TCP 实现的。TCP 可以说是最重要的传输层协议,既然如此,作为开发人员,就有必要把 TCP 的核心概念和原理搞清楚。除此之外,诸如三次握手、四次挥手、滑动窗口和拥塞控制这些概念更是高频面试题,这就更有理由深入学习一下 TCP 了,本文就为大家详细梳理一下 TCP 的核心概念和原理。注:由于本文图片较多,标注有 by HYN 的图片为作者自制,其它来自于网络或参考资料,侵删

一、TCP 简介

第一部分先为大家介绍一下 TCP 的主要概念,并讲解一下 TCP 的三个重要特性——1. 面向连接;2. 基于字节流;3. 可靠性。

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

网络分层模型.png

网络分层模型

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

1.1 面向连接

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

1.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 在整个过程中所扮演的角色。

TCP 在网络数据传输中扮演的角色.png

TCP 在网络数据传输中的位置和角色

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

报文段简介

上面多次提到了报文段的概念,其结构非常重要,后面的连接过程和拥塞控制等内容也要用到相关概念,先在这里介绍一下。

TCP 报文段结构.png

TCP 报文段结构

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

  • 端口号 (Source Port and Destination Port):每个 TCP 报文段都包含源端和目的端的端口号,用于寻找发送端和接收端应用进程。这两个值加上 IP 首部中的源端 IP 地址和目的端 IP 地址就可以确定一个唯一的 TCP 连接。
  • 序号 (Sequence Number):这个字段的主要作用是用于将失序的数据重新排列。TCP 会隐式地对字节流中的每个字节进行编号,而 TCP 报文段的序号被设置为其数据部分的第一个字节的编号。序号是 32 bit 的无符号数,取值范围是0到 232 - 1。
  • 确认序号 (Acknowledgment Number):接收方在接受到数据后,会回复确认报文,其中包含确认序号,作用就是告诉发送方自己接收到了哪些数据,下一次数据从哪里开始发,因此,确认序号应当是上次已成功收到数据字节序号加 1。只有 ACK 标志为 1 时确认序号字段才有效。
  • 首部长度 (Header Length):首部中的选项部分的长度是可变的,因此首部的长度也是可变的,所以需要这个字段来明确表示首部的长度,这个字段占 4 bit,4 位的二进制数最大可以表示 15,而首部长度是以 4 个字节为一个单位的,因此首部最大长度是 15 * 4 = 60 字节。
  • 保留字段 (Reserved):占 6 位,未来可能有具体用途,目前默认值为0.
  • 控制位 (Control Bits):在三次握手和四次挥手中会经常看到 SYN、ACK 和 FIN 的身影,一共有 6 个标志位,它们表示的意义如下:

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

1.3 可靠性

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

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

上面为大家介绍了 TCP 最重要的三个特点,在本文第一部分的最后,再来看看 TCP 和 UDP 的对比吧。

1.4 TCP 和 UDP 的区别

UDP TCP
是否连接 无连接 面向连接
是否可靠 不可靠,没有确认机制、流量控制和拥塞控制 可靠,有确认机制、流量控制和拥塞控制
连接对象个数 支持一对一,一对多,多对一和多对多交互通信 只支持一对一通信
传输方式 面向报文 面向字节流
首部开销 首部开销小,固定8字节 首部开销较大,最小20字节,最大60字节
适用场景 适用于实时应用(IP电话、视频会议、直播等) 适用于要求可靠传输的应用,如文件传输等

二、TCP 的连接控制

2.1 建立连接

2.1.1 三次握手

这个问题简直太经典了,如果你在面试中只被问到了一个关于 TCP 的问题,那大概率就是关于三次握手的问题。TCP 的重要特性之一就是面向连接,连接双方在发送数据之前必须经历握手的阶段,那具体的过程是怎样的呢?先来看图,大家最好可以动手简单画画这个图,当然还有后文四次挥手的图,帮助加深记忆。

三次握手过程.png

三次握手过程

如图所示,双方之间的三个蓝色箭头就表示了三次握手过程中所发生的数据交换:

  1. 第一次握手:客户端向服务器发送报文段1,其中的 SYN 标志位 (前文已经介绍过各种标志位的作用)的值为 1,表示这是一个用于请求发起连接的报文段,其中的序号字段 (Sequence Number,图中简写为seq)被设置为初始序号x (Initial Sequence Number,ISN),TCP 连接双方均可随机选择初始序号。发送完报文段1之后,客户端进入 SYN-SENT 状态,等待服务器的确认。
  2. 第二次握手:服务器在收到客户端的连接请求后,向客户端发送报文段2作为应答,其中 ACK 标志位设置为 1,表示对客户端做出应答,其确认序号字段 (Acknowledgment Number,图中简写为小写 ack) 生效,该字段值为 x + 1,也就是从客户端收到的报文段的序号加一,代表服务器期望下次收到客户端的数据的序号。此外,报文段2的 SYN 标志位也设置为1,代表这同时也是一个用于发起连接的报文段,序号 seq 设置为服务器初始序号y。发送完报文段2后,服务器进入 SYN-RECEIVED 状态。
  3. 第三次握手:客户端在收到报文段2后,向服务器发送报文段3,其 ACK 标志位为1,代表对服务器做出应答,确认序号字段 ack 为 y + 1,序号字段 seq 为 x + 1。此报文段发送完毕后,双方都进入 ESTABLISHED 状态,表示连接已建立。

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

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

  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)进行对比,如果相同,则是一个正常连接,然后分配资源并建立连接,否则拒绝建立连接。

2.2.2 同时打开

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

同时打开.png

同时打开的过程

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

2.2 关闭连接

2.2.1 四次挥手

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

四次挥手过程.png

四次挥手过程

四次挥手详细过程如下:

  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 状态。

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

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

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

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

2.2.2 同时关闭

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

同时关闭过程.png

同时关闭过程

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

三、TCP 的流量控制与滑动窗口

3.1 什么是流量控制?

TCP 连接双方的主机都为该连接设置了发送缓存和接收缓存,这些缓存起到了蓄水池的作用,我们肯定不能把上层应用程序发来的数据一股脑儿发送到网络中,而是利用发送缓存将其缓存起来,然后再按一定的速率通过网络发送给对方,而接收缓存的作用是把对方传来的数据先缓存起来,等到己方应用程序有空的时候再来取走数据。示意图如下:

TCP 缓存.png

TCP 缓存示意图

在此过程中,如果接收方应用程序读取数据的速度小于发送方的数据发送速度,将导致接收方的接收缓存溢出,造成数据丢失,这显然不是我们想看到的。因此 TCP 为应用程序提供了流量控制服务 (flow-control service),以消除发送方使接收方的接收缓存溢出的可能性。简单来说流量控制的目的就是协调发送方的数据发送速度,使其与接收方的数据处理速度相匹配,避免数据丢失,那么如何实现流量控制呢?

3.2 早期的流量控制模式——停止-等待模式 (stop-wait)

顾名思义就是发送方在发送一个数据包后就停止发送,等待对方响应 ACK,然后才能继续发送数据。这种模式的具体实现为 Positive Acknowledgment With Retransmission (PAR),意为带重传的肯定确认协议,其实现方式如下图所示:

PAR 示意图.png

PAR 示意图

这种实现很简单,发送方在发送数据包 (图中的msg)时会设置一个计时器,然后等待接收方的 ACK,接收方在收到数据后会返回 ACK 作为应答,发送方在收到 ACK 后会发送下一个数据包。如果由于网络原因造成数据包或者 ACK 丢失时,计时器会超时,然后发送方会重新发送未被确认的数据包。可以看到,这种模式虽然可以确保数据传输的可靠性,但是有个致命的缺点,那就是效率太低?如果是你,你会怎么对这个方案进行优化呢?

既然每次发送只一个数据包效率太低,那就多发送几个,然后给这些数据包编上号,接收端必须对每一个包进行确认,这样设备 A 一次多发送几个片段,而不必等候 ACK,同时接收端也要告知它能够收多少,这样发送端发起来也有个限制,当然还需要保证顺序性,不要乱序,对于乱序的状况,我们可以允许等待一定情况下的乱序,比如说先缓存提前到的数据,然后去等待需要的数据,如果一定时间没来就丢掉乱序的数据,来保证顺序性,这样的话,数据传输效率就可以大大提高。不过 TCP 也没有采用这种方案,而是在此基础上实现更加复杂的滑动窗口。

3.3 滑动窗口

首先给大家推荐一个视频,讲得很不错 https://www.bilibili.com/vide...

我们可以把发送方的发送缓存中的字节分为以下四类,每个编号对应一个字节:

发送缓存字节 分类.png

发送缓存中的字节分类
  1. 第一类:已发送且已确认,这些数据已经发送成功并已经被确认的数据,比如图中的前31个bytes,这些数据其实的位置是在窗口之外了,下一步将被移出发送缓存。窗口内顺序最低的字节被确认之后,窗口左边界会向右移动,称为窗口合拢。
  2. 第二类:已发送但未收到确认,这部分数据已经被发送出去,但是还没有收到接收端的 ACK,认为并没有完成发送,这部分数据属于窗口内的数据。
  3. 第三类:未发送但是接收方已经准备好接收,这部分是尽快发送的数据,这部分数据已经被加载到缓存中,也在发送窗口中,正在等待发送,其实这个窗口是完全有接收方告知的,接收方告知当前可以接受这些数据,所以发送方需要尽快的发送。
  4. 第四类:未发送且接收方未准备好接收,这些数据属于未发送,同时接收端也不允许发送的,因为这些数据已经超出了发送端所接收的范围。
3.3.1 发送窗口和接收窗口

发送窗口.png

发送窗口

发送窗口:图中的黑色框就是发送方的发送窗口,其大小由两个因素决定:1、接收方的提供的窗口大小 (TCP 报文段首部中的 window 字段),发送方在三次握手阶段首次得到这个值,之后的通信过程中接收方会根据自己的可用缓存对这个值进行动态调整;2、发送方会根据网络情况维护一个拥塞窗口变量 (后文介绍)。发送窗口的大小取这两个值的最小值。对于发送方来说,发送窗口分为两部分,分别是已经发送的部分(已经发送了,但是没有收到ACK)和可用窗口,接收端允许发送但是没有发送的那部分称为可用窗口。

接收窗口:对于接收端也是有一个接收窗口的,类似发送端,接收端的数据有3个分类,因为接收端并不需要等待ACK所以它没有类似的接收并确认了的分类,情况如下

  1. Received and ACK Not Send to Process:这部分数据属于接收了数据但是还没有被上层的应用程序接收;
  2. Received Not ACK: 已经接收,但是还没有回复 ACK;
  3. Not Received:有空位,还没有被接收的数据。
3.3.2 滑动窗口是如何滑动的?

滑动窗口流程.png

滑动窗口的滑动过程

累积确认概念:TCP 并不是每一个报文段都会回复一个 ACK ,可能会对两个报文段发送一个ACK,也可能会对多个报文段发送 1 个 ACK,这称为累积确认。比如说发送方有 1/2/3 3 个报文段,先发送了2,3 两个报文段,但是接收方期望收到1报文段,这个时候 2/3 报文段就只能放在缓存中等待报文1的空洞被填上,如果报文段1一直不来,报文2/3也将被丢弃,如果报文1来了,那么会发送一个 ACK 对第3个报文段进行确认,就代表对这三个报文段全部进行了确认。

下面举例说明一下窗口滑动的过程:

  1. 在握手过程中,接收方通告的窗口大小为20字节,所以发送方将发送窗口大小设置为20字节。
  2. 从图中的"上一个发送窗口的位置"(灰色虚线框)说起, 32-51号字节恰好处于发送窗口中,恰好20个字节,假设 TCP 将其分为 4 个报文段进行发送,每个报文段 5 个字节数据,分别记为 seg1 32-36, seg2 37-41, seg3 42-46, seg4 47-51。
  3. TCP 将有序发送 seg1、seg2、seg3和seg4四个报文段,如果这四个报文段都顺利到达接收方 (图中并不是这样),接收方将发回一个累积确认的 ACK 报文段,其中 ack = 52,代表希望收到下一个报文段的起始字节编号,报文段中也会继续通告窗口大小,如果还是20字节,那么发送方的窗口将整体向右移动20字节,如果通告的窗口值变小,比如变成15,那么发送窗口左边界移动20字节,右边界移动15字节。
  4. 如果在发送过程中 seg2 报文段丢失,而其他三个报文段正常到达接收方,那么接收方会先接受这三个报文段,然后返回 ACK 报文段,ack = 37,表示希望收到的下一个报文段的起始字节号为37,也就是seg2报文段。如果通告窗口值未发生变化,发送方在收到 ACK 后会将窗口整体右移5个字节,也就变成了图中的位置。
  5. 由于 seg2 还未收到 ACK,当重传计时器超时后,发送方会重新发送 seg2,此时52-56号字节又落到了发送窗口中,TCP 将其封装成 报文段进行发送,如果接收方全部顺利收到,会返回一个累积确认的 ACK,ack = 57,表示希望收到的下一个报文段的起始字节号为57。

接下来就是重复上述过程,直到 TCP 字节流的所有数据发送完毕。在这个过程中,接收方会根据自己接收缓存的剩余空间动态调整窗口值,对发送方进行流量控制。文字描述可能不够直观,大家可以参考上文推荐的视频。另外推荐一个动图演示的网站 动画地址,可以观看滑动窗口的动态效果,如下图 (此演示未考虑丢包的情况):

滑动窗口动画效果

四、TCP 的拥塞控制

4.1 什么是拥塞控制?

当数据从一个大的管道 (比如一个快速局域网)向一个较小的管道 (比如较慢的广域网)发送的时候就会发生拥塞,还有一种情况就是当多个输入流到达一个路由器,而路由器的输出流小于这些输入流的总和时,也会发生拥塞。举个例子就好理解了,第一种情况就好像源源不断的车流从八车道进入四车道,如果不进行控制,必然造成道路拥堵;第二种情况类似于很多车辆汇入十字路口,如果进的速度大于出的速度,再不加以控制,必然也会造成拥堵。于是 TCP 提供了响应的机制来应对这种情况,也就是 TCP 的拥塞控制。

4.2 如何实现拥塞控制?

TCP 一共使用了四种算法来实现拥塞控制:1、慢开始 (slow-start);2、拥塞避免 (congestion avoidance);3、快速重传 (fast retransmit);4、快速恢复 (fast recovery)。

这里先介绍一下拥塞窗口 (congestion window,简写为 cwnd)的概念:拥塞窗口是由发送方根据网络状况维护的一个变量,用于控制自己的数据发送速率。前文提到了发送方的发送窗口受两个变量约束,一是接收方通告的窗口大小值,二就是发送方自身的拥塞窗口,实际的发送窗口大小取二者最小值。

4.2.1 慢开始(慢启动)

在早期的 TCP Tahoe 版本中,只用到了慢开始和拥塞避免算法,如图所示:

慢开始和拥塞避免.png

慢开始和拥塞避免

如图所示,在刚开始,TCP 采用慢开始算法。慢开始不是指拥塞窗口的增长速度慢(增长速度是指数增长,非常快),而是指 TCP 开始发送设置 cwnd=1。思路就是不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大 逐渐增加拥塞窗口的大小。这里用报文段的个数的拥塞窗口大小举例说明慢启动算法,实时拥塞窗口大小是以字节为单位的。为了防止 cwnd 增长过大引起网络拥塞,设置一个慢开始门限(slow start threshold,简写为 ssthresh),初始值为16 ,

当cnwd < ssthresh,使用慢开始算法
当 cnwd = ssthresh,既可使用慢开始算法,也可以使用拥塞避免算法
当 cnwd > ssthresh,使用拥塞避免算法

4.2.2 拥塞避免

当拥塞窗口大小达到初始 ssthresh 值时,转而采用拥塞避免算法。拥塞避免并非完全能够避免拥塞,是说在拥塞避免阶段将拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞,思路:让拥塞窗口 cwnd 缓慢地增大,即每经过一个往返时间 RTT 就把发送方的拥塞窗口加一。无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。然后把拥塞窗口设置为 1,执行慢开始算法。

4.2.3 快速重传

TCP Reno.png

TCP Reno 应用了四种算法

有时候的发送方未收到某个报文段的确认也并一定就说明一定是出现了网络拥塞,也可能是其他原因,所以直接执行慢开始算法会影响整体效率,后来的 TCP Reno 版本解决了这一问题,那就是采用快速重传和快速恢复算法。

快速重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方),而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。由于不需要等待设置的重传计时器到期,能尽早重传未被确认的报文段,能提高整个网络的吞吐量。

4.2.4 快速恢复

当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把 ssthresh 门限减半。 但是接下去并不执行慢开始算法。考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将 cwnd 设置为 ssthresh 的大小, 然后执行拥塞避免算法。

五、TCP 粘包与拆包

5.1 TCP 粘包和拆包的原因

我们知道 TCP 是以字节流的方式传输数据,传输的最小单位为一个报文段(segment)。TCP 首部 中有个选项 (Options)的字段,常见的选项为 MSS (Maximum Segment Size最大消息长度),它是收发双方协商通信时每一个报文段所能承载的最大有效数据的长度。数据链路层每次传输的数据有个最大限制MTU (Maximum Transmission Unit),一般是1500字节,超过这个量要分成多个报文段,MSS 则是这个最大限制减去 TCP 的首部,光是要传输的数据的大小,一般为1460字节。
MSS = MTU - Header

TCP 为提高性能,发送端会将需要发送的数据发送到发送缓存,等待缓存满了之后,再将缓存中的数据发送到接收方。同理,接收方也有接收缓存这样的机制,来接收数据。

上面这些是发生 TCP 粘包和拆包的前提,下面是具体的原因:

  1. 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
  2. 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
  3. 应用程序写入数据小于剩余缓存大小,网卡将应用多次写入的数据先缓存起来,然后一起发送到网络上,这将会发生粘包。
  4. 接收数据端的应用层没有及时读取接收缓存中的数据,将发生粘包。

5.2 TCP 粘包和拆包的解决方案

  1. 设置定长消息,服务端每次读取既定长度的内容作为一条完整消息。
  2. 设置消息边界,数据结尾尾增加特殊字符分割。
  3. 使用带消息头的协议,消息头存储消息开始标识及消息长度信息,接收方获取消息头的时候解析出消息长度,然后向后读取该长度的内容。

六、结语

本文为大家梳理了 TCP 的核心概念和原理,也分享了一些高频面试题,希望对你有帮助。尽管文章字数超过了一万字,但是 TCP 的很多细节还远远没有介绍到,如果想进一步了解,可以参考一下下面参考资料中提到的几本书。

参考资料:

  1. 《TCP/IP详解卷一:协议》
  2. 《计算机网络自顶向下方法》
  3. 《TCP/IP Guide》
  4. TCP-IP详解:滑动窗口(Sliding Window)

IamHYN
1.6k 声望1k 粉丝

Java开发工程师,立志于输出干货...