type
status
date
slug
summary
tags
category
icon
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 则提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。
给出了一些应用层协议主要使用的运输层协议(UDP或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层)最大一次传输数据的大小。默认值是 1500 字节。如果 IP 层有小于等于 MTU 大小的数据需要发送,只需要一个 IP 包就可以完成发送任务;当 IP 层有一个超过 MTU 大小的数据要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU,这样才能完成数据的发送。
  • MSS:Maximum Segment Size ,最大报文段长度,即 TCP 提交给 IP 层最大报文大小,这个报文大小是除去 IP 和 TCP 头部之后的数据的最大⻓度
一般来说,MTU = MSS + IP 头部长度 + TCP 头部长度
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 报文的所有分片都得重传。为了只重传丢失的数据,我们会在 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 三次握手过程

假定主机 A 运行的是TCP客户程序,而 B 运行 TCP 服务器程序。最初两端的 TCP 进程都处于CLOSED(关闭)状态。图中在主机下面的方框分别是 TCP 进程所处的状态。请注意,A 主动打开连接,而 B 被动打开连接。
notion image
  • 一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。
notion image
  • 客户端会随机初始化序号( client_isn ),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志 位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
notion image
  • 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号( server_isn ),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1 , 接着把 SYNACK 标志位置为 1 。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
notion image
  • 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位 置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客 户到服务器的数据,之后客户端处于 ESTABLISHED 状态。
  • 服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。
一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送 数据了。
注意:第三次握手是可以携带数据的,前两次握手是不可以携带数据的。
💡
为什么握手是三次,不是两次,四次?
三次握手的主要原因:防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
notion image
假如网络拥堵,客户端发送的 SYN 报文,或者服务器返回的 SYN+ACK 报文都有可能发生拥堵或者丢失,客户端特定时间内收不到报文触发超时重传,会再次发送SYN报文。
假如客户端已经发送了多个 SYN 报文,目前收到了一个 SYN+ACK 报文。使用三次握手的机制,客户端有足够的上下文来判断这个报文是否属于历史连接:
  • 如果是历史连接(序列号过期或超时),则第三次握手发送的报文是 RST 报文,以此中止历史连接。
  • 如果不是历史连接,则第三次发送的报文是 ACK 报文,通信双方就会成功建立连接。
为什么不是两次?
两次握手连接不能判断当前连接是否是历史连接。假如双方正常通信已经完成了,过了一段时间后客户端突然收到一个 SYN+ACK 旧报文。如果是两次握手的话这时候双方会建立起通信,但事实上双方通信已经完成了,这个报文属于脏数据,这次通信不应该被建立。
为什么不是四次?
在第二次握手里面,服务器已经把确认客户端序列号发送自己的序列号合并为一个步骤,所以相当于四次握手简化为三次握手了。
在第三次握手之后,就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

3.2 半连接队列和全连接队列

在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
  • 半连接队列,也称 SYN 队列
  • 全连接队列,也称 accepet 队列
服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来
不管是半连接队列还是全连接队列,都有最大⻓度限制,超过限制时,内核会直接丢弃,或返回 RST 包。
notion image

3.3 SYN攻击

假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入 SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的 SYN 接收队列,使得服务器不能为正常用户服务。
避免SYN攻击的策略:启用 net.ipv4.tcp_syncookies 参数。
notion image
  • 当 「 SYN 队列」满之后,后续服务器收到 SYN 包,不进入「 SYN 队列」。
  • 计算出一个 cookie 值,再以 SYN + ACK 中的「序列号」返回客户端。
  • 服务端接收到客户端的应答报文时,服务器会检查这个 ACK 包的合法性。如果合法,直接放入到「 Accept 队列」。
  • 最后应用通过调用 accpet() socket 接口,从「 Accept 队列」取出的连接。

3.4 TCP的保活机制

已经建立好的 TCP 连接,如果长时间没有数据交互,就会触发保活机制。这个机制的原理是这样的:
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:
  • tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
  • tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
  • tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。

4、四次挥手

tcp连接断开通过四次挥手完成。注意和三次挥手不同,三次挥手只能由客户端发起,而四次挥手双方都可以主动断开连接,断开连接后主机中的「资源」将被释放。
notion image
  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。服务器进程这时应通知高层应用进程,因而A → B 这个方向的连接就释放了,这时的 TCP 连接处于半关闭(half-close)状态,即 A 已经没有数据要发送了,但 B 若发送数据,A 仍要接收。
  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态,等待服务端发出的连接释放报文段。
  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态,等待客户端的确认。
  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态。
  • 服务器收到了 ACK 应答报文后,就进入了 CLOSED 状态,至此服务端已经完成连接的关闭。
  • 客户端在经过 2MSL 时间后,自动进入 CLOSED 状态,至此客户端也完成连接的关闭。
