type
status
date
slug
summary
tags
category
password

1、传输层概述

1.1 传输层协议

传输层为两个主机中的进程提供数据传输服务。注意这里的端点目标是进程而不是应用程序,因为一个应用程序里面可能会运行多个进程。例如下图所示,主机 A 的应用进程 AP1 和主机 B 的应用进程 AP3 通信,而与此同时,应用进程 AP2 也和对方的应用进程 AP4 通信。
notion image
从这里可以看出网络层和传输层有明显的区别:网络层是为主机之间提供逻辑通信,而传输层为应用进程之间提供端到端的逻辑通信
TCP/IP 运输层的两个主要协议都是因特网的正式标准,即:
  • UDP(User Datagram Protocol):用户数据报协议。UDP 在传送数据之前不需要先建立连接。远地主机的运输层在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 却是一种最有效的工作方式。
  • TCP(Transmission Control Protocol):传输控制协议。TCP 则提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。
下面是主流应用层协议使用的运输层协议:
notion image

1.2 传输层的端口号

为了明确知道传输层的报文要交付到目标主机的哪一个应用程序,就必须用统一的方法(而这种方法必须与特定操作系统无关)对 TCP/IP 体系的应用进程进行标志。解决这个问题的方法就是在运输层使用协议端口号(protocol port number),或通常简称为端口(Port)。这就是说,虽然通信的终点是应用进程,但我们只要把要传送的报文交到目的主机的某一个合适的目的端口,剩下的工作(即最后交付目的进程)就由 TCP 来完成。
在后面将讲到的 UDP 和 TCP 的首部格式中,我们将会看到它们都有源端口目的端口这两个重要字段。当运输层收到IP层交上来的运输层报文时,就能够根据其首部中的目的端口号把数据交付应用层的目的应用进程。由此可见,两个计算机中的进程要互相通信,不仅必须知道对方的 IP 地址(为了找到对方的计算机),而且还要知道对方的端口号(为了找到对方计算机中的应用进程)
TCP/IP 的传输层用一个 16 位端口号来标志一个端口。16 位的端口号可允许有 65535 个不同的端口号。可以分为下面两类:
  • 服务器端使用的端口号。这里又分为两类:
    • 最重要的一类叫做熟知端口号(well-known port number)或系统端口号,数值为 0~1023。这些数值可在网址 www.iana.org 查到。IANA 把这些端口号指派给了 TCP/IP 最重要的一些应用程序,让所有的用户都知道。常用的熟知端口号有:
      • notion image
    • 另一类叫做登记端口号,数值为 1024~49151。这类端口号是为没有熟知端口号的应用程序使用的。使用这类端口号必须在 IANA 按照规定的手续登记,以防止重复。
  • 客户端使用的端口号,数值为 49152~65535。由于这类端口号仅在客户进程运行时才动态选择,因此又叫做短暂端口号。这类端口号是留给客户进程选择暂时使用。当服务器进程收到客户进程的报文时,就知道了客户进程所使用的端口号,因而可以把数据发送给客户进程。通信结束后,刚才已使用过的客户端口号就不复存在,这个端口号就可以供其他客户进程使用。

2、TCP和UDP协议介绍

2.1 TCP协议

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。这次有三个重要概念:
  • 面向连接:这就是说,应用程序在使用 TCP 协议之前,必须先建立 TCP 连接。而且每一条 TCP 连接只能有两个端点(endpoint),每一条 TCP 连接只能是点对点的(一对一),不能像 UDP 协议可以一个主机同时向多个主机发送消息。
  • 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证传送的数据无差错、不丢失、不重复、并且按序到达
  • 字节流:虽然应用程序和 TCP 交互是一次一个数据块,但 TCP 把应用程序交付的数据转换成一连串的无结构的字节流。TCP 并不知道所传送的字节流的含义,但是可以保证接收方的应用程序有能力识别收到的字节流,把它还原成有意义的应用层数据。如下图所示
notion image
为了实现以上功能特性,TCP 协议做了以下设计:
  • 为了保证可靠性,采用确认机制和超时重传。TCP 为了保证不发生丢包,就给每个字节一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK); 如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。TCP 用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
  • 为了实现流量控制,采用滑动窗口协议。协议中规定,对于窗口内未经确认的分组需要重传。
  • 为了实现拥塞控制,采用慢启动算法。

2.2 TCP报文格式

notion image
一个 TCP 报文分为首部和数据两部分,而 TCP 的全部功能都体现在首部的各字段上。TCP 报文段首部的前 20 个字节是固定的,后面有 4n 字节是根据需要而增加的选项(n是整数)。因此 TCP 首部的最小长度是 20 字节
首部固定部分各字段的意义如下:
  • 源端口号 :16 位,数据发起者的端口号。
  • 目标端口号:16位,数据接收者的端口号。
  • 序列号:32 位,序号范围是 [0, 2^32 - 1],共 4294967296 个序号。TCP 是面向字节流的。在一个 TCP 连接中传送的字节流中的每一个字节都按顺序编号。整个要传送的字节流的起始序号必须在连接建立时设置,是由计算机生成的随机数作为其初始值。首部中的序号字段值则指的是本报文段所发送的数据的第一个字节的序号。例如,一报文段的序号字段值是 301,而携带的数据共有 100 字节。这就表明:本报文段的数据的第一个字节的序号是 301,最后一个字节的序号是 400。显然,下一个报文段(如果还有的话)的数据序号应当从 401 开始,即下一个报文段的序号字段值应为 401。
  • 确认应答号:32 位,是期望收到对方的下一个报文段数据的第一个字节的序号。例如,B 正确收到了A发送过来的一个报文段,其序号字段值是 501,而数据长度是 200 字节(序号 501~700),这表明 B 正确收到了 A 发送的到序号700为止的数据。因此,B 期望收到 A 的下一个数据序号是701,于是 B 在发送给 A 的确认报文段中把确认号置为 701。请注意,现在的确认号不是 501,也不是 700,而是 701。发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决不丢包的问题。
  • 首部长度:又称为数据偏移,4 位,指 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远,这个字段实际上是指出TCP报文段的首部长度。单位是 32 位字(以 4 字节为计算单位)。
  • 保留位:6 位,保留为今后使用,目前应置为 0。
  • 控制位:6 个控制位说明本报文段的性质,分别对应六个字段。
    • URG:该位为 1 时,告诉系统此报文段中有紧急数据,应尽快传送。
    • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。只有当 ACK=1 时确认应答号字段才有效;当 ACK=0 时,确认应答号无效。
    • PSH:该值为 1 时,应尽快地交付接收应用进程,而不再等到整个缓存都填满了后再向上交付。
    • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
    • SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
    • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
  • 窗口大小:16 位,窗口指的是发送本报文段的一方的接收窗口(而不是自己的发送窗口),窗口值告诉对方:从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量。窗口字段明确指出了现在允许对方发送的数据量,用于流量控制。例如,设确认号是 701,窗口字段是 1000。这就表明,从 701 号算起,发送此报文段的一方还有接收 1000 个字节数据(字节序号是 701 ~ 1700)的接收缓存空间。
  • 检验和:16 位,用于检测发送过程中是否出现错误,检验和字段检验的范围包括首部和数据这两部分。
  • 紧急指针:16 位,紧急指针仅在 URG = 1 时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据)。因此,紧急指针指出了紧急数据的末尾在报文段中的位置。当所有紧急数据都处理完时,TCP就告诉应用程序恢复到正常操作。
  • 选项:长度可变,最长可达40字节。TCP 最初只规定了一种选项,即最大报文段长度 MSS( MSS并不是整个TCP报文段的最大长度,而是“TCP报文段长度减去TCP首部长度”)。后面又陆续增加了几个选项。如窗口扩大选项、时间戳选项等[RFC 1323]。以后又增加了有关选择确认(SACK)选项[RFC 2018]。
  • 填充:这是为了使整个首部长度是 4 字节的整数倍。
