Loading...

TCP的基础知识

  iwehdio的博客园:https://www.cnblogs.com/iwehdio/

学习自:

TCP基本认识

  • TCP头部格式:

    • 序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
    • 确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决不丢包的问题。
    • 控制位:
      • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN包之外该位必须设置为 1 。
      • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
      • SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
      • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
  • 为什么需要 TCP 协议?

    • IP 层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。
    • 如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。
    • TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。
  • 什么是TCP?

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

    • 用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。
    • 建立一个 TCP 连接是需要客户端与服务器端达成上述三个信息的共识。
      • Socket:由 IP 地址和端口号组成
      • 序列号:用来解决乱序问题等
      • 窗口大小:用来做流量控制
  • 如何唯一确定一个 TCP 连接呢?

    • TCP 四元组可以唯一的确定一个连接,四元组包括如下:源地址、源端口、目的地址、目的端口。
    • 源地址和目的地址的字段(32位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。
    • 源端口和目的端口的字段(16位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。
  • 有一个 IP 的服务器监听了一个端口,它的 TCP 的最大连接数是多少?

    • 服务器通常固定在某个本地端口上监听,等待客户端的连接请求。因此,客户端 IP 和 端口是可变的,其理论值计算公式为:最大TCP连接数=客户端的IP数×客户端的端口数。
    • 对 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数,约为 2 的 48 次方。
    • 当然,服务端最大并发 TCP 连接数远不能达到理论上限。
      • 首先主要是文件描述符限制,Socket 都是文件,所以首先要通过 ulimit 配置文件描述符的数目。包括系统级、用户级和进程级的配置;
      • 另一个是内存限制,每个 TCP 连接都要占用一定内存,操作系统的内存是有限的。
      • 一条空的TCP连接至少要占用3.3KB,而要接受和发送数据,至少需要各4KB的空间,而且还需要CPU资源。
  • UDP 和 TCP 有什么区别呢?分别的应用场景是?

    • UDP 不提供复杂的控制机制,利用 IP 提供面向「无连接」的通信服务。UDP 协议真的非常简单,头部只有 8 个字节( 64 位)。
      • 目标和源端口:主要是告诉 UDP 协议应该把报文发给哪个进程。
      • 包长度:该字段保存了 UDP 首部的长度跟数据的长度之和。
      • 校验和:校验和是为了提供可靠的 UDP 首部和数据而设计。
    • TCP和UDP的区别:
      • 连接:TCP 是面向连接的传输层协议,传输数据前先要建立连接。UDP 是不需要连接,即刻传输数据。
      • 服务对象:TCP 是一对一的两点服务,即一条连接只有两个端点。UDP 支持一对一、一对多、多对多的交互通信
      • 可靠性:TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。UDP 是尽最大努力交付,不保证可靠交付数据。
      • 拥塞控制、流量控制:TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
      • 首部开销:TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
      • 传输方式:TCP 是流式传输,没有边界,但保证顺序和可靠。UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
      • 分片不同:TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层,但是如果中途丢了一个分片,则就需要重传所有的数据包,这样传输效率非常差,所以通常 UDP 的报文应该小于 MTU。
    • 应用场景:
      • 由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:FTP 文件传输、HTTP / HTTPS。
      • 由于 UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:包总量较少的通信,如 DNS 、SNMP 等;视频、音频等多媒体通信;广播通信。
  • 为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢?

    • 原因是 TCP 有可变长的「选项」字段,而 UDP 头部长度则是不会变化的,无需多一个字段去记录UDP 的首部长度。
  • 为什么 UDP 头部有「包长度」字段,而 TCP 头部则没有「包长度」字段呢?

    • TCP 是如何计算负载数据长度:TCP数据长度=IP包总长度-IP首部长度-TCP首部长度。
    • 其中 IP 总长度 和 IP 首部长度,在 IP 首部格式是已知的。TCP 首部长度,则是在 TCP 首部格式已知的,所以就可以求得 TCP 数据的长度。
    • UDP 也是基于 IP 层的呀, UDP 的数据长度也可以通过这个公式计算。DP 「包长度」是冗余的,可能是为了网络设备硬件设计和处理方便,首部长度需要是 4 字节的整数倍。

TCP连接建立

  • TCP的三次握手过程:

    • 一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN状态。
    • 客户端会随机初始化序号( client_isn ),将此序号置于 TCP 首部的「序号」字段中,同时把SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
    • 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号( server_isn ),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn +1 , 接着把 SYN 和 ACK 标志位置为 1 。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
    • 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态。

    • 服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。
    • 第三次握手是可以携带数据的,前两次握手是不可以携带数据的。
    • 一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
  • 如何在 Linux 系统中查看 TCP 状态?

    • TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。

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

    • 首先,因为三次握手才能保证双方具有接收和发送的能力。
    • 其次,重点在于为什么三次握手才可以初始化Socket、序列号和窗口大小并建立 TCP 连接。
    • 避免历史连接:三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。
      • 客户端连续发送多次 SYN 建立连接的报文,在网络拥堵情况下:
        • 一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端;
        • 那么此时服务端就会回一个 SYN + ACK 报文给客户端;
        • 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送 RST 报文给服务端,表示中止这一次连接。
      • 如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接:
        • 如果是历史连接(序列号过期或超时),则第三次握手发送的报文是 RST 报文,以此中止历史
          连接;
        • 如果不是历史连接,则第三次发送的报文是 ACK 报文,通信双方就会成功建立连接。
    • 同步双方初始序列号:两次握手只能同步一方。
      • TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
        • 接收方可以去除重复的数据;
        • 接收方可以根据数据包的序列号按序接收;
        • 可以标识发送出去的数据包中, 哪些是已经被对方收到的。
      • 当客户端发送携带「初始序列号」的 SYN报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
      • 四次握手中,服务端的ACK应答和SYN请求可以合成一次。
      • 而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
    • 避免资源浪费:两次握手造成冗余连接。
      • 如果只有「两次握手」,当客户端的 SYN 请求连接在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的ACK 确认信号,所以每收到一个 SYN 就只能先主动建立一个连接。
      • 这会造成,如果客户端的 SYN 阻塞了,重复发送多次 SYN 报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
    • 小结:
      • TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
      • 不使用「两次握手」和「四次握手」的原因:
        • 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列
          号;
        • 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
  • 为什么每次的初始序列号 ISN 是不相同的?

    • 如果一个已经失效的连接被重用了,但是该旧连接的历史报文还残留在网络中,如果序列号相同,那么就无法分辨出该报文是不是历史报文,如果历史报文被新的连接接收了,则会产生数据错乱。
    • 所以,每次建立连接前重新初始化一个序列号主要是为了通信双方能够根据序号将不属于本连接的报文段丢弃。
    • 另一方面是为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收。
  • 初始序列号 ISN 是如何随机产生的?

    • 起始 ISN 是基于时钟的,每 4 毫秒 + 1,转一圈要 4.55 个小时。
    • RFC1948 中提出了一个较好的初始化序列号 ISN 随机生成算法。
      • ISN = M + F (localhost, localport, remotehost, remoteport)
      • M 是一个计时器,这个计时器每隔 4 毫秒加 1。
      • F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。
  • 既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?

    • MTU和MSS:

    • MTU :一个网络包的最大长度,以太网中一般为 1500 字节;
      MSS :除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度;

    • 如果在 TCP 的整个报文(头部 + 数据)交给 IP 层进行分片,会有什么异常呢?

      • 当 IP 层有一个超过 MTU 大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后发送。由接收的目标主机的 IP层来进行重新组装后,再交给上一层 TCP 传输层。
      • 这看起来井然有序,但这存在隐患的,那么当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。
      • 因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。
      • 当接收方发现 TCP 报文(头部 + 数据)的某一片丢失后,则不会响应 ACK 给对方,那么发送方的TCP 在超时后,就会重发「整个 TCP 报文(头部 + 数据)」。因此,可以得知由 IP 层进行分片传输,是非常没有效率的。
    • 所以,为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。

    • 经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。

  • 什么是 SYN 攻击?如何避免 SYN 攻击?

    • SYN攻击:

      • 我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态。
      • 但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的 SYN 接收队列(未连接队列),使得服务器不能为正常用户服务。
    • Linux 内核的 SYN (未完成连接建立)队列与 Accpet (已完成连接建立)队列是如何工作的?

      • 当服务端接收到客户端的 SYN 报文时,会将其加入到内核的「 SYN 队列」;
        接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;
      • 服务端接收到 ACK 报文后,从「 SYN 队列」移除放入到「 Accept 队列」;
    • 应用通过调用 accpet() socket 接口,从「 Accept 队列」取出连接。

    • 如果应用程序过慢时,就会导致「 Accept 队列」被占满。

    • 如果不断受到 SYN 攻击,就会导致「 SYN 队列」被占满。

    • 解决方式一:

      • 通过修改 Linux 内核参数,控制队列大小和当队列满时应做什么处理。当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。

        # 该队列的最大值
        net.core.netdev_max_backlog
        # SYN_RCVD 状态连接的最大个数
        net.ipv4.tcp_max_syn_backlog
        # 超出处理能力时,对新的 SYN 直接回报 RST,丢弃连接:
        net.ipv4.tcp_abort_on_overflow
        
    • 解决方式二:使用SYN Cookie算法。

      • 当 「 SYN 队列」满之后,后续服务器收到 SYN 包,不进入「 SYN 队列」;
      • 服务器计算出一个 cookie 值,再以 SYN + ACK 中的「序列号」返回客户端,
      • 服务端接收到客户端的应答报文时,服务器会检查这个 ACK 包的合法性。如果合法,直接放入到「 Accept 队列」。
  • 什么是 TCP 半连接队列和全连接队列?

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

    image-20210104195856294

TCP连接断开

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

    • TCP 断开连接是通过四次挥手方式。双方都可以主动断开连接,断开连接后主机中的「资源」将被释放。

    • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN报文,之后客户端进入 FIN_WAIT_1 状态。

    • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。

    • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。

    • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。

    • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态

    • 服务器收到了 ACK 应答报文后,就进入了 CLOSED 状态,至此服务端已经完成连接的关闭。

    • 客户端在经过 2MSL 一段时间后,自动进入 CLOSED 状态,至此客户端也完成连接的关闭。

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

  • 为什么挥手需要四次?

    • 关闭连接时,客户端向服务端发送 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 的时间,以确保报文已被自然消亡。
    • 首先,保证老的连接的报文段在网络中消失,使下一个新的连接中不会出现这种旧的连接请求的报文段。 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
    • 其次,保证全双工连接的关闭。如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 Fin 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方。
    • 2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
    • 在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在TIME_WAIT 的时间为固定的 60 秒。
  • 为什么需要 TIME_WAIT 状态?

    • 主动发起关闭连接的一方,才会有 TIME-WAIT 状态。
    • 防止具有相同「四元组」的「旧」数据包被收到。
      • 假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后,如果有相同端口的 TCP 连接被复用后,被延迟的数据包抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题。
      • 经过 2MSL 这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
    • 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。
      • 四次挥手的最后一个 ACK 报文如果在网络中被丢失了,此时如果客户端TIME-WAIT 过短或没有,则就直接进入了 CLOSED 状态了,那么服务端则会一直处在LASE_ACK 状态。
      • 当客户端发起建立连接的 SYN 请求报文后,服务端会发送 RST 报文给客户端,连接建立的过程就会被终止。
  • TIME_WAIT 过多有什么危害?

    • 过多的 TIME-WAIT 状态主要的危害有两种:
      • 第一是内存资源占用;
      • 第二是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口。
    • 客户端受端口资源限制:
      • 客户端TIME_WAIT过多,就会导致端口资源被占用,因为端口就65536个,被占满就会导致无法创建新的连接。
    • 服务端受系统资源限制:
      • 由于一个四元组表示 TCP 连接,理论上服务端可以建立很多连接,服务端却只监听一个端口 但是会把连接扔给处理线程,所以理论上监听的端口可以继续监听。
      • 但是线程池处理不了那么多一直不断的连接了。所以当服务端出现大量 TIME_WAIT 时,系统资源被占满时,会导致处理不过来新的连接。
  • 如何优化 TIME_WAIT?

    • 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项。
      • 复用处于 TIME_WAIT 的 socket 为新的连接所用。
    • net.ipv4.tcp_max_tw_buckets。
      • 当系统中处于 TIME_WAIT 的连接一旦超过某个值时,系统就会将所有的TIME_WAIT 连接状态重置
    • 程序中使用 SO_LINGER ,应用强制使用 RST 关闭。
      • 该TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT 状态,直接关闭。
  • 如果已经建立了连接,但是客户端突然出现故障了怎么办?

    • TCP 有一个机制是保活机制。这个机制的原理是这样的:
      • 定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文。
      • 该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
    • 在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,默认下2h+9*75s即2小时11分15秒才可以发现一个死亡连接。
    • 如果开启了 TCP 保活,需要考虑以下几种情况:
      • 第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
      • 第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。
      • 第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。

TCP的Socket编程

重传机制

  • TCP 实现可靠传输的方式之一,是通过序列号与确认应答。在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。(发送数据1~1000,返回确认并确认号为1001)

  • 超时重传:

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

    • RTT 就是数据从网络一端传送到另一端所需的时间,也就是包的往返时间。

    • 超时重传时间是以 RTO (Retransmission Timeout 超时重传时间)表示。

      • 当超时时间 RTO 较大时,重发就慢,丢了半天才重发,没有效率,性能差;
      • 当超时时间 RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
    • 超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。

    • 「报文往返 RTT 的值」是经常变化的,因为我们的网络也是时常变化的。也就因为「报文往返RTT 的值」 是经常波动变化的,所以「超时重传时间 RTO 的值」应该是一个动态变化的值。

    • 估计往返时间,通常需要采样以下两个:

      • 需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个平滑 RTT 的值,而且这个值还是要不断变化的,因为网络状况不断地变化。
      • 除了采样 RTT,还要采样 RTT 的波动范围,这样就避免如果 RTT 有一个大的波动的话,很难被发现的情况。
    • RTO的计算:

    • 如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。

    • 超时触发重传存在的问题是,超时周期可能相对较长。

  • 快速重传:

    • TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。

    • 第一份 Seq1 先送到了,于是就 Ack 回 2;

    • 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;

    • 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;

    • 发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。

    • 最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。

    • 快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。

    • 快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。

  • SACK方法:Selective Acknowledgment 选择性确认

    • 要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。

    • sack大于ack就表明有些数据没收到。

    • 如果要支持 SACK ,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。

  • Duplicate SACK:

    • 又称 D-SACK ,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。
    • 解决ack丢包:
      • 「发送方」发送了(30003499)和(35003999)两个数据包,都被成功接收了。
      • 「接收方」发给「发送方」的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499)
      • 于是「接收方」发现数据是重复收到的,于是回了一个 SACK = 30003500,告诉「发送方」30003500 的数据早已被接收了,因为 ACK 都到了 4000 了,已经意味着 4000 之前的所有数据都已收到,所以这个 SACK 就代表着 D-SACK 。
      • 也就是SACK 小于ack就代表着 D-SACK,数据重新发送了 。这样「发送方」就知道了,数据没有丢,是「接收方」的 ACK 确认报文丢了。
    • 解决网络延时:
      • 数据包(1000~1499) 被网络延迟了,导致「发送方」没有收到 Ack 1500 的确认报文。
      • 而后面报文到达的三个相同的 ACK 确认报文,就触发了快速重传机制,但是在重传后,被延迟的数据包(1000~1499)又到了「接收方」;
      • 所以「接收方」回了一个 SACK=1000~1500,因为 ACK 已经到了 3000,所以这个 SACK 是 DSACK,表示收到了重复的包。
      • 这样发送方就知道快速重传触发的原因不是发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延迟了。
    • D-SACK 有这么几个好处:
      1. 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
      2. 可以知道是不是「发送方」的数据包被网络延迟了;
      3. 可以知道网络中是不是把「发送方」的数据包给复制了;
    • 在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能(Linux 2.4 后默认打开)。

滑动窗口

  • 为什么引入窗口:

    • TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。

    • 这种方式的缺点是效率比较低的。而且数据包的往返时间越长,通信的效率就越低。

    • TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。

    • 窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。

    • 窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。

    • 假设窗口大小为 3 个 TCP 段,那么发送方就可以「连续发送」 3 个 TCP 段,并且中途若有 ACK丢失,可以通过「下一个确认应答进行确认」。

    • 只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答。

  • 窗口大小由哪一方决定?

    • TCP 头里有一个字段叫 Window ,也就是窗口大小。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
    • 所以,通常窗口的大小是由接收方的窗口大小来决定的。发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。
  • 发送方的滑动窗口:

    • 发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口:

      • 1 是已发送并收到 ACK确认的数据:1~31 字节

      • 2 是已发送但未收到 ACK确认的数据:32~45 字节

      • 3 是未发送但总大小在接收方处理范围内(接收方还有空间):46~51字节

      • 4 是未发送但总大小超过接收方处理范围(接收方没有空间):52字节以后

    • 当发送方把可用窗口中的数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了。

    • 当收到之前发送的数据 32~36 字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56 字节又变成了可用窗口,那么后续也就可以发送 52~56 这 5 个字节的数据了。

  • 程序是如何表示发送方的四个部分的呢?

    • TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。

      • SND.WND :表示发送窗口的大小(大小是由接收方指定的);
      • SND.UNA :是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是#2 的第一个字节。
      • SND.NXT :也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3的第一个字节。
      • 指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节了。
    • 那么可用窗口大小的计算就是:可用窗口大 = SND.WND -(SND.NXT - SND.UNA)

  • 接收方的滑动窗口:

    • 接收方缓存的数据,根据情况分为三个部分:

      • 1 + #2 是已成功接收并确认的数据(等待应用进程读取);

      • 3 是未收到数据但可以接收的数据(包括已接收但未确认的数据);

      • 4 未收到数据并不可以接收的数据;

    • 使用两个指针进行划分:

      • RCV.WND :表示接收窗口的大小,它会通告给发送方。
      • RCV.NXT :是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。
      • 指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量,就可以指向 #4 的第一个字节了。
  • 接收窗口和发送窗口的大小是相等的吗?

    • 并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。
    • 因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。

流量控制

  • 流量控制:
    • 发送方不能无脑的发数据给接收方,要考虑接收方处理能力。
    • 如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致频繁触发重发机制,从而导致网络流量的无端的浪费。
    • 为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。
  • 操作系统缓冲区与滑动窗口的关系:
    • 我们假定了发送窗口和接收窗口是不变的,但是实际上,发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,会被操作系统调整。
    • 当应用进程没办法及时读取缓冲区的内容时,也会对我们的缓冲区造成影响。
  • 操心系统的缓冲区,是如何影响发送窗口和接收窗口的呢?
    • 当应用程序没有及时读取缓存时:
      • 服务端非常繁忙,应用进程只读取了一部分数据,还有 一部分数据占用着缓冲区,于是接收窗口相当于缩小了。
      • 服务端发送确认信息时,将窗口大小通告给客户端。以便客户端发送适合该窗口大小的数据。
      • 如果接收端的接收窗口为0,在通告给发送端后,发送窗口减少为 0。也就是发生了窗口关闭。
      • 当发送方可用窗口变为 0 时,发送方实际上会定时发送窗口探测报文,以便知道接收方的窗口是否发生了改变。
    • 当服务端系统资源非常紧张的时候:
      • 操心系统可能会直接减少了接收缓冲区大小,这时应用程序又无法及时读取缓存数据,那么这时候就有严重的事情发生了,会出现数据包丢失的现象。
      • 服务端收到了数据时,如果发现数据大小超过了接收窗口的大小,就会把数据包丢失了。
      • 而客户端如果在接收到窗口为0的通告之前又发送了数据,在接收到窗口为0的通告后可用窗口可能变为负值。
      • 如果发生了先减少缓存,再收缩窗口,就会出现丢包的现象。
      • 为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况。
  • 窗口关闭的潜在危险:
    • TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。
    • 如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。
    • 接收方向发送方通告窗口大小时,是通过 ACK 报文来通告的。
    • 那么,当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,那麻烦就大了。
    • 这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。
  • TCP 是如何解决窗口关闭时,潜在的死锁现象呢?
    • TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。
    • 如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
      • 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器;如果接收窗口不是 0,那么死锁的局面就可以被打破了。
    • 窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接。
  • 糊涂窗口综合症:
    • 如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。
    • 如果发送窗口小到了一定的程度,为此发送一个数据包是不经济的。
    • 发生该行为的条件:
      • 接收方可以通告一个小的窗口
      • 而发送方可以发送小数据
    • 怎么让接收方不通告小窗口呢?
      • 当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0 ,也就阻止了发送方再发数据过来。
      • 等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。
    • 怎么让发送方避免发送小数据呢?
      • Nagle算法主要是避免发送小的数据包,要求TCP连接上最多只能有一个未被确认的小分组,在该分组的确认到达之前不能发送其他的小分组。
      • 使用 Nagle 算法满足以下两个条件中的一条才可以发送数据:
        • 要等到窗口大小 >= MSS 或是 数据大小 >= MSS
        • 收到之前发送数据的 ack 回包
      • 只要没满足上面条件中的一条,发送方一直在囤积数据,直到满足上面的发送条件。

拥塞控制

  • 为什么要有拥塞控制呀,不是有流量控制了吗?
    • 的流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。
    • 一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。
    • 在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大。
    • 于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。
    • 为了在「发送方」调节所要发送数据的量,定义了一个叫做「拥塞窗口」的概念。
  • 什么是拥塞窗口?和发送窗口有什么关系呢?
    • 拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。
    • 发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是发送窗口是拥塞窗口和接收窗口中的最小值。
    • 拥塞窗口 cwnd 变化的规则:
      • 只要网络中没有出现拥塞, cwnd 就会增大;
      • 但网络中出现了拥塞, cwnd 就减少。
  • 怎么知道当前网络是否出现了拥塞呢?
    • 其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了用拥塞。
  • 拥塞控制有哪些控制算法?
    • 拥塞控制主要是四个算法:慢启动、拥塞避免、拥塞发生、快速恢复。
    • 慢启动:
      • TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量。
      • 慢启动的规则是:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。(拥塞窗口为4,则返回4个ack后变为8)
      • 慢启动算法,发包的个数是指数性的增长。
    • 慢启动涨到什么时候是个头呢?
      • 有一个叫慢启动门限 ssthresh (slow start threshold)状态变量。
        • 当 cwnd < ssthresh 时,使用慢启动算法。
        • 当 cwnd >= ssthresh 时,就会使用「拥塞避免算法」。
    • 拥塞避免:
      • 当拥塞窗口 cwnd 「超过」慢启动门限 ssthresh 就会进入拥塞避免算法。一般来说 ssthresh 的大小是 65535 字节。
      • 拥塞避免的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。
      • 拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长,还是增长阶段,但是增长速度缓慢了一些。
      • 一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。当触发了重传机制,也就进入了「拥塞发生算法」。
    • 拥塞发生:
      • 发生超时重传的拥塞发生算法:
        • 使用拥塞发生算法。这个时候,ssthresh 和 cwnd 的值会发生变化:
          • ssthresh 设为 cwnd/2 ,
          • cwnd 重置为 1
        • 接着,就重新开始慢启动。这种方式太激进了,反应也很强烈,会造成网络卡顿。
      • 发生快速重传的拥塞发生算法:
        • TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下:
          • cwnd = cwnd/2 ,也就是设置为原来的一半;
          • ssthresh = cwnd ;
          • 进入快速恢复算法
    • 快速恢复:
      • 快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。
      • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);
      • 重传丢失的数据包;
      • 如果再收到重复的 ACK,那么 cwnd 增加 1;
      • 如果收到新数据的 ACK 后,把 cwnd 设置为快重传拥塞发生中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 三次重发 ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态。

iwehdio的博客园:https://www.cnblogs.com/iwehdio/

posted @ 2020-12-30 22:25  iwehdio  阅读(587)  评论(0编辑  收藏  举报