💡
为什么挥手是四次?
  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
  • 服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
所以每个方向都需要一个 FIN 和一个 ACK,而服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手导致多了一次。
💡
为什么需要TIME_WAIT?
主动发起关闭连接的一方,才会有 TIME-WAIT 状态。需要 TIME-WAIT 状态的原因:
  • 防止本次连接旧的已经失效的报文出现在下次连接。如果 TIME-WAIT 过短或者没有,假如网络拥堵,关闭本次连接再重新打开连接之后,旧连接的数据包才到达,就会造成数据混乱。而足够长的 TIME-WAIT 足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的
    • notion image
  • 保证客户端发送的四次挥手的最后一个报文能够到达服务端。四次挥手的最后一个 ACK 报文如果在网络中被丢失了,此时如果客户端 TIME-WAIT 过短或没有,则就直接进入了 CLOSED 状态了,那么服务端则会一直处在 LASE_ACK 状态,后面客户端和服务端就不能正常建立连接。而足够长的 TIME-WAIT 可以保证服务端没有收到四次挥手的最后一个 ACK 报文时,则会重发 FIN 关闭连接报文并等待新的 ACK 报文,从而帮助双方都正确关闭
    • notion image
💡
为什么TIME_WAIT的时间是2MSL?
MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最⻓时间,超过这个时 间报文将被丢弃。在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时 间为固定的 60 秒。
为什么TIME_WAIT的时间是2MSL?比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。设置2MSL的时间,可以保证发送方发出去的数据都收到响应。
2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端􏰁发的 FIN 报文,那么 2MSL 时间将重新计时。
💡
TIME_WAIT过多的危害和优化
发送方过多的 TIME-WAIT 状态的 TCP 连接会占用有限的端口资源,一般可以开启的端口为 32768~61000。如果发起连接一方的 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。
优化 TIME_WAIT 的方法:
  • 复用处于 TIME_WAIT 的 socket 为新的连接所用。客户端在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。使用 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 参数。

5、重传机制

TCP 针对数据包丢失的情况,会用重传机制解决。
常⻅的重传机制:
  • 超时重传
  • 快速重传
  • 选择重传

5.1 超时重传

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

5.2 快速重传

如果丢失的数据包都等超时时间到了才进行重传,有时候未免太慢了。所以还有另外一套重传机制:快速重传
notion image
快速重传机制的核心思想就是:发送方当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段

5.3 选择重传

快速重传虽然解决了重传缓慢的问题,但是面临重传什么包的问题。例如收到三个同样的ACK,只能说明这个ACK对应的包丢失了,但是并不知道这三个ACK是哪些发出去的包返回的,也就是不知道服务器已经收到了哪些包。在快速重传的基础上,返回最近收到的报文段的序列号范围,这样客户端就知道哪些数据包已经到达服务器了,这就是选择重传。所以选择重传可以看做快速重传的一种优化处理。
例如 TCP 的接收方在接收对方发送过来的数据字节流的序号不连续,结果就形成了一些不连续的字节块(如图5-21所示)。可以看出,序号 1~1000 收到了,但序号 1001~1500 没有收到。接下来的字节流又收到了,可是又缺少了 3001~3500。再后面从序号 4501 起又没有收到。也就是说,接收方收到了和前面的字节流不连续的两个字节块。如果这些字节的序号都在接收窗口之内,那么接收方就先收下这些数据,但要把这些信息准确地告诉发送方,使发送方不要再重复发送这些已收到的数据
notion image
从图 5-21 可看出,和前后字节不连续的每一个字节块都有两个边界:左边界和右边界。因此在图中用四个指针标记这些边界。请注意,第一个字节块的左边界 L1 = 1501,但右边界 R1 = 3001 而不是 3000。这就是说,左边界指出字节块的第一个字节的序号,但右边界减 1 才是字节块中的最后一个序号。同理,第二个字节块的左边界 L2 = 3501,而右边界 R2 = 4501

5.3.1 SACK机制

选择重传基于 SACK(Selective Acknowledgment) 机制。在建立 TCP 连接时,在 TCP 首部的选项中加上 SACK 的选项,而双方必须都事先商定好。然后在 SACK 字段里面报告收到的不连续的字节块的边界,这样发送方就可以知道哪些数据没收到,然后就可以只重传丢失的数据。
上面这个场景,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重传机制,通过 SACK 信息发现 [1501, 3001)[3501, 4501) 这两段数据丢失了,重发时就只选择了这两个 TCP 段的数据进行重发。