💡
TCP 四元组可以唯一的确定一个连接
四元组包括如下:
  • 源地址
  • 源端口
  • 目的地址
  • 目的端口
其中源地址和目的地址的字段(32位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机;源端口和目的端口的字段(16位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。所以 TCP 协议只包含端口号,不包含 IP 地址。
💡
MTU 和 MSS 是什么
  • MTU(Maximum Transmit Unit):最大传输单元,物理网络单次数据传输时允许的最大数据包大小,即 IP 层提交给物理接口(数据链路层)的单次传输的最大数据长度,包含 IP 头和 TCP 头的长度。MTU 受物理网络(如以太网、Wi-Fi)的帧格式限制,以太网(Ethernet)的默认 MTU 值是 1500 字节。如果 IP 层有小于等于 MTU 大小的数据需要发送,只需要一个 IP 包就可以完成发送任务;当 IP 层有一个超过 MTU 大小的数据要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU,这样才能完成数据的发送。
  • MSS(Maximum Segment Size):最大报文段长度,TCP 协议单次传输的有效数据部分(Payload)的最大值,即 TCP 提交给 IP 层的单次传输的最大报文长度,不包含 IP 头和 TCP 头的长度。MSS 概念只适用于 TCP 协议,UDP 协议无 MSS 概念。
notion image
一般来说:
例如底层物理接口 MTU = 1500 字节,则 MSS = 1500 - 20(IP Header) - 20 (TCP Header) = 1460 字节。如果应用有 2000 字节需要发送,需要分割成两个 segment 才可以完成发送,第一个 TCP segment = 1460,第二个 TCP segment = 540。
为了需要 MSS 和 MTU 两个概念呢?
根本原因是避免 IP 分片。IP 层没有超时重传等纠错机制。如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。为了只重传丢失的数据,我们会在 TCP 层把超过 MSS 大小的数据提前进行数据分片,确保最后在 IP 层组装分片长度不会大于 MTU,自然也就不用 IP 分片了。经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。

2.3 UDP协议

UDP 不提供复杂的控制机制,只在 IP 层的数据报之上增加了少部分功能
  • 复用和分用
  • 差错检测
UDP 的主要特点是:
  • UDP 是无连接的,即发送数据之前不需要建立连接(当然,发送数据结束时也没有连接可释放),因此减少了开销和发送数据之前的时延。
  • UDP 使用尽最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的连接状态表(这里面有许多参数)。
  • UDP 是面向报文的。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。这就是说,应用层交给 UDP 多长的报文,UDP 就照样发送,即一次发送一个报文。
  • UDP 没有拥塞控制,因此网络出现的拥塞不会使源主机的发送速率降低。这对某些实时应用是很重要的。很多的实时应用(如 IP 电话、实时视频会议等)要求源主机以恒定的速率发送数据,并且允许在网络发生拥塞时丢失一些数据,但却不允许数据有太大的时延。UDP 正好适合这种要求。
  • UDP支持一对一、一对多、多对一和多对多的交互通信。
  • UDP的首部开销小,只有 8 个字节,比 TCP 的 20 个字节的首部要短。

2.4 UDP报文格式

UDP 协议头部只有 8 个字节( 64 位)。
notion image
  • 源端口号:在需要对方回信时选用。不需要时可用全0。
  • 目的端口号:这在终点交付报文时必须要使用到。
  • 包⻓度:该字段保存了 UDP 首部的⻓度跟数据的⻓度之和,其最小值是8(仅有首部)。
  • 校验和:检测 UDP 用户数据报在传输中是否有错。有错就丢弃。

2.5 TCP和UDP的区别

  1. 连接
      • TCP 是面向连接的传输层协议,传输数据前先要建立连接。
      • UDP 是不需要连接,即刻传输数据。
  1. 服务对象
      • TCP 是一对一的两点服务,即一条连接只有两个端点。
      • UDP 支持一对一、一对多、多对多的交互通信
  1. 可靠性
      • TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。
      • UDP 是尽最大努力交付,不保证可靠交付数据。
  1. 拥塞控制、流量控制
      • TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
      • UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
  1. 首部开销
      • TCP 首部⻓度较⻓,会有一定的开销,首部在没有使用「选项」字段时是20个字节,如果使用了「选项」字段则会变⻓的。
      • UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
  1. 传输方式
      • TCP 是流式传输,没有边界,但保证顺序和可靠。
      • UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
  1. 分片不同
      • TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
      • UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层,但是如果中途丢了一个分片,在实现可靠传输的 UDP 时则就需要重传所有的数据包,这样 传输效率非常差,所以通常 UDP 的报文应该小于 MTU。

2.6 使用Wireshark解析TCP报文

我们使用 tcpdump 命令对 TCP 报文进行抓包,并使用 wireshark 工具解析 TCP 报文并进行可视化展示,这样可以加深对 TCP 报文格式的理解。
比如我们要请求 elasticsearch 的默认地址 http://localhost:9200
1、使用 tcpdump 命令对请求地址为回环地址 lo 网卡,端口为 9200 的数据包进行抓包,并把抓取到的数据保存在 es.pcap 文件。
2、使用 wireshark 工具打开 es.pcap 文件,如下图所示
notion image
3、查看本次 TCP 连接的第一个报文的首部字段,这个也是建立连接前三次握手的第一次握手。
notion image
这里可以看到:
  • Source Port:源端口号为 42944
  • Destination Post:目标端口号为 9200
  • Sequence Number:序列号,真实值是 2737882246,这是计算机随机生成的一串数字。相对偏移值是 0,这里是相对本次 TCP 连接的其他报文的偏移量。
  • Acknowledgement Number:确认应答号,真实值和相对偏移值都是 0
  • Header Length:首部长度,即 TCP 报文除去报文数据之外的报文首部长度。这里是 8,即 8 * 4 = 32 字节。
  • Flags:这里六位保留位都是 0,控制位里面只有 SYN = 1。TCP 规定除了最初建立连接时的 SYN 位必须设置为 1 。
  • Window:服务器向客户端返回的滑动窗口大小为 43690
  • Checksum:校验和。
  • Urgent Pointer:紧急指针。
  • Options:可选项。这里标明了 MSS 为 65495 字节。

3、连接管理

3.1 三次握手过程

TCP(传输控制协议)通过三次握手(Three-way Handshake)机制建立可靠的双向连接。
假定主机 A 运行的是 TCP 客户程序,而 B 运行 TCP 服务器程序。最初两端的 TCP 进程都处于 CLOSED(关闭)状态。图中在主机下面的方框分别是 TCP 进程所处的状态。请注意,A 主动打开连接,而 B 被动打开连接。
notion image
  1. SYN(同步)
      • 客户端 → 服务器:发送一个 SYN=1 的 TCP 报文,并随机生成初始序列号seq=x(表示客户端数据起始序号)。该报文不包含应用层数据。
      • 客户端状态SYN_SENT(等待服务器确认)。
        • notion image
  1. SYN-ACK(同步确认)
      • 服务器 → 客户端
        • 回复 SYN=1ACK=1 的报文,确认客户端的序列号(ack=x+1)。
        • 同时发送自己的初始序列号seq=y(服务器数据起始序号)。
      • 服务器状态SYN_RECEIVED(等待客户端确认)。
        • notion image
  1. ACK(确认)
      • 客户端 → 服务器
        • 发送 ACK=1 的报文,确认服务器的序列号(ack=y+1),可能携带应用层数据。
        • 回复序列号seq=x+1(因为 SYN 占用一个序号)。
      • 双方状态:进入ESTABLISHED(连接建立完成)。
        • notion image
一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
注意:第三次握手是可以携带数据的,前两次握手是不可以携带数据的。
💡
为什么握手是三次,不是两次,四次?
需要三次握手而不是两次或四次的原因:
两次握手(客户端发送 SYN,服务端回复 SYN-ACK,存在两个问题:
  • 历史连接干扰问题。假如网络拥堵,客户端发送的 SYN 报文,或者服务器返回的 SYN-ACK 报文都有可能发生拥堵或者丢失。如果客户端之前发送的延迟的 SYN 报文(因网络拥堵)突然到达服务端,服务端会误认为这是一个新连接,直接分配资源并回复 SYN-ACK。但客户端知道这是过期的报文,会忽略服务端的响应,导致服务端资源浪费(如分配内存、维护连接状态)。
  • 无法确认客户端的接收能力。如果 SYN-ACK 丢失,服务端认为连接已建立,但客户端并不知道,会导致单向连接(服务端等待数据,客户端却认为连接未建立)。
三次握手解决历史连接问题:假如网络拥堵,客户端发送 SYN 报文后在特定时间内收不到报文触发超时重传,会再次发送 SYN 报文。假如客户端已经发送了多个 SYN 报文,目前收到了一个 SYN+ACK 报文。使用三次握手的机制,客户端有足够的上下文来判断这个报文是否属于历史连接:
  • 如果是历史连接(序列号过期或超时),则第三次握手发送的报文是 RST 报文,以此中止历史连接。
  • 如果不是历史连接,则第三次发送的报文是 ACK 报文,通信双方就会成功建立连接。
notion image
场景类比:
  • 两次握手:类似“你听到我说话吗?”→“我听到了!”但对方不确定你是否听到了他的回复。
  • 三次握手:完整对话:
    • A:“你听到我了吗?”(SYN)
    • B:“听到了!你听到我了吗?”(SYN-ACK)
    • A:“听到了!”(ACK)
    • 此时双方均确认通信正常。
四次握手:理论上四次握手(客户端 SYN → 服务端 SYN-ACK → 服务端 ACK → 客户端 ACK)也能实现可靠连接,但是三次握手已经足够建立可靠连接。原因是在第二次握手里面,服务器已经把确认客户端的序列号(服务端 SYN-ACK)发送服务端的序列号(服务端 ACK)合并为一个步骤,所以相当于四次握手简化为三次握手了,所以不需要使用更多的通信次数。

3.2 四次挥手过程

TCP 连接断开通过四次挥手完成。注意和三次挥手不同,三次挥手只能由客户端发起,而四次挥手双方都可以主动断开连接,断开连接后主机中的「资源」将被释放。
  1. 第一次挥手(FIN →)主动关闭方(客户端)发送 FIN=1 的报文,并进入 FIN_WAIT_1 状态,表示不再发送数据,但可以接收数据。
  1. 第二次挥手(ACK ←)
      • 被动关闭方(服务端)收到 FIN 后,回复 ACK 确认报文,进入 CLOSE_WAIT 状态。
      • 客户端收到 ACK 后进入 FIN_WAIT_2 状态,等待对方的 FIN 报文。
      • 此时连接处于半关闭(half-close)状态,服务端仍可发送未传完的数据。
  1. 第三次挥手(FIN ←):服务端完成数据发送后,发送 FIN=1 报文,进入 LAST_ACK 状态,等待最后的 ACK
  1. 第四次挥手(ACK →)
      • 客户端收到 FIN 后,发送 ACK 确认报文,进入 TIME_WAIT 状态(等待 2MSL 时间确保被动方收到 ACK)。
      • 服务端收到 ACK 后立即关闭连接,进入 CLOSED 状态。
      • 客户端在 2MSL 时间结束后关闭连接,,进入 CLOSED 状态。
notion image
💡
为什么挥手是四次?
TCP 的断开连接需要四次挥手是因为连接存在半关闭状态:一方发送 FIN 后仍有可能接收数据,发送 ACK 之后才会关闭连接。
  • 四次挥手:服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。所以服务端的 ACK 和 FIN 是分开发送的(在发送期间服务端仍可接收数据)。
  • 三次握手:服务器收到客户端的 SYN 报文时,把对客户端的响应报文(ACK 报文)和服务端的主动报文(SYN 报文)合并到一次请求发送给客户端。所以服务端的 ACK 和 SYN 是一起发送的。
从而导致四次挥手比三次握手多了一次请求。

3.3 TCP的保活机制

TCP 的保活机制(Keepalive)是一种用于检测连接是否存活的机制,主要用于管理长时间空闲的连接,防止因长时间空闲导致中间设备(如防火墙/NAT)丢弃连接。
触发条件:连接在tcp_keepalive_time(默认7200秒,即2小时)内无数据传输。
探测过程
  1. 发送保活探测包(空 ACK,序列号为对方期望值减 1)。
  1. 等待响应,超时时间为tcp_keepalive_intvl(默认75秒)。
  1. 若未收到响应,重试tcp_keepalive_probes次(默认9次)。
  1. 全部失败后,关闭连接。
涉及参数
  • tcp_keepalive_time:空闲多久后开始探测,默认值是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
  • tcp_keepalive_intvl:探测间隔(默认75秒)。
  • tcp_keepalive_probes:最大探测次数(默认9次)。表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以确认一个「死亡」连接。
TCP Keepalive 与 HTTP Keep-Alive 区别
  • TCP Keepalive:传输层机制,检测连接存活。
  • HTTP Keep-Alive:应用层机制,复用连接传输多个 HTTP 请求/响应。

4、重传机制

TCP 针对数据包丢失的情况,会用重传机制解决。常⻅的重传机制:
  • 超时重传:发送方为每个发出的数据包启动一个重传定时器,若在 RTO 时间内未收到 ACK 确认,则重传该数据包。
  • 快速重传:收到 3 个重复 ACK(DupACK)时,认为数据包丢失,立即重传而不等待超时。
  • 选择重传:接收方通过 SACK 选项告知发送方已成功接收的非连续数据块,发送方仅重传真正丢失的部分。

4.1 超时重传

超时重传是指为发送每个数据包设定一个定时器,当超过指定的时间后没有收到对方的 ACK 确认应答报文,就会重发该数据,
这种重传的概念是很简单的,但重传时间的选择却是 TCP 最复杂的问题之一。如果把超时重传时间设置得太短,就会引起很多报文段的不必要的重传,使网络负荷增大。但若把超时重传时间设置得过长,则又使网络的空闲时间增大,降低了传输效率。那么超时重传的时间应该设置多大呢?这涉及到超时重传的两个重要概念:
  • RTT (Round-Trip Time):往返时间,也就是从数据发出去到收到相应确认所需的时间
  • RTO (Retransmission Timeout):超时重传时间。
RTO 的值会影响重传机制的效率:
  • 当超时时间 RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差;
  • 当超时时间 RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
理论上超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。初始 RTO 通常为 1 秒(Linux默认值)。RFC 2988 建议使用下式计算 RTO:
notion image
RTTD 是 RTT 的偏差的加权平均值,它与RTTS和新的RTT样本之差有关。RFC 2988建议这样计算 RTTD。当第一次测量时,RTTD 值取为测量到的 RTT 样本值的一半。在以后的测量中,则使用下式计算加权平均的 RTTD:
notion image
实际上「报文往返 RTT 的值」是经常变化的,因为我们的网络也是时常变化的。也就因为「报文往返 RTT 的值」是经常波动变化的,所以「超时重传时间 RTO 的值」应该是一个动态变化的值。

4.2 快速重传

如果丢失的数据包都等超时时间到了才进行重传,有时候未免太慢了。所以还有另外一套重传机制:快速重传。
notion image
快速重传机制的核心思想就是:发送方当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。
  • 基于“冗余ACK”推断丢包(如期待 Seq=100 但收到 Seq=200 的 ACK,则重复发送三次 ACK Seq=100)。
  • 通常与选择性确认(SACK)结合,精确重传丢失的片段。

4.3 选择重传

快速重传虽然解决了重传缓慢的问题,但是面临重传什么包的问题。例如收到三个同样的ACK,只能说明这个 ACK 对应的包丢失了,但是并不知道这三个 ACK 是哪些发出去的包返回的,也就是不知道服务器已经收到了哪些包。选择重传是只重传真正丢失的数据包,而不是重传所有未确认的包。选择重传通常和快速重传结合使用。

4.3.1 SACK机制

选择重传基于SACK(Selective Acknowledgment)机制,作用是实现接收方明确告知发送方哪些数据块已经成功接收,使发送方只需重传真正丢失的数据包
选择性重传的工作过程:
  1. 协商阶段:在 TCP 三次握手时,双方通过 TCP 选项字段协商是否支持 SACK。SYN 包中带有 SACK permitted 选项表示支持该功能
  1. 选择性确认(SACK):接收方检测到数据包不连续时(出现空洞),在 ACK 报文中包含 SACK 选项,SACK 选项包含一个或多个已接收但未被确认的数据块边界。这样接收方可以明确告知发送方哪些数据块已经正确接收。
  1. 选择性重传:发送方根据 SACK 信息可以知道接收方哪些数据没收到,然后只重传真正丢失的数据段。
SACK 选项的格式:SACK 选项在 TCP 头部中通过 Kind=5 标识,格式如下:
  • Kind=5:标识 SACK 选项。
  • Length:选项的总长度(以字节为单位)。
  • Left/Right Edge:表示已接收的数据块的起始和结束序列号(不含右边界)。
示例:
notion image
  1. TCP 的接收方在接收对方发送过来的数据字节流的序号不连续,结果就形成了一些不连续的字节块(如上所示)。可以看出:序号 1~10001501~30003501~4500 的数据收到了,但序号 1001~15003001~3500 的数据没有收到。在这里每一个丢失的字节块都有两个边界:左边界和右边界。第一个字节块的左边界 L1 = 1501,右边界 R1 = 3001 (注意不是 3000 )。同理,第二个字节块的左边界 L2 = 3501,右边界 R2 = 4501
  1. 接收方触发快速重传机制,重传三次 ACK 报文,包含 SACK 选项,内容是[1501, 3001)[3501, 4501)
  1. 发送方通过 SACK 信息发现 [1501, 3001)[3501, 4501) 这两段数据丢失了,重发时就只选择了这两个 TCP 段的数据进行重发。

4.3.2 D-SACK机制

D-SACK(重复 SACK)是对 SACK 机制的扩展,使得扩展后的 SACK 可以告知发送方有哪些数据包自己重复接收了。引入 D-SACK 的目的是使 TCP 进行更好的流控:
  • 让发送方知道,是发送的包丢了,还是返回的 ACK 包丢了。
  • 网络上是否出现了包失序。
  • 数据包是否被网络上的路由器复制并转发了。
  • 是不是自己的 timeout 太小了,导致重传。
D-SACK 的规则如下:
  1. 第一个段将包含重复收到的报文段的序号
  1. 跟在 D-SACK 之后的 SACK 将按照 SACK 的方式工作
  1. 如果有多个被重复接收的报文段,则 D-SACK 只包含其中第一个
如何判断是普通 SACK 还是 D-SACK?D-SACK使用了SACK 的第一个段来作为判断标准:
  1. 如果 SACK 的第一个段的范围被 ACK 所覆盖,那么就是 D-SACK
  1. 如果 SACK 的第一个段的范围被 SACK 的第二个段覆盖,那么就是 D-SACK
例如:
1、满足第一个判断标准
notion image
由于ACK 4000大于[3000,3500],因此[4000, SACK=3000-3500]是D-SACK。发送端首先向接收端发送了3000-3499,3500-3999报文,接收端都收到了,但是接收端返回的ACK 3500及4000都丢失,导致发送端重传了3000-3499报文。接收端收到发送端重新发送的3000-3499报文,通过[4000,SACK=3000-3500]告知发送端,发送端就知道第一次的3000-3499报文接收端是收到了,由于当前ACK到了4000,那么4000之前的数据也都收到了。
2、满足第二个判断标准
notion image
由于第一个段[5000-5500]被第二个段[4500-5500]所覆盖,所以[4000, SACK=5000-5500, 4500-5500]是D-SACK,而前面的[4000, SACK=4500-5000]及[4000, SACK=4500-5500]都是普通的SACK。含义是4000前的包收到,5000-5499包重复收到,4500-5500的包都收到,4000-4499的包丢失。

5、流量控制

TCP 的流量控制(Flow Control)机制用于确保发送方不会以过快的速率发送数据,导致接收方缓冲区溢出。其核心是通过滑动窗口协议(Sliding Window Protocol)实现的。

5.1 滑动窗口

滑动窗口是指为发送方/接收方定义一个窗口,窗口里面的每一条数据无需等待上一条数据的确认应答就可以继续发送。TCP 头里有一个字段叫 Window,这个窗口是接收方的窗口大小,作用是接收方告诉发送方自己还有多少缓冲区可以接收数据,这样发送方就可以根据接收方的处理能力来发送数据,不会导致接收方处理不过来。
滑动窗口的作用:
  1. 避免接收方缓冲区溢出:控制发送方的数据量,使其不超过接收方的处理能力。
  1. 提高网络利用率:如果 TCP 每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了,再发送下一个,这种发送效率会很低。通过滑动窗口发送方无需等待应答就同时发送多个数据包,可以提高数据包的网络发送效率。
滑动窗口的类型:
  • 发送窗口(Send Window, SWND):发送方维护,包含以下三部分:
    • 已发送未确认:等待接收方 ACK 的数据。
    • 可发送未发送:允许立即发送的数据。
    • 不可发送:超出窗口范围的数据。
  • 接收窗口(Receive Window, RWND):接收方通过 TCP 头部的Window字段通告剩余缓冲区大小,直接影响发送窗口。
  • 拥塞窗口(Congestion Window, CWND):由拥塞控制算法动态调整,最终发送窗口取min(RWND, CWND),即拥塞窗口的大小是由接收方的窗口大小来决定的。

5.1.1 发送方的滑动窗口

notion image
发送方的滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。
  • SND.WND:表示发送窗口的大小,大小是由接收方指定的
  • SND.UNA:指向已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。是绝对指针。
  • SND.NXT:指向未发送但可发送的第一个字节的序列号,也就是 #3 的第一个 字节。绝对指针。
  • 指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移就可以指向 #4 的第一个字节了。
那么我们这里就有两个窗口:
  • 发送窗口:又称为通知窗口,发送窗口 = SND.WND
  • 可用窗口:又称为有效窗口,可用窗口大小 = SND.WND - (SND.NXT - SND.UNA)

5.1.2 接收方的滑动窗口

notion image
其中三个接收部分,使用两个指针进行划分:
  • RCV.WND:表示接收窗口的大小,它会通告给发送方。
  • RCV.NXT:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字 节。
  • 指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量就可以指向 #4 的第一个字节了。
💡
接收窗口和发送窗口的大小是相等的吗?
并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。
因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以 很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过 程是存在时延的,所以接收窗口和发送窗口是约等于的关系。

5.2 利用滑动窗口实现流量控制

  1. 初始化:接收方告知发送方自己的初始窗口大小(例如 RWND=400)。发送方根据 RWND 发送数据(如发送字节 1-100)。
  1. 数据发送与ACK
    1. 接收方收到数据后,返回ACK(如 ACK 101 表示字节 1-100 已接收)。
    2. 发送方收到 ACK 后滑动窗口,继续发送新数据(如字节 101-200)。
  1. 处理丢包或延迟:
    1. 接收方发现丢包情况,进行了三次流量控制。第一次把窗口减小到RWND=300,第二次又减到 RWND=100,最后减到 rwnd = 0,即不允许发送方再发送数据了。
    2. 发送方将暂停发送新的数据包,通过超时重传或快速重传(结合选择性确认 SACK)机制重传未确认的数据包。直到接收方重新发出一个新的窗口值(例如RWND=100),发送方才会开始发送新的数据包。
notion image

5.3 零窗口和窗口探测

TCP 零窗口是指接收方通过 TCP 窗口大小 Window 字段告知发送方其接收缓冲区已满,暂时无法接收更多数据的状态
零窗口的工作原理:
  1. 当接收方的 TCP 接收缓冲区满时,会在 ACK 报文中将窗口大小设置为 0
  1. 发送方收到零窗口通知后必须停止发送数据,防止接收方被流量淹没。
当发送方收到零窗口通知后,会启动窗口探测机制来检测接收方窗口状态是否变化。
  1. 发送方收到零窗口通知后启动一个持续定时器,定时器到期后,发送方发送一个 1 字节的窗口探测 (Window probe) 报文
  1. 接收方处理窗口探测报文:
    1. 如果接收方仍处于零窗口状态,会再次回复 ACK(0)
    2. 如果接收方缓冲区已有空间,会回复自己现在的接收窗口大小。
窗口探测机制的作用:
  • 防止窗口死锁:如果没有窗口探测机制,接收方的接收缓冲区有可用空间后,发送的第一个包含非零窗口大小的 ACK 报文如果在网络中丢失了,那就会造成死锁:发送方一直等待非零窗口通知,而接收方一直等待发送方发送数据。
notion image

5.4 Nagle算法和延迟确认

虽然滑动窗口可以发送多个数据包提高了通信效率,但是如果频繁的进行 TCP 小包通信,通信效率还是非常低下的。Nagle 算法延迟确认(Delayed ACK)是 TCP 协议中用于提高网络效率的机制。
  • Nagle 算法:TCP 发送方使用的优化策略,当发送方有少量数据要发送(小于MSS)时,如果之前发送的数据尚未被确认,则暂存这些数据。直到收到之前数据的 ACK,或者积累的数据达到 MSS 大小,才发送出去。
  • 延迟确认(Delayed ACK):当接收方收到数据包时,不会立即发送 ACK 确认,而是等待一段时间(通常 200-500ms)再发送 ACK 确认。接收方可以把这段时间内产生的要发送的数据累积起来一起发送给发送方。
💡
注意 Nagle 算法和延迟确认不能同时开启。
当延迟确认和 Nagle 算法同时启用时,会导致性能下降:
  1. 发送方启用 Nagle 算法后,先发出一个小报文。
  1. 接收方启用延迟确认机制后,在收到小报文的时候,不会立刻回复 ACK 报文,只能干等着发送方的下一个报文到达。发送方由于 Nagle 算法机制,在未收到第一个报文的确认前,是不会发送后续的数据。所以接收方只能等待最大时间 200ms 后,才回 ACK 报文。
  1. 发送方收到第一个报文的 ACK 报文后,才可以发送后续的数据。
    1. notion image

5.4.1 Nagle算法

Nagle 算法的工作原理:若发送应用进程把要发送的数据逐个字节地送到 TCP 的发送缓存,则发送方先把第一个数据字节先发送出去,把后面到达的数据字节都缓存起来。当发送方在收到「对前一个报文段的确认」或「数据⻓度达到 MSS 大小」时后才继续发送下一个报文段。
notion image
右侧启用了 Nagle 算法,它的发送数据的过程:
  1. 一开始由于没有已发送未确认的报文,所以就立刻发了 H 字符。可以看出,Nagle 算法一定会有一个小报文,也就是在最开始的时候。
  1. 接着,在还没收到对 H 字符的确认报文时,发送方就一直在囤积数据,直到收到了确认报文后,此时没有已发送未确认的报文,于是就把囤积后的 ELL 字符一起发给了接收方。
  1. 等收到对 ELL 字符的确认报文后,于是把最后一个 O 字符发送了出去。
Nagle 算法默认是打开的。Nagle 的适用场景:
  • 大数据量传输:例如文件传输。
  • 对延迟不敏感的应用。
需要禁用 Nagle 算法的场景(在 Socket 设置 TCP_NODELAY 选项来关闭):
  • 交互式应用:如 SSH、Telnet。
  • 实时性要求高的应用:如在线游戏、视频会议。

5.4.2 延迟确认

延迟确认的工作原理:当接收方收到数据包时,不会立即发送 ACK 确认,而是等待一段时间(通常200-500ms)
  • 如果在这段时间内有响应数据要发送时,响应数据会随着 ACK 报文一起立刻发送给对方;或者对方的第二个数据报文到达了,也会立刻发送 ACK 报文。
  • 不满足以上发送条件时,接收方会等到时间超时后单独发送 ACK 报文。
延迟确认的作用:
  • 将多个小数据包的 ACK 合并为一个,提高网络利用率。
notion image

6、拥塞控制

拥塞控制的作用防止过多的数据注入到网络中,导致网络中的路由器或链路过载。在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环,所以就出现了拥塞控制。
拥塞控制的核心算法:
  • 慢启动(Slow Start):初始阶段快速探测可用带宽。
  • 拥塞避免(Congestion Avoidance):避免因窗口过大导致网络拥塞。
  • 快速重传(Fast Retransmit):避免等待超时,快速修复丢包。
  • 快速恢复(Fast Recovery):避免因单个丢包导致窗口骤降。
TCP 的拥塞控制算法的版本演进:
  • Tahoe:TCP 的最早版本,只包含慢启动和拥塞避免,丢包后 cwnd 重置为1。
  • Reno:在 Tahoe 算法的基础上,增加快速重传和快速恢复算法,目前使用最广泛的算法。
  • Newreno:基于 Reno 的改进版本,主要是改进了快速恢复算法,处理多个丢包场景。
💡
拥塞控制和流量控制的区别
拥塞控制与流量控制的关系密切,它们之间也存在着一些差别。
  • 拥塞控制是一个全局性的过程,涉及到所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。
  • 流量控制往往指点对点通信量的控制,是个端到端的问题(接收端控制发送端)。流量控制所要做的就是抑制发送端发送数据的速率,以便使接收端来得及接收。
可以用个简单例子说明这种区别。
  • 设某个光纤网络的链路传输速率为 1000Gb/s。有一个巨型计算机向一个 PC 机以 1Gb/s 的速率传送文件。显然,网络本身的带宽是足够大的,因而不存在产生拥塞的问题。但流量控制却是必需的,因为巨型计算机必须经常停下来,以便使 PC 机来得及接收。
  • 但如果有另一个网络,其链路传输速率为 1Mb/s,而有 1000 台大型计算机连接在这个网络上。假定其中的 500 台计算机分别向其余的 500 台计算机以 100kb/s 的速率发送文件。那么现在的问题已不是接收端的大型计算机是否来得及接收,而是整个网络的输入负载是否超过网络所能承受的。

6.1 慢启动

拥塞机制定义了几个重要的概念:
  • 拥塞窗口(cwnd:用于动态调整发送方在网络中能发送但未被确认的数据量。只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。一般来说,swnd = min(cwnd, rwnd) ,也就是说发送窗口是拥塞窗口和接收窗口中的最小值。
  • 慢启动阈值(ssthresh:作用是防止拥塞窗口 cwnd 增长过大引起网络拥塞。一般来说 ssthresh 的大小是 65535 字节。并规定:
    • cwnd < ssthresh 时,使用「慢启动算法」。
    • cwnd > ssthresh 时,就会使用「拥塞避免算法」。
    • cwnd = ssthresh 时,既可使用「慢启动算法」,也可使用「拥塞避免算法」。
慢启动(Slow Start)算法的目的是初始阶段快速探测可用带宽。当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么就有可能引起网络拥塞,因为现在并不清楚网络的负荷情况。经验证明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口。慢启动算法的工作过程:
  1. 先把拥塞窗口 cwnd 设置为 1MSS(最大报文段长度)的大小。
  1. 每当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1,直到 cwnd 大小达到 ssthresh
notion image
如上所示,连接建立完成后,一开始初始化 cwnd = 1 ,表示可以传一个 MSS 大小的数据。
  • 当收到一个 ACK 确认应答后,cwnd 增加 1,于是一次能够发送 2 个
  • 当收到 2 个的 ACK 确认应答后, cwnd 增加 2,于是就可以比之前多发 2 个,所以这一次能够发送 4 个
  • 当这 4 个的 ACK 确认到来的时候,每个确认 cwnd 增加 1, 4 个确认 cwnd 增加 4,于是就可以比之前多发 4 个,所以这一次能够发送 8 个。
由此可以看出慢启动算法发包的个数是指数性的增⻓

6.2 拥塞避免

当拥塞窗口 cwnd 超过慢启动⻔限 ssthresh 就会进入拥塞避免算法。拥塞避免算法的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。换而言之,当收到 cwnd 数量的 ACK 时,cwnd 的大小就会加 1。
notion image
  • 因为 ssthresh 为 8,当 8 个 ACK 应答确认到来时,每个确认增加 1/8,8 个 ACK 确认 cwnd 一共增加 1,于是这一次能够发送 9个 MSS 大小的数据,变成了线性增⻓。
由此可见,拥塞避免算法就是将原本慢启动算法的指数增⻓变成了线性增⻓。
拥塞避免算法虽然把发送包的增长速度变成了线性增长,但毕竟还在增⻓阶段。就这么一直增⻓着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。在 Tahoe 版本的 TCP 拥塞控制算法里面,当拥塞避免阶段发送方判断网络出现拥塞的时候,就会回退为慢启动算法。具体过程:
  1. 当发送方判断网络出现拥塞(判断依据是 RTO 超时还没收到 ACK 确认),设置 ssthresh = cwnd/2cwnd 重置为 1。
  1. 发送方进入慢启动算法,
这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。
notion image

6.3 快速重传和快速恢复

Tahoe 版本的 TCP 拥塞控制算法从拥塞控制算法突然降级到慢启动算法会突然减少数据流,导致网络剧烈抖动,这种方式太激进了,并不推荐日常使用。所以 Reno 版本提出了更温和的快速重传+快速恢复算法。
  1. 快速重传算法:当接收方发现丢了一个报文的时候,发送三次该报文的前一个报文段的 ACK。发送方只要收到三个重复 ACK 就应当立即重传对方尚未收到的报文,而不必继续等待为该报文设置的重传计时器到期。
    1. notion image
      如上所示,接收方收到了 M1 和 M2 后都分别发出了确认。现假定接收方没有收到 M3 但接着收到了 M4。显然,接收方不能确认 M4,因为 M4 是收到的失序报文段(按照顺序的M3还没有收到)。接收方应及时发送对 M2 的重复确认,连续发送三次,这样做可以让发送方及早知道报文段 M3 没有到达接收方。发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段 M3,而不必继续等待为 M3 设置的重传计时器到期。由于发送方能尽早重传未被确认的报文段,因此采用快重传后可以使整个网络的吞吐量提高约 20%。
  1. 快速恢复算法:通常会搭配快速重传算法使用,作用是避免因单个丢包导致窗口骤降。快速恢复算法认为,你还能收到 3 个重复的 ACK 说明网络也不那么糟糕(如果网络发生了严重的拥塞,就不会一连有好几个报文段连续到达接收方,也就不会导致接收方连续发送重复确认),没必要像超时重传那么激烈。所以快速恢复算法不像慢启动算法一样直接把拥塞窗口 cwnd 设置为 1,而是把 cwnd 值设置为慢开始门限 ssthresh 减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。步骤如下:
    1. ssthresh = cwnd/2
    2. cwnd = ssthresh + 3(这里的 ssthresh 是第一步的计算结果,3 是指快速重传收到的三个重复的 ACK),进入快速恢复算法。
    3. 如果收到重复数据的 ACK,那么 cwnd 增加 1。
    4. 如果收到新数据的 ACK,把 cwnd 设置为 ssthresh 最开始的值,然后再次进入拥塞避免算法。原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的拥塞避免状态了。
    5. notion image
在采用快速恢复算法时,慢开始算法只是在 TCP 连接建立时和网络出现超时时才使用。采用这样的拥塞控制方法使得 TCP 的性能有明显的改进。

6.5 拥塞避免算法过程

notion image
  1. 当新建连接时,进入慢启动算法。cwnd 初始化为 1MSS 大小,发送端开始按照拥塞窗口大小发送数据。每当有一个报文段被确认时,cwnd 的大小加 1。这样 cwnd 的值就随着网络往返时间(RTT)呈指数增长
  1. cwnd 超过慢启动门限 ssthresh 后,慢启动过程结束,进入拥塞避免算法。每当 cwnd 数量报文段被确认时,cwnd 的大小加 1cwnd 的值就随着 RTT 呈线性增长
  1. 当检测到拥塞状态(判断拥塞的依据是 TCP 对每一个报文段都有一个定时器,称为重传定时器(RTO),当 RTO 超时且还没有得到 ACK 报文,或者连续三次收到相同的 ACK 报文,就判断出现了拥塞情况),进入快读重传算法,立刻连续发送三次丢失的报文段的前一个报文段的 ACK,然后进入快速恢复算法。
  1. 在快速恢复算法阶段,把ssthresh 的值减半,然后 cwnd 设置为 ssthresh 新值的一半加 3。每当收到重复数据包的 ACK 时,cwnd 增加 1。当收到新数据包的 ACK 时,把 cwnd 设置为 ssthresh 的原始值,结束快速恢复算法,再次进入拥塞避免算法。

常见问题

TCP为什么是可靠的协议

TCP 主要通过以下机制确保数据的可靠传输:
  • 面向连接:通过三次握手和四次挥手保证连接是可靠的。
  • ACK 应答机制:基于序列号的确认应答机制,保证接收方最终接收到数据和数据包的有序性。
  • 重传机制:对于没有收到数据包,提供超时重传、快速重传、选择重传等机制。
  • 流量控制:为了协调发送方的发送包和接收方的接收包的速度,提供基于滑动窗口的流量控制机制。
  • 拥塞控制:为了维护网络环境的稳定,提供拥塞控制机制。
可靠性体现的场景:
  • 丢包:通过重传机制恢复。
  • 乱序:通过序列号重新排序。
  • 拥塞:通过拥塞控制避免网络瘫痪。
  • 错误:通过校验和检测损坏数据。

拆包粘包问题

TCP 粘包是指发送方发送的若干数据包到达接收方时粘成一个包的现象。从接收缓冲区看,后一个数据包的头紧接着前一个数据包的尾。
问题原因
这是由 TCP 协议的特性决定:
  • 面向字节流:TCP 是面向字节流的协议,没有消息边界。
  • 拆包:为了避免 IP 分片,TCP 会将字节流数据拆分为多个不超过 MSS 大小的数据段。
  • 粘包:为提高效率,Nagle 算法会合并小数据包
解决方案是应用层自定义消息边界,有以下几种解决方案:
  1. 固定长度消息:每条消息都采用固定长度,不足部分用特定字符填充。实现简单,但浪费带宽。
  1. 特殊分隔符:在消息末尾添加特殊分隔符(如\n\r\n等)。例如:
    1. 消息头+消息体:在消息前添加固定长度的消息头,在消息头里面指明消息体的长度。推荐使用。

      泛洪(SYN)攻击(三次握手)

      在介绍泛洪攻击前先介绍半连接队列(SYN Queue)全连接队列(Accept Queue),这是 TCP 三次握手过程中,服务端的 Linux 内核用于管理连接请求的两个关键数据结构:
      1、半连接队列(SYN 队列)
      • 作用:当服务器收到客户端的 SYN 包后,会发送 SYN+ACK 包,内核将该连接放入半连接队列。简单来说就是用来存储客户端发送了 SYN 包但尚未完成三次握手的连接(即处于 SYN_RECEIVED 状态的连接)。
      • 队列溢出:如果队列已满且新 SYN 到达,服务器可能会丢弃 SYN 包(或返回 RST)。这是 SYN Flood 攻击的利用点:攻击者发送大量 SYN 但不回复 ACK,耗尽队列资源。
      • 相关配置(Linux):
        • net.ipv4.tcp_max_syn_backlog:控制半连接队列的最大长度。
        • net.ipv4.tcp_syncookies:启用 SYN Cookie 机制(默认值为 1),可在队列满时防御 SYN Flood。
      2、全连接队列(Accept 队列)
      • 作用:当服务器收到客户端的 ACK(第三次握手)后,内核将连接从半连接队列移到全连接队列,然后等待应用层调用 accept() 时会从全连接队列中取出连接。用于存储已完成三次握手但尚未被应用层调用 accept() 取走的连接(即处于 ESTABLISHED 状态的连接)。
      • 队列溢出:如果队列已满且新连接完成握手,服务器可能会忽略客户端的 ACK(导致客户端认为连接已建立,但服务器实际丢弃)。客户端可能会重传 ACK,或服务器发送 RST(取决于实现)。
      • 相关配置(Linux):
        • net.core.somaxconn:定义全连接队列的最大长度(默认值通常为 128)。
        • 应用层监听时的 backlog 参数(如 listen(fd, backlog)),最终队列长度为 min(backlog, somaxconn)
      notion image
      SYN 攻击是一种常见的拒绝服务攻击(DoS/DDoS)类型,它利用了 TCP 协议的三次握手过程中的漏洞。
      1. 正常TCP三次握手
          • 客户端发送 SYN 包到服务器
          • 服务器回应 SYN-ACK 包
          • 客户端发送 ACK 包,连接建立
      1. 攻击方式
          • 攻击者发送大量伪造源 IP 的 SYN 包到目标服务器
          • 服务器为每个 SYN 包分配资源,把该连接放入半连接队列,并发送 SYN-ACK 响应
          • 由于源 IP 是伪造的,不会有 ACK 响应返回
          • 服务器半连接队列被大量半开连接占用甚至导致队列溢出,无法服务端无法建立正常的连接
      解决方案:
      • 启用 SYN Cookies:设置 net.ipv4.tcp_syncookies = 1 ,内核会在检测到可能的 SYN Flood 攻击(半连接队列满时)自动启用 SYN Cookie 机制。核心思想是不存储半开状态的连接,而是通过加密算法生成一个特殊的 SYN Cookie 作为初始序列号(ISN)嵌入在 SYN-ACK 中。客户端返回的 ACK 必须包含正确的 Cookie 值,服务器才会真正分配连接资源。具体步骤:
          1. 客户端发送 SYN:攻击者或正常客户端发送 SYN 报文。
          1. 服务器生成 SYN Cookie:服务器通过 哈希算法(基于源/目标 IP+端口、时间戳、MSS 等)计算一个 Cookie 值,作为 SYN-ACK 的序列号(ISN) 返回给客户端。服务器不记录这个 SYN 请求,节省了内存。
          1. 合法客户端回复 ACK
              • 正常客户端会返回 ACK,其中的确认号(ACK number) = ISN(Cookie) + 1。
              • 服务器收到 ACK 后,重新计算 Cookie 值,验证是否匹配。
              • 如果匹配,则分配连接资源(进入 ESTABLISHED 状态);否则丢弃。
          1. 攻击者的 SYN 无法通过验证
              • 攻击者通常不会完成握手(不发送 ACK),或者发送的 ACK 无法通过 Cookie 验证(因为伪造的 IP/端口无法生成正确的 Cookie)。
              • 服务器直接丢弃无效请求,避免资源耗尽。
      • 增加最大半开连接数。调整 net.ipv4.tcp_max_syn_backlog 参数,默认值是 1024 或 2048(受系统版本影响),可以适当增加该参数。

        TIME_WAIT问题(四次挥手)

        💡
        为什么需要TIME_WAIT?
        主动发起关闭连接的一方,才会有 TIME-WAIT 状态。需要 TIME-WAIT 状态的原因:
        • 让网络中残留的旧报文失效,避免影响新连接。如果 TIME-WAIT 过短或者没有,假如网络拥堵,关闭本次连接再重新打开连接之后,旧连接的数据包才到达,就会造成数据混乱。而足够长的 TIME-WAIT 足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的
          • notion image
        • 保证最后一个 ACK 报文能够到达被动关闭方。四次挥手的最后一个 ACK 报文如果在网络中被丢失了,此时如果客户端 TIME-WAIT 过短或没有,则就直接进入了 CLOSED 状态了,那么服务端则会一直处在 LASE_ACK 状态,后面客户端和服务端就不能正常建立连接。而足够长的 TIME-WAIT 可以保证。服务端没有收到四次挥手的最后一个 ACK 报文时,则会重发 FIN 关闭连接报文并等待新的 ACK 报文,从而帮助双方都正确关闭。
          • notion image
        💡
        为什么TIME_WAIT的时间是2MSL?
        MSL(Maximum Segment Lifetime)是报文最大生存时间,是指报文在网络上存在的最⻓时间,超过这个时间报文将被丢弃。默认是 30s。在 Linux 系统中,TIME_WAIT 的默认持续时间是 2MSL,即 60s。
        TIME_WAIT设置为 2MSL 的原因:网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。设置 2MSL 的时间,可以保证发送方发出去的数据都收到响应。
        2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端发的 FIN 报文,那么 2MSL 时间将重新计时。
        💡
        TIME_WAIT过多的危害和优化
        发送方系统中存在过多的 TIME-WAIT 状态的 TCP 连接会占用有限端口资源,导致无法建立新连接。一般可以开启的端口为 32768~61000
        解决方案:
        • 启用 TIME_WAIT 复用:复用处于 TIME_WAIT 的 socket 为新的连接所用。客户端在调用 connect() 函数时,内核会随机找一个 TIME_WAIT 状态超过 1 秒的连接给新的连接复用。
          • 减小 TIME_WAIT 超时时间(默认60秒)
            • 增大可用端口范围
              计算机网络系列(四):网络层(IP)计算机网络系列(二):应用层(HTTP)
              Loading...