文章目录
- 🌈 一、TCP 协议介绍
- ⭐ 1. TCP 协议的特点
- 🌈 二、TCP 协议格式
- ⭐ 1. TCP 报头中各字段的含义
- ⭐ 2. 各 TCP 标志位的用途
- ⭐ 3. 使用结构体描述 TCP 报头
- 🌈 三、TCP 的窗口
- ⭐ 1. TCP 的发送和接收缓冲区
- ⭐ 2. TCP 为什么存在缓冲区
- ⭐ 3. TCP 的缓冲区大小
- 🌈 四、TCP 保证可靠性的机制
- ⭐ 1. 确认应答
- 🌙 1.1 TCP 的通信模式
- 🌙 1.2 使用序号和确认序号唯一标识消息和应答
- ⭐ 2. 超时重传
- 🌙 2.1 TCP 的去重功能
- 🌙 2.2 超时重传的等待时间
- ⭐ 3. 连接管理
- 🌙 3.1 操作系统如何实现对连接的管理
- 🌙 3.2 TCP 通过三次握手建立连接
- 🌙 3.3 TCP 通过四次挥手断开连接
- ⭐ 4. 流量控制
- 🌙 4.1 接收端应将自己接收数据的能力告知发送端
- 🌙 4.2 设置 PSH 标志位让对端尽快处理数据
- 🌙 4.3 第一次发数据时如何得知对方的窗口大小
- ⭐ 5. 滑动窗口
- 🌙 5.1 滑动窗口的功能
- 🌙 5.2 滑动窗口的大小变化
- 🌙 5.3 滑动窗口通过双指针实现
- 🌙 5.4 滑动窗口如何解决丢包
- 🌙 5.5 如何防止滑动窗口滑出缓冲区
- ⭐ 6. 拥塞控制
- 🌙 6.1 使用拥塞控制解决网络拥堵
- 🌙 6.2 使用拥塞窗口实现拥塞控制
- 🌙 6.3 如何调整拥塞窗口的大小
- ⭐ 7. 延迟应答
- 🌙 7.1 延迟应答的策略
- ⭐ 8. 捎带应答
- 🌈 五、面向字节流
- ⭐ 1. TCP 以字节为单位发送和接收数据
- ⭐ 2. TCP 程序的读和写不需要逐个对应
- 🌈 六、粘包问题
- ⭐ 1. 什么是粘包问题
- ⭐ 2. 如何解决粘包问题
- ⭐ 3. UDP 不存在粘包问题
- 🌈 七、TCP 异常情况
- ⭐ 1. 进程异常终止
- ⭐ 2. 机器重启
- ⭐ 3. 机器掉电 / 网线断开
- 🌈 八、基于 TCP 实现的应用层协议
- 🌈 九、TCP 与 UDP
- ⭐ 1. TCP 与 UDP 的对比
- ⭐ 2. 如何用 UDP 实现可靠传输 (面试题)
🌈 一、TCP 协议介绍
- 传输控制协议 TCP (Transmission Control Protocol) 是互联网中使用最广泛的传输层协议,它用于对数据进行详细的控制。
- TCP 能占据如此重要的地位,其根本原因就在于它提供了详尽的可靠性保证。
- 基于 TCP 实现的上层应用有很多,诸如 HTTPS、HTTPS、FTP、SSH 等都是基于 TCP 协议实现的。
- 甚至 MySQL 的底层使用的也是 TCP 协议。
⭐ 1. TCP 协议的特点
- 面向连接:应用程序在使用 TCP 协议传送数据前,必须先建立 TCP 连接。在传送完数据后,还必须释放建立的 TCP 连接。
- 保证可靠性:通过 TCP 连接传送的数据所具备的特点::无差错、不丢失、不重复、按序到达。
- 全双工通信:通信双方的应用进程在任何时候都能发送数据。
- 面向字节流:传输的数据是以字节为单位的字节流序列。
🌈 二、TCP 协议格式
⭐ 1. TCP 报头中各字段的含义
- 16 位源端口号:发送方主机的进程端口号,用来标识数据从哪个进程来。
- 16 位目的端口号:目的主机的进程端口号,用来标识数据要到哪个进程去。
- 32 位 TCP 序号:标识本报文段所发送的数据的第一个字节的编号。
- 32 位 TCP 确认序号:接收方期望收到发送方下一个报文段的第一个字节数据的编号。
- 4 位 TCP 首部长度:单位是 4 字节,记录 TCP 头部有多少个 32 位 bit (4 字节)。所以 TCP 报头最大长度是 15 * 4 = 60 字节。
- 由于 TCP 选项最多占 40 个字节,需要能够表示 40 字节的 TCP 选项 + 20 字节的 TCP 报头的固定长度。
- 6 位保留:为 TCP 将来的发展所预留的空间,目前这 6 位全部都必须为 0。
- 6 位 TCP 标志位:用来区分 TCP 报文的类型。
- 16 位窗口大小:表示发送该 TCP 报文的发送端的接收窗口还能接收多少字节的数据流,该字段主要用于流量控制。
- 16 位检验和:用于确认传输的数据是否损坏。
- 16 位紧急指针:用于标识哪部分数据为紧急数据。
- 32 位 TCP 选项:长度不定 (但必须是 32 的整数倍,最多为 40 字节),内容可变,必须使用 TCP 首部的实际长度来区分 TCP 选项的具体长度。
- TCP 选项的具体长度 = TCP 报头的实际长度 - TCP 报头的固定长度 (20 字节)。
⭐ 2. 各 TCP 标志位的用途
URG
:用来记录紧急指针是否有效。ACK
:用来记录确认序号是否有效,只要该位为 1,则表示该 TCP 报文是应答报文。PSH
:用来提示接收端的应用程序立刻将 TCP 接收缓冲区中的数据读走。RST
:要求对方重新建立连接 (重新执行三次握手的过程);通常将携带 RST 标识的报文称为复位报文段。SYN
:请求与对方建立连接;通常将携带 SYN 标识的报文称为同步报文段。FIN
:请求与对方断开连接;通常将携带 FIN 标识的报文称为结束报文段。
⭐ 3. 使用结构体描述 TCP 报头
- 网络协议都是用 C 语言写的通信双方都能认识的结构体类型,TCP 协议自然也不例外。
// TCP 报文头部,总长度 20 字节
typedef struct _tcp_hdr
{unsigned short src_port; // 源端口号unsigned short dst_port; // 目的端口号unsigned int seq_no; // 序列号unsigned int ack_no; // 确认号
#if LITTLE_ENDIANunsigned char reserved_1 : 4; // 保留 6 位中的 4 位首部长度unsigned char thl : 4; // 记录 tcp 报文头部的长度unsigned char flag : 6; // 6 位的 TCP 标志位unsigned char reseverd_2 : 2; // 保留 6 位中的 2 位
#elseunsigned char thl : 4; // 记录 tcp 报文头部的长度unsigned char reserved_1 : 4; // 保留 6 位中的 4 位首部长度unsigned char reseverd_2 : 2; // 保留 6 位中的 2 位unsigned char flag : 6; // 6 位的 TCP 标志位
#endifunsigned short wnd_size; // 16 位窗口大小unsigned short chk_sum; // 16 位 TCP 检验和unsigned short urgt_p; // 16 位紧急指针
} tcp_hdr;
🌈 三、TCP 的窗口
⭐ 1. TCP 的发送和接收缓冲区
- TCP 的窗口采用缓冲区的形式实现。
- TCP 通信双方在进行通信时,本质上是将 TCP 发送方的发送窗口 (缓冲区) 中的数据拷贝到 TCP 接收方的接收窗口 (缓冲区) 。
- 发送缓冲区:用来暂时保存还未发送的数据。
- 接收缓冲区:用来暂时保存接收到的数据。
- 由应用层中的应用程序往 TCP 的发送缓冲区中写入数据。当上层应用调用
write 或 send
这样的系统调用接口时,实际上不是将数据直接发到网络中,而是向下交付给传输层 (将数据从应用层拷贝到 TCP 的发送缓冲区)。 - 由应用层中的应用程序从 TCP 的接收缓冲区中读取数据。当上层应用调用
read 或 recv
这样的系统调用接口时,实际上也不是直接从网络中读取数据,而是将数据从 TCP 的接收缓冲区中拷贝到应用层。
- 网络通信的本质:将发送方的发送缓冲区中的数据拷贝到接收方的接收缓冲区。
⭐ 2. TCP 为什么存在缓冲区
- 为什么要有发送缓冲区:数据在网络传输的过程中可能会出现丢包的情况,这时候就需要发送方重新发送数据,因此需要提供一个发送缓冲区来暂存未发送 / 未成功发送的数据。只有当被发送出去的数据被接收方确认收到,发送缓冲区中的这部分数据所占用的空间才可以被其他数据使用。
- 为什么要有接收缓冲区:接收端处理数据的速度是有限的,为了不大面积丢弃没来得及处理的数据,TCP 提供了接收缓冲区用来暂存这些待处理数据。
⭐ 3. TCP 的缓冲区大小
- 发送端给接收端发送数据时,说白了就是将发送方自己的发送缓冲区中的数据拷贝到接收方的接收缓冲区。
- 但是缓冲区的容量不可能是无限的。如果接收端处理数据的速度 < 发送端发送数据的速度,那么接收端的接收缓冲区就肯定会被填满。之后发送端再有数据到达的话,接受端就只能将这些数据丢弃了。
- 因此在 TCP 报头中就存在一个 16 位的窗口大小字段,用来标识自身的接收缓冲区中还有多少字节的空间。
- 因此,TCP 的缓冲区范围是 0 ~ 65535 字节。
- 接收端在给发送端传来的报文作应答时,就可以填充这个 TCP 报文中的窗口大小字段,告诉发送端自己的接收缓冲区内还有多少空间,让发送方自行决定发送速度。
🌈 四、TCP 保证可靠性的机制
⭐ 1. 确认应答
-
确认应答机制 (ACK) 是 TCP 所有用来保证可靠性的机制中最重要的机制。
-
ACK 机制通过 TCP 报头中的 32 位序号以及 32 位确认序号来实现。
-
ACK 机制并不能保证通信双方的全部消息的可靠性,而是通过收到对方的应答,来保证发送方曾经发送给对方的某条信息被对方可靠的收到了。
- 注:应答报文单纯就只是一个 TCP 报头,不携带任何数据。
- 发送方只要收到了应答,就能保证发送方发送的数据一定被对方收到。
-
通信双方都可以作为发送方和接收方,只要发送方没收到接收方传回来的应答,就可以判断出自己曾经发送的数据丢了。
-
确认应答机制保证的并不是最新的消息的可靠性,而是保证历史消息的可靠性。
- 发送方能通过确认应答机制知道自己曾经发送的数据到底丢没丢,这才是可靠。
- 通信双方都使用确认应答机制,就能保证通信双方所发送的历史消息的可靠性。
- 注:接收方只会对接收到的消息做应答,而不会对接收到的应答做应答。即不保证应答的可靠性,只保证数据的可靠性。
🌙 1.1 TCP 的通信模式
- 上面所演示的都是发送方一次向接收方发送一条消息,发送方只有在收到应答之后才能继续发送下一条消息,这种通信模式的发送效率低。
- 为了解决这个问题,可以让发送方批量消息,然后接收方再对这些请求批量进行应答。
- 这时候其他问题又来了,发送方没有办法区分收到的多条 ACK 分别对应的是自己曾经发送过的哪一条消息。
- 为了解决这个问题,在 TCP 报头引入了序号与确认序号来为每个消息与应答都分配独一无二的编号,这样就不会混乱了。
🌙 1.2 使用序号和确认序号唯一标识消息和应答
- 由于 TCP 是面向字节流进行传输的,可以讲 TCP 的 发送和接收 缓冲区都当成一个字符数组。
- 序号就是字节缓冲区的数组下标,通信双方都各自持有一套发送缓冲区和接收缓冲区。
- 发送方发送的 data 的序号范围在 1 ~ 100,那么接收方要返回的 ACK 应答报文中的确认序号就是 100 + 1 = 101。
- 发送方在收到 ACK 后提取出确认序号 101,用 101 - 1 就判断出这个 ACK 是对序号范围在 1 ~ 100 的请求的应答。
- 确认序号的定义:确认序号用来表示在确认序号之前的数据都已经全部被收到。
- 根据确认序号的定义,即使应答报文在传输过程中丢失了一部分,只要有一个应答被发送方收到,都能知道确认序号之前的数据成功被接收。
- 例:接收方返回了确认序号为 1001、2001、3001 的 3 个应答报文给发送方。即使前两个应答在半路丢了,只要 3001 这个应答被发送方接收,发送方就能确定在 3001 之前的所有报文都成功被接收。
- 确认序号除了告知发送方之前发送的数据已经被成功接收,还能告诉发送方接下来发送的数据的序号应该从哪个数字开始。
- 引入了确认序号之后,还能减少接收方要返回的应答报文的数量。
- 既然发送方能够通过一个确认序号知道确认序号之前的数据被接收,那么接收方也可以只返回对最后一个数据的应答。
⭐ 2. 超时重传
- 发送方在发送完消息后,如果一段时间内没有收到 ACK 应答,发送方就会重新发送该消息。
- 丢包分为两种情况:一是发送方发送的数据报文丢失;二是接收方返回的应答报文丢失。不管是哪种情况,只要发送方在规定时间内没有收到 ACK 应答报文,都会触发超时重传机制。
🌙 2.1 TCP 的去重功能
- 如果发送方认为数据包丢失,从而触发了超时重传机制。但接收方实际上已经收到了发送方的数据报文,只是因为某些原因而没有及时返回应答报文。这时候接收方就会收到重复的数据报文,为了应对这种情况,TCP 引入了去重功能。
- TCP 中存在着一个叫接收缓冲区的的存储空间,接收端会将接收到的数据放到对应缓冲区中。根据数据的序号判断是否有重复的数据,如果没重复,就将接收到的数据拷贝到对应位置的接收缓冲区中;如果重复,则将后面到来的数据丢弃。
举个例子
- 发送方发送一段序号在 1 ~ 8 的数据,而接收方将收到的数据存储在接收缓冲区中的 1 ~ 8 号位置。
- 然后,发送方因为某些原因触发了超时重传机制,又向接收方发送了一段序号在 1 ~ 8 的数据。接收方在收到这段数据后,会先查看接收缓冲区中的 1 ~ 8 号位置是否已经被占用了。如果没被占用,则将收到的数据拷贝到对应位置,如果被占用了,则将新接收到的数据丢弃。
🌙 2.2 超时重传的等待时间
- 既然发送方会在等待时间之后触发超时重传机制,那么如何设置这个等待时间就是一个问题。
1. 触发超时重传的等待时间不能太长或太短
- 等待时间太长:会导致在丢包后,对方长时间收不到对应的数据,进而影响整体重传的效率。
- 等待时间太短:会导致接收方收到大量的重复数据。没准接收方的响应报文正在奔向发送方的路上呢,发送方就急不可耐的又发了一份数据给接收方。发送方发送报文也是有消耗的。
2. 如何设置触发超时重传的等待时间
- 超时重传等待时间需要合理的设置,最理想的状况就是找到一个最小时间,保证应答报文一定能在这个时间内返回。
- 实际上,因为网络环境的问题,这个等待时间是会变的,不可能是固定的值。
- TCP 为了保证不管在什么环境下都能有较高的性能,会动态计算超时重传的最大超时时间。
- Linux 以 500ms 为一个单位控制超时时间,每次判断超时重传的超时时间都是 500ms 的整数倍。
- 如果在触发了一次超时重传后,依然收不到应答,下一次触发超时重传的等待时间就是 2 × 500ms。
- 如果还是收不到应答,下一层触发超时重传的等待时间就是 4 × 500ms,按指数形式递增。
- 当累计触发了一定次数的超时重传机制后,TCP 就认为是网络或对端主机出现异常,直接强制关闭连接。
⭐ 3. 连接管理
- TCP 协议在通信前需要建立连接,而 TCP 需要经过三次握手建立连接,TCP 需要经过四次挥手断开连接。
- TCP 的可靠性保证的是连接的可靠性,要保证传输数据的可靠性就要建立一条稳定的通信链路。
🌙 3.1 操作系统如何实现对连接的管理
- 只有建立一条连接 TCP 才能实现它的各种可靠性机制。一台机机器上同时会存在大量的连接,操作系统需要将这些连接管理起来。
- 操作系统想要管理好这些连接,就需要一个用来描述连接的结构体类型,这个结构体中包含了连接的各种属性字段,所有定义出来的连接结构体对象最终都会以某种数据结构组织管理起来。此时操作系统对连接的管理就变成了对这个数据结构的增删查改。
- 建立连接,就是在操作系统中用连接结构体定义一个对象,然后填充该对象内的各种字段,最后将这个连接结构体对象插入到用以管理连接的数据结构中。
- 断开连接,就是将某个连接结构体对象从用来管理连接对象的数据结构中删除,释放该连接对象曾经占用的各种资源。
🌙 3.2 TCP 通过三次握手建立连接
- 通信双方在进行 TCP 通信前,需要先建立连接。这个建立连接的过程被称为三次握手。
- 三次握手就是发送方和接收方互相交换自己的 TCP 报头,通过 TCP 标志位中的 SYN 和 ACK 标志位的状态 (0 和 1) 来建立连接。
1. 三次握手的过程
- 第一次握手:客户端发送一个 TCP 标志位中的 SYN 标志位 (请求建立连接标志位) 为 1 的 TCP 报文给服务端。服务端在收到客户端发来的 TCP 报文后,看到 TCP 标志位中的 SYN 标志位为 1,知道了客户端想要与自己建立连接。
- 第二次握手:服务端收到客户端发来的连接请求报文后,向客户端返回一个 ACK 标志位为 1 的 TCP 应答报文,表示确认收到客户端的连接请求报文。并且将该报文的 SYN 标志位也置为 1,表示服务端请求与客户端建立连接 (同意和客户端建立连接)。
- 第三次握手:客户端收到服务端返回的报文后,看到这个报文的 ACK 标志位为 1,知道了服务端已经收到了自己在第一次握手时发出的连接请求报文。同时,看到了 SYN 的标志为也为 1,知道服务端同意与自己建立连接。最后客户端对服务端返回一个 ACK 应答报文,不管服务端有没有收到这个 ACK 应答报文,客户端都认为连接建立完成。
2. 建立连接的本质
- 赌:赌在第三次握手后,客户端发出去的 ACK 应答报文能够被服务端收到。
- 如果不赌的话,服务端就必须对第三次握手时客户端发过来的应答报文作应答。但这时候服务端就又不知道自己的这个对应答报文的应答报文是否被客户端成功接收,又需要客户端返回一个对应答报文的应答的应答报文。最终会导致没完没了的套下去,因此只能靠赌。
- 即使赌输了也没关系,客户端不知道自己的应答报文服务端有没有收到,但服务端知道啊。如果服务端没有收到客户端发来的对第二次握手时自己发送出去的 ACK + SYN 报文的应答,服务端会触发超时重传机制。
3. 如果服务端正处在触发超时重传的等待时间,此时客户端发来了消息该怎么办
- 虽然服务端会触发超时重传机制,但这也是需要时间的。
- 客户端只会认为在三次握手完了之后,连接建立成功。客户端立马就会发起通信,但此时服务端还没有到触发超时重传的时间,这就尬住了。
- 说是这么说,但服务端不会真的呆住。如果服务端没有收到客户端的 ACK 报文,但却收到了客户端发来的其他消息。服务端就会在准备返回给客户端的 ACK 应答报文中将
RST
标志位置 1,让客户端重新执行三次握手建立连接。 - 通过上述方式,即使后续通信过程中连接断开了,接收方也能向发送方返回 RST 标志位为 1 的 TCP 报文,让对方重新执行三次握手建立连接。
4. 为什么是三次握手 (高频面试题)
- 如果一次握手就能建立起连接,就可能产生 SYN 洪水,即客户端可以低成本疯狂的向服务器发送 SYN 报文,服务器就会瞬间挂满不会被使用的连接 (连接是有成本的),从而导致服务器的可用资源越来越少。
- 如果二次握手就能建立起连接,同一次握手同理。客户端发一个 SYN 报文,服务端就得返回一个 ACK 报文。如果客户端屏蔽掉服务端发回来的 ACK 报文,只向服务端发送 SYN 请求报文,同样能造成 SYN 洪水问题。
- 虽然三次握手也会存在 SYN 洪水攻击的问题,但客户端想要攻击服务端,就必须接收第二次握手时服务端发回来的 ACK 报文,然后向服务端发送 ACK 报文,客户端得先完成三次握手。直接提升了客户端发起 SYN 洪水攻击的成本。
- 总结起来就两句话:
-
三次握手是验证双方通信信道的最小次数 (快速验证网络的连通性)
-
一次握手只能验证服务端能接收客户端的连接请求。并不能验证客户端能正确收还是正确能发。
-
二次握手只能验证客户端能正确发送连接请求,不能验证服务端能不能正确发。
-
-
三次握手能够保证建立双方通信的共识意愿
- 客户端想和服务端建立连接得获得服务端的同意 (服务端的 ACK 报文)。
- 服务端想和客户端建立连接得获得客户端的同意 (客户端的 ACK 报文)。
🌙 3.3 TCP 通过四次挥手断开连接
- 操作系统维护连接也是需要成本的,因此 TCP 通信双方在通信结束之后还需要断开连接,这个过程被称为四次挥手。
四次挥手的过程
- 第一次挥手:客户端向服务端发送一个 TCP 标志位中的 FIN 标志位为 1 的 TCP 报文,表示请求与服务端断开连接。
- 第二次挥手:服务端在收到客户端的请求报文后,发现 FIN 标志位为 1,知道客户端想要断开连接。于是执行第二次挥手,对客户端的 FIN 报文作 ACK 应答,表示同意与客户端断开连接。但是此时服务端还不能直接与客户端断开连接,因为可能还有其他数据没发送给客户端,需要先将这些数据发给客户端才行。
- 第三次挥手:服务端此时已经没有数据需要发给客户端了。但由于 TCP 是全双工通信 (通信双方的地位对等),断开连接必须征得双方同意,光服务端同意客户端的断开连接请求还不够,还需要客户端同意服务端的断开连接请求。于是服务端执行第三次挥手,服务端向客户端发送一个 FIN 标志位为 1 的 TCP 报文,请求与客户端断开连接 。
- 第四次挥手:客户端在收到服务端的报文后,通过值为 1 的 FIN 标志位知道服务端请求和自己断开连接。于是执行第四次挥手,发送一个 ACK 报文给服务端,表示同意服务端的 FIN 请求。
⭐ 4. 流量控制
- 通信双方在进行通信时,本质上是在将发送方的发送缓冲区中的数据拷贝到接收方的接收缓冲区。
- 接收端处理数据的速度是有限的。如果发送端发送数据的速度太快,导致接收端的接收缓冲区很快被占满,发送端之后再发给接收端的数据就会被接收端丢弃 (丢包)。
- TCP 会根据接收端接收数据的能力来决定发送端发送数据的速度,这种机制被称为流量控制。
🌙 4.1 接收端应将自己接收数据的能力告知发送端
- 接收端会在要返回给发送端的 ACK 报文中填充 16 位窗口大小字段,告知自己的接收缓冲区中还能存放多少数据。
- 由于 TCP 是全双工通信,通信双方都要执行流量控制。任何一方在作为发送方发送 TCP 报文时,都要填写 TCP 报头中的 16 位窗口大小字段,来告知对端主机自己的接收缓冲区的剩余空间。
- 发送端在收到 ACK 报文后,会根据获取到的接收端的接收缓冲区大小来调整自己发送数据的速度。
- 当发送端得知接收端接收数据的能力 (接收缓冲区没空间) 时,就会暂时停止发送数据。
- 但发送端并不会一直停止发送数据,等到接收端的接收缓冲区中又有足够的空间时,发送端就会重新开始发送数据。现在的问题就成了发送端该咋知道啥时候能继续发数据。
1. 发送端怎么知道什么时候可以继续发送数据
- 等待告知:在接收端的上层应用将接收缓冲区的数据读走后,接收端就会向发送端发送一个 TCP 报文,主动将自己的窗口大小告知给发送端,发送端在得知有空间了后,就会继续向接收端发送数据。
- 主动询问:发送端每隔一段时间就会向接收端发送一个裸的 TCP 报文 (只有报头),接收端在对这个报文作应答时,会自动在 ACK 报文中填充 16 位窗口大小,发送方就能知道接收方的接收缓冲区大小了。
- 上述两种策略在网络中被同时使用,没有选择谁这种说法,同时使用才能最快速建立对接收缓冲区的共识。
🌙 4.2 设置 PSH 标志位让对端尽快处理数据
- 如果接收端的窗口大小一直为 0 时,发送端不可能会一直向接收端发送探测报文,这时候就要用上 TCP 标志位中的
PSH
标志位了。 PSH
标志位的全称是 PUSH,用来提示接收端的应用程序立刻将 TCP 接收缓冲区中的数据读走。
- 虽然以这种例子来说明
PSH
标志位的功能,但不代表这个标志位只能在接收缓冲区中没空间时使用。PSH
标志位本质上是让对端尽快处理数据。
🌙 4.3 第一次发数据时如何得知对方的窗口大小
- 通信双方在进行 TCP 通信时需要经过三次握手建立连接。双方在握手的过程中,也是在交换各自的报文。在交换的报文中,除了验证双方的通信信道是否畅通外,还交互了其他信息。
- 这个交互的其他信息中,就包括了自己的接收缓冲区的剩余容量。因此,通信双方就可以在通信之间就得知对方接收数据的能力。
⭐ 5. 滑动窗口
- 流量控制是让发送方根据接收方的数据接收能力来调整发送数据的速度。问题是,发送方应如何根据接收方的数据接收能力来调整发送速度?
- 发送报文后, 收到 ACK 应答前,在这期间处于超时重传的等待时间。在等待时间以内,发送方不能将已经发送的报文丢弃,而是要保存起来。 问题是,这些数据应该保存在哪?
- 为了解决上述问题,引入了滑动窗口机制,滑动窗口是发送缓冲区中的一部分。
1. 将发送缓冲区中的数据分成三部分
- 已经发送 & 已经收到应答的数据。
- 已经发送 & 还没收到应答的数据 (滑动窗口)。
- 待发送的数据。
🌙 5.1 滑动窗口的功能
- 在滑动窗口内的数据不需要等待应答,可直接发送给接收端。
- 发送端在收到某一个确认序号时,表示该确认序号之前的数据已经被接收端接收,可以直接将滑动窗口的起始位置移动到确认序号处。
🌙 5.2 滑动窗口的大小变化
- 滑动窗口的大小不是固定的,会根据接收端的数据接收能力来调整滑动窗口的大小。
- 接收端的数据接收能力强,滑动窗口就大;接收端的数据接收能力弱,滑动窗口就小。
- 注:滑动窗口的实际大小不止依靠接收端的窗口大小,还要根据之后的拥塞控制中的拥塞窗口来判断。这里暂时以滑动窗口大小等于接收端窗口大小来举例。
- 且由于滑动窗口左侧的都是已经收到应答 (被接收端成功接收) 的数据,因此滑动窗口不会向左滑。
1. 扩大滑动窗口
- 假设接收端的应用层从接收缓冲区中拿走了 1000 个字节的数据,那么接收端的接收缓冲区就会多出 1000 字节的可用空间。接收端会在返回给发送端的 TCP 报文中告知发送端,接收端的接收缓冲区空余空间变成了 4000 字节。此时发送端的发送缓冲区的滑动窗口就会扩大成 4000 个字节。
2. 缩小滑动窗口
- 以滑动窗口的大小为 4000 字节为例:如果接收端已经接收到了 1001 ~ 2000 这段数据,将这 1000 字节的数据放进接收缓冲区中,并对这段数据作出应答。
- 但是应用层却不从接收缓冲区中拿取数据,就会导致接收缓冲区的可用空间少了 1000 字节,接收端会告诉发送端自己的接收缓冲区空闲空间变成了 3000 字节,于是发送方的发送窗口大小就缩小到 3000 字节。
🌙 5.3 滑动窗口通过双指针实现
-
前面已经提到过,TCP 的缓冲区本质上就是个字符数组,而 TCP 报文中的序号就是这个字符数组的下标。
-
那么滑动窗口本质上就是通过两个指针 (数组下标) 来进行维护。
- 可定义 win_start 和 win_end 两个整型变量,用 win_start 标识滑动 窗口的起始下标,用 win_end 标识滑动窗口就的结尾下标。
-
当发送端收到接收端返回的的应答报文时,假设应答报文中的确认序号为 N,窗口大小为 M (接收端返回的 TCP 报文中的 16 位窗口大小)。此时就可以将 win_start 更新为 N,将 win_end 更新为 win_start + M。
- 注:win_start 和 win_end 维护的是一个前闭后开的区间,即为
[win_start, win_end)
- 注:win_start 和 win_end 维护的是一个前闭后开的区间,即为
- 当滑动窗口为零时,就是让 win_start == win_end
- 当滑动窗口扩大时,就是固定住 win_start,让 win_end++
- 当滑动窗口缩小时,就是固定住 win_end,让 win_start++
🌙 5.4 滑动窗口如何解决丢包
-
当发送端一次性发送多个报文数据时,可以将丢包问题大体上分为以下 2 种:
-
数据包已经抵达,ACK 报文丢了。
-
数据包丢失。
-
1. 数据包已经抵达,ACK 报文丢了
- 根据确认序号的定义,发送端能够根据接收端返回的 ACK 报文中所携带的确认序号来判断该序号前的数据是否被成功接收。
- 例:主机 A 收到了主机 B 最后发来的 ACK (6001),此时主机 A 就能知道序号在 6001 之前的数据已经成功被主机 B 接收。
2. 数据包丢失
- 如果接收端没有接收到序号在前的报文,反而是收到了序号在后的报文,接收端就会提醒发送端应该应该发送序号是多少的报文。
- 例:丢失的是 1001 ~ 2000 这个报文,主机 B 并没有收到该报文,但是却收到了 2001 ~ 3000 的报文,主机 B 就会在 ACK 报文中将确认序号填充为 1001,提醒主机 A 下一个应该发送序号以 1001 开头的报文。
- 如果主机 A 连续收到了三次确认序号都是 1001 的 ACK 报文,就会将 1001 ~ 2000 的数据包重新发送。这种数据包补发的机制被称为快重传。
- 如果通信已经接近末期,做不到连续收到三次确认序号相同的 ACK 报文,就无法触发快重传机制,转而触发超时重传机制。
- 之后如果接收端收到了这个 1001 ~ 2000 的数据包,就会直接发送确认序号为 6001 的 ACK 报文,表示 6001 之前的数据已被接收。
🌙 5.5 如何防止滑动窗口滑出缓冲区
- ACK 报文中的确认序号只会增加,这就导致了滑动窗口只会向右滑动。
- 如果将发送缓冲区想象成一种环状结构,滑动窗口就只会在这个环形队列中顺时针转圈圈,不可能发生越界的问题。
⭐ 6. 拥塞控制
🌙 6.1 使用拥塞控制解决网络拥堵
- 在网络中进行通信时,不止要考虑通信双方的问题,还应该考虑通信双方之间的网络通路的问题。
- 虽然 TCP 可以使用滑动窗口来高效可靠的发送大量数据。但如果在通信的初始阶段就发送大量的数据,就可能造成网络拥塞的问题。
- 互联网中存在着大量的主机,这些主机通过网络进行连接。在不清楚当前网络状况的情况下,贸然发送大量数据,就可能使得网络变拥堵。
- 为了知道网络状况,TCP 引入了慢启动机制:先发送少量的数据,用来摸清楚当前的网络拥堵情况,再决定发送数据的速度。
- 发送端发送少量数据给接收端,由于 TCP 通信双方都做了流量控制,如果还出现大面积丢包,则说明是网络出问题了。
- 如果通过慢启动机制确定了是因为网络问题导致的丢包,就不能再搞什么超时重传、快重传了。
- 在网络拥堵的情况下,继续往网络发数据只会导致网络状况越来越差。
- 为了解决上述问题网络拥堵的问题,TCP 引入了拥塞控制 机制。拥塞控制实现的是让使用同一个网络进行通信的主机,具有拥塞避免的共识!
- 如果所有使用同一个网络进行通信的主机都有意识的避免造成网络拥塞,就能大大减少发生网络拥塞的概率。
- 在同一网络下的主机如果通过慢启动机制发现网络出现了拥堵,那么这些主机都会主动进行拥塞避免,减少往网络中发送数据。
- 如果通过慢启动机制发现网络状况良好,就逐步增大数据的发送量。
🌙 6.2 使用拥塞窗口实现拥塞控制
- 在进行 TCP 通信时,不仅要考虑接收端能不能抗住发送端的数据 (滑动窗口),还要考虑网络能不能扛得住发送端发送的数据 (拥塞窗口)。
- 为了实现拥塞控制机制,TCP 引入了拥塞窗口 (整型值),用来记录网络的数据接收能力。
拥塞窗口的定义
- 如果一次性向网络中发送的数据量不超过拥塞窗口,则不会引发网络拥塞。如果一次性向网络中发送的数据量超过了拥塞窗口,则可能引发网络拥塞。
- 发送端发送的数据量既要在接收方的数据接收能力内,也要在网络的数据接收能力内。
- 因此,滑动窗口的实际大小应该是
min(应答窗口,拥塞窗口)
,取接收端的数据接收能力和网络的数据接受能力的较小值。 - 拥塞窗口的大小是根据网络的拥堵状况来动态变化的。
🌙 6.3 如何调整拥塞窗口的大小
- 就如同没人一开始就知道网络的拥堵状况一样,也没法知道拥塞窗口一开始该设置成多大。
- 假设拥塞窗口最开始的值为 20,通过慢启动机制,发送端一开始发送一条报文后如果能够收到对这条,则说明网络状况良好。将拥塞窗口的大小调整为 21,然后让发送端发送 2 条报文,如果能正常收到应答,则继续将拥塞窗口的大小调整为 22。以此类推,直到开始出现大面积丢包,才开始减小拥塞窗口的大小。
慢启动的阈值
- 像上面那样的拥塞窗口增长速度,是指数级别的。慢启动只是启动比较慢而已,但是增长速度非常快。
- 为了控制增长速度,不能单纯的以指数的方式扩大增长窗口,因此慢启动的阈值就出现了。
- 慢启动的阈值:当拥塞窗口的大小超过某个阈值时,不再按照指数的方式,而是实行线性增长。
- 指数增长:在进行 TCP 通信的初期,拥塞窗口的值为 20,并不断以指数的方式进行增长。
- 加法增大:慢启动的阈值初始设为接收端的接收缓冲区的剩余容量,图中慢启动的阈值就是 16。当拥塞窗口的值增大道 16 时就从指数增长转为线性增长。
- 这个慢启动阈值 16 是上一次发生网络拥塞时,拥塞窗口的一半。
- 乘法减小:当拥塞窗口增长到会造成网络拥堵的情况时,必须要尽快恢复通信。因此,慢启动的阈值变成
拥塞窗口的值 * 0.5
,然后让拥塞窗孔的值重新变成 1 进行指数增长。
⭐ 7. 延迟应答
- 接收端如果在收到数据时,立刻对该数据作出应答。那么每一次的应答,所返回的接收窗口就可能比较小。
举个例子
- 假设接收端的接收缓冲区剩余空间为 1 MB,在收到一份 500 KB 的数据后,如果立刻对这个分数据作应答,此时返回的窗口大小就是 500 KB。
- 实际上,接收端处理数据的速度是相当快的,10ms 内就能将接收缓冲区内的这 500 KB 数据消费掉。
- 但是在这种情况之下,接收端还没有达到自己处理数据的极限,即使窗口再大一些,也能搞得定。
- 如果接收端对收到的数据进行延迟应答,譬如说等待个 30 ms,将收到的 500 KB 数据处理完,此时接收端的数据接收窗口又是 1 MB。
🌙 7.1 延迟应答的策略
- 窗口越大,网络吞吐量就越大,传输效率也就越高。TCP 就要要再保证网络不发生拥堵的情况下,尽可能的提升传输效率。
- 但并不是所有的数据包都可以被延迟应答。
不是所有的包都可以被延迟应答
- 数量限制:每接收到 N个包就对这批数据的最后一个进行应答。
- 时间限制:延迟时间应该在触发超时重传的等待时间内,一旦超过最大延迟时间就应答一次。
- 具体的数量和超时时间,根据操作系统的不同也有差异。一般将 N 取 2,超时时间取 200ms。
⭐ 8. 捎带应答
- 由于 TCP 实现的是全双工通信,通信双方都可以作为发送方和接收方进行发送数据和接收数据。
- 作为接收方返回应答报文给发送方时,如果此时接收方还有其他消息要捎带着发给对方,就可以也作为发送方将自己的消息跟着 ACK 报文一起发出。
- 通过这种方式,某一方就不需要连发两条报文,而是直接将应答和消息合并。
- 为了实现捎带应答机制,TCP 报文中才会同时有序号和确认序号 (对端发过来的报文中,可能会携带数据)。
- 捎带应答最直接的好处就是提升了发送数据的效率,让通信双方就不用发送单纯的确认报文。
- 由于执行了捎带应答的报文携带了有效数据,对端主机在收到该报文后就会对齐作出应答。当收到这个应答后,就可以知道自己发送的数据 和 ACK 报文都被对方可靠的收到了。
🌈 五、面向字节流
- 在创建一个 TCP 的 socket 时,会在内核中分别创建一个发送缓冲区和接收缓冲区。
⭐ 1. TCP 以字节为单位发送和接收数据
- 在上层应用调用
write
系统调用将数据写入到内核中时,数据会先写入到发送端的发送缓冲区中。 - 如果要发送的数据的字节数太多,就会将这些数据拆分成多个 TCP 数据包发出。
- 如果要发送的数据的字节数太少,就会让这些数据先在缓冲区中等待。待缓冲区内积压了差不多的待发送数据,或者其他合适的时候将这些数据发送出去。
- 接收数据时,数据也是从网卡的驱动程序到达接收端的接收缓冲区中。然后由接收端的应用程序调用
read
系统调用从接收缓冲区中拿取数据。
⭐ 2. TCP 程序的读和写不需要逐个对应
- TCP 不关心 发送 / 接收 缓冲区中装的是什么数据,它只知道这些都只是字节数据。TCP 只会按照发送需求不断的往接收端发送数据。
- TCP 的任务就是将这些字节数据可靠的发送到对端的接收缓冲区中。写入数据的次数和读取数据的次数不会完全一致。
- 往发送缓冲区中写入 100 个字节的数据时,不需要考虑如何写。可以调用一次 write 写 100 个字节,也可以调用 100 次 write,每次写入一个字节。
- 从接收缓冲区中读取 100 个字节的数据时,不需要考虑如何读。可以调用一次 read 读100 个字节,也可以调用 100 次 read,每次读取一个字节。
🌈 六、粘包问题
- 由于 TCP 使用面向字节流的方式进行通信,因此必然会遇到粘包问题。
⭐ 1. 什么是粘包问题
- 站在 TCP 的角度看,它没有报文的概念。在 TCP 眼里只有缓冲区中以字节为单位的数据。
- TCP 只会按照需求不断的发送数据给接收端,TCP 不在乎一次性到底发送了几个数据报文,它只知道自己一次性发送了多少字节的数据。
- 在应用层看来,它无法识别接收缓冲区的这一堆字节数据中到底有几个数据包。以及判断出这些数据包位于接收缓冲区的第几个字节。
⭐ 2. 如何解决粘包问题
- 粘包问题不是 TCP 能解决的,这个问题得依靠应用层来解决。
- 想要避免出现粘包问题,总结起来就是明确报文和报文间的边界。
1. 解决定长数据包的粘包问题
- 保证每次读取都按照固定的大小读取即可。假设一个数据包固定为 16 KB,从缓冲区头部开始依次按照 16 KB 读取即可。
2. 解决变长数据包的粘包问题
- 在包头的位置,约定用于记录该数据包总长度的字段,从而知道包的结束位置。
- 如 HTTP 报头中就包含 Content-Length 字段,用来表示正文的长度。
- 在包和包之间使用明确的分隔符 (应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可)。
⭐ 3. UDP 不存在粘包问题
- UDP 报头的大小固定只有 8 个字节,且 UDP 报头中还包含着 UDP 报文长度这个字段,用以记录整个 UDP 报文的大小。
- 站在应用层的角度看,只能发送和接收完整的的 UDP 报文,不存在发送 半个 UDP 报文这种说法,报文与报文之间界限明确。
🌈 七、TCP 异常情况
- 当客户端正常访问服务器时,如果客户端突然出现以下情况,建立好的连接会怎么样?
⭐ 1. 进程异常终止
- 当进程退出时,会释放该进程曾经打开的文件描述符所指向的文件。
- 当客户端进程退出时,相当于自动调用了
close
系统调用关闭了文件描述符所对应的文件。此时双方的操作系统会在底层正常完成四次挥手的过程,然后释放对应的连接资源。 - 即,进程进程异常终止时,会自动释放文件描述符,TCP 底层可以正常发送 FIN 报文,和进程正常退出没区别。
⭐ 2. 机器重启
- 重启机器的情况和进程异常终止的情况一样。操作系统会先 kill 掉所有进程然后再重启主机。
- 此时双方操作系统也能正常完成四次挥手的过程,然后释放对应的连接资源。
⭐ 3. 机器掉电 / 网线断开
- 当客户端掉线后,服务端没办法短时间内知道,因此服务端会暂时维持着与客户端的连接。但服务端由于保活策略的原因,不会一直维持着与客户端的连接。
1. 保活策略
- 服务端会定期检查客户端的存在状况,判断客户端是否在线。如果服务端连续多次没有收到来自客户端的 ACK 应答,就可以判断出客户端已经离线,从而直接断掉与客户端的这条连接。
- 服务端也有可能通过定期向服务端发送信息来报平安,如果服务端长时间没有收到来自客户端的报信,也可能超时关闭与客户端的连接。
2. 操作系统基本不使用保活策略
- 在正常通信中,TCP 的保活间隔是几十分钟或一个小时。
- 服务端可能每隔一个小时才会发出一个询问报文,用来询问客户都是否在线。
- 对于操作系统来说,维持连接是一件很困难的事。保活策略主要还是由应用层程序来做。
🌈 八、基于 TCP 实现的应用层协议
- 以下应用层协议都是基于传输层的 TCP 协议来实现的:
协议 | 说明 |
---|---|
HTTP | 超文本传输协议 |
HTTPS | 安全数据传输协议 |
SSH | 安全外壳协议 |
TELNET | 远程终端协议 |
FTP | 文件传输协议 |
SMTP | 电子邮件传输协议 |
🌈 九、TCP 与 UDP
⭐ 1. TCP 与 UDP 的对比
- 是否保证可靠性不能用来判断这两个协议谁好谁坏,而应该作为一种特点看待。
- TCP 保证可靠性的前提是引入前面那一堆用来保证可靠性的机制,需要比 UDP 做更多的工作。
协议 | 应用场景 |
---|---|
TCP | 用于需要保证可靠传输的情况,应用于文件传输、重要状态更新等场景 |
UDP | 用于对高速传输和实时性要求较高的通信领域,如早期的 QQ、视频传输等 |
- 一般而言,如果对可靠性有点要求,或者自己都不知道选择什么协议时,选 TCP 就行。
⭐ 2. 如何用 UDP 实现可靠传输 (面试题)
- 当被问到这个问题时,一定要想到 TCP 协议是如何保证可靠性的。
- 想要让 UDP 也能变得可靠,只要选择性为 UDP 添加 TCP 的保证可靠的机制即可。