5.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的包丢失。

6、流量控制

发送方不能无脑的发数据给接收方,要考虑接收方处理能力。
如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。
为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制
流量控制基于滑动窗口

6.1 滑动窗口

如果 TCP 每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了,再发送下一个,这种发送效率会很低。所以我们可以定义一个窗口,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值,这就是滑动窗口
TCP 头里有一个字段叫 Window,也就是窗口大小。 这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
所以,通常窗口的大小是由接收方的窗口大小来决定的。发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。

6.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)

6.1.2 接收方的滑动窗口

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

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

notion image
接收方的主机 B 进行了三次流量控制。第一次把窗口减小到 rwnd = 300,第二次又减到 rwnd = 100,最后减到 rwnd = 0,即不允许发送方再发送数据了。这种使发送方暂停发送的状态将持续到主机 B 重新发出一个新的窗口值为止。我们还应注意到,B 向 A 发送的三个报文段都设置了 ACK = 1,只有在 ACK = 1 时确认号字段才有意义。

6.3 零窗口和窗口探测

发送方和接收方分别有两个缓冲区,一个是 buffer,指的是内核缓冲区,另外一个是 application,指的是应用层的缓冲区。对于发送方来说,当要发送数据的时候,需要从 application 拷贝数据到 buffer,而对于接收方来说,当接收到数据的时候,会先缓存到 buffer,然后再由应用程序从 buffer 读取数据。
当接收方的 buffer 堆积了大量数据但 application 还没来得及读取,最终导致 buffer 满了,这时接收方就会给发送方通报一个接收窗口为0的报文,表示不能再接收数据,这就是零窗口
当接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,那就会造成死锁:
  • 发送方一直等待非 0 窗口通知。
  • 接收方一直等待发送方发送数据。
为了解决死锁问题,TCP 为每个连接设有一个持续定时器,只要发送方收到对方的零窗口通知,就启动持续计时器。如果持续计时器超时,就会发送窗口探测 (Window probe) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
notion image
  • 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器。
  • 如果接收窗口不是 0,那么死锁的局面就可以被打破了。

6.4 延迟确认和Nagle算法

虽然滑动窗口可以发送多个数据包提高了通信效率,但是如果频繁的进行 TCP 小包通信, 通信效率还是是非常低下的。为了提高网络传输效率;
  • 对于发送方来说可以使用 Nagle算法
  • 对于接收方来说可以使用 延迟确认

6.4.1 Nagle算法

Nagle 算法的策略:若发送应用进程把要发送的数据逐个字节地送到 TCP 的发送缓存,则发送方就把第一个数据字节先发送出去,把后面到达的数据字节都缓存起来。当发送方收到对第一个数据字符的确认后,再把发送缓存中的所有数据组装成一个报文段发送出去,同时继续对随后到达的数据进行缓存。只有在收到「对前一个报文段的确认」或「数据⻓度达到 MSS 大小」时后才继续发送下一个报文段
notion image
右侧启用了 Nagle 算法,它的发送数据的过程:
  • 一开始由于没有已发送未确认的报文,所以就立刻发了 H 字符。
  • 接着,在还没收到对 H 字符的确认报文时,发送方就一直在囤积数据,直到收到了确认报文后,此时没有已发送未确认的报文,于是就把囤积后的 ELL 字符一起发给了接收方。
  • 等收到对 ELL 字符的确认报文后,于是把最后一个 O 字符发送了出去。
可以看出,Nagle 算法一定会有一个小报文,也就是在最开始的时候。
Nagle 算法默认是打开的,数据到达较快而网络速率较慢时,用这样的方法可明显地减少所用的网络带宽。对于一些需要小数据包交互的场景的程序,比如 telnet 或 ssh 这样的交互 性比较强的程序,则需要关闭 Nagle 算法。在 Socket 设置 TCP_NODELAY 选项来关闭这个算法。

6.4.2 延迟确认

如果接收方每次一收到数据就回复没有携带数据的 ACK,它的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报文。为了提高效率就需要接收方延迟确认
TCP 延迟确认的策略:
  • 当有响应数据要发送时,ACK 会随着响应数据一起立刻发送给对方。
  • 当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应数据可以一起发送
  • 如果在延迟等待发送 ACK 期间,对方的第二个数据报文又到达了,这时就会立刻发送 ACK
    • notion image
💡
Nagle 算法和延迟确认不能同时开启。
当 TCP 延迟确认 和 Nagle 算法混合使用时,会导致时耗增⻓。
  • 发送方先发出一个小报文,接收方收到后,由于延迟确认机制,自己又没有要发送的数据,只能干等着发送方的下一个报文到达;
  • 而发送方由于 Nagle 算法机制,在未收到第一个报文的确认前,是不会发送后续的数据。所以接收方只能等待最大时间 200 ms 后,才回 ACK 报文,发送方收到第一个报文的确认报文后,也才可以发送后续的数据。
    • notion image

7、拥塞控制

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

7.1 慢启动

为了在「发送方」调节所要发送数据的量,定义了一个叫做拥塞窗口的概念。拥塞窗口 cwnd 是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的,拥塞窗口变化的原则是:只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数
拥塞窗口 cwnd 、发送窗口 swnd 和接收窗口 rwnd 的关系:swnd = min(cwnd, rwnd),也就是说发送窗口是拥塞窗口和接收窗口中的最小值。但是 rwnd 是由对端确定的,网络环境对其没有影响,所以在考虑拥塞的时候我们一般不考虑 rwnd 的值。
慢开始算法的思路是这样的。当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么就有可能引起网络拥塞,因为现在并不清楚网络的负荷情况。经验证明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。通常在刚刚开始发送报文段时,先把拥塞窗口 cwnd 设置为一个最大报文段 MSS 的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个 MSS 的数值。用这样的方法逐步增大发送方的拥塞窗口 cwnd,可以使分组注入到网络的速率更加合理。
为了防止拥塞窗口 cwnd 增长过大引起网络拥塞,还需要设置一个慢开始门限 ssthresh 状态变量,并规定
  • cwnd < ssthresh 时,使用「慢启动算法」。
  • cwnd > ssthresh 时,就会使用「拥塞避免算法」。
  • cwnd = ssthresh 时,既可使用「慢启动算法」,也可使用「拥塞避免算法」。
总结以上,慢启动的规则是:cwnd 的初始值设置为 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 个。
由此可以看出慢启动算法发包的个数是指数性的增⻓

7.2 拥塞避免

当拥塞窗口 cwnd 「超过」慢启动⻔限 ssthresh 就会进入拥塞避免算法。一般来说 ssthresh 的大小是 65535 字节。
拥塞避免算法的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。换而言之,当收到 cwnd 数量的 ACK 时,cwnd 的大小就会加1
notion image
  • 因为 ssthresh 为 8,当 8 个 ACK 应答确认到来时,每个确认增加 1/8,8 个 ACK 确认 cwnd 一共增加 1,于是这一次能够发送 9个 MSS 大小的数据,变成了线性增⻓。
拥塞避免算法就是将原本慢启动算法的指数增⻓变成了线性增⻓
拥塞避免算法虽然把发送包的增长速度变成了线性增长,但毕竟还在增⻓阶段。就这么一直增⻓着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。
只要发送方判断网络出现拥塞(其根据就是没有按时收到确认),就要把慢开始门限 ssthresh 设置为出现拥塞时的发送方窗口值的一半(但不能小于2)[插图]。然后把拥塞窗口 cwnd 重新设置为 1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。
  1. ssthresh = cwnd/2
  1. cwnd 重置为 1。
  1. 进入慢启动算法。
notion image

7.3 快速重传和快速恢复

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

7.5 拥塞避免算法过程

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

常见问题

TCP为什么是可靠的协议

  • 三次握手和四次挥手保证连接是可靠的。
  • 基于序列号的确认应答机制,保证数据包的有序性。
  • 对于没有收到数据包,提供重传机制。
  • 为了协调发送方的发送包和接收方的接收包的速度,提供流量控制机制。
  • 为了维护网络环境的稳定,提供拥塞控制机制。

解决time_wait过多的问题

解决方案很简单,通过修改/etc/sysctl.conf文件,服务器能够快速回收和重用那些TIME_WAIT的资源

解决CLOSE_WAIT过多的问题

CLOSE_WAIT很多,表示说要么是你的应用程序写的有问题,没有合适的关闭socket;要么是说,你的服务器CPU处理不过来(CPU太忙)或者你的应用程序一直睡眠到其它地方(锁,或者文件I/O等等),你的应用程序获得不到合适的调度时间,造成你的程序没法真正的执行close操作。

泛洪攻击

计算机网络系列(四):网络层(IP)计算机网络系列(二):应用层(HTTP)