传输层可靠传输协议---TCP

TCP如何保证可靠数据传输

​ ACK确认应答机制、校验和、序列号、流量控制、拥塞控制、ARQ超时重传

ARQ,自动重传请求(Automatic Repeat-reQuest,ARQ),包括停等式ARQ、GBN回退n帧的ARQ、选择性重传ARQ、混合ARQ

ARQ超时重传

​ 由于TCP要保证有序性,比如片段3丢失了,就算4-100都收到了,发送方在100时片段3没有收到ACK而超时,那么就会悲观的认为4-100也没收到,全部重传,非常浪费。当然接收方在3丢失的情况下,4-100的ACK都不会发出去,这是滑动窗口的机制决定的。 超时触发重传存在的问题是:超时周期可能相对较长。那是不是可以有更快的方式呢?于是就可以用「快速重传」机制来解决超时重发的时间等待。

  • 所谓快速重传收到3个相同的ACK就会触发! 所以在3丢失时:接收方收到4,发送2的ACK;收到5,发送2的ACK;收到6发送2的ACK。此时已经累计3次重复ACK,触发快速重传,那么4-100就不用白白重传了!
    快速重传只解决了超时的问题,但是没解决要重传哪一部分的问题:是重传丢失的?还是全部重传的?此时引入SACK解决这个问题。

  • SACK(Selective Acknowledgment),在快速重传的基础上,返回最近收到的报文段的序列号范围,这样客户端就知道,哪些数据包已经到达服务器了。 比如收到4,那么ACK2时就带上SACK4-4,收到5,那么ACK2时就带上SACK4-5,收到6,那么ACK2时就带上SACK4-6。这样发送方就不必悲观地重传已收到的4-6片段了,只需要重传3即可。
    如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。

  • DSACK,即Duplicate SACK,这个机制是在 SACK 的基础上,额外携带信息,告知发送方有哪些数据包自己重复接收了。让 TCP 可以更好的做网络流控。 优点:可以解决ACK丢包、网络延迟的问题!

    • ACK丢包:
      比如接收方发送的3次ACK3由于网络中断全部丢失导致重传,但其实接收方已经收到了!那么在重传第一次成功时,网络终于恢复,接收方的SACK还是3-6,发送方一看就知道:哦!原来我发的没丢,只是之前你的ACK丢了!

    • 网络延迟:
      比如由于网络延迟,发送方的以为某一段没收到!开启重传,但当那一段终于被接收到了后,SACK中也把这一段记录下来,在下一次ACK中发送方一看:哦!原来我发的没丢,只是延迟罢了,就不需要继续重传了!
      所以可以知道,此时的SACK字段是D-SACK!
      在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能(Linux 2.4 后默认打开)。

流量控制
  • 滑动窗口
    根据重传机制来看,发送方每次都需要等待连续的ACK才能确认是否发送成功,这样由于网络问题(延时、中断)导致吞吐率、效率低!因此引入滑动窗口的概念,即无需等待应答而可以继续发送的最大值! 比如 没滑动窗口前,3、4、5发完,必须超时前等到对应的3、4、5 ACK都收到才确认发送成功,否则可能造成重传,引入滑动窗口后,就算3的ACK丢失,在窗口范围内,只要后续收到4或5的ACK,那么都相当于默认3已ACK! 这个模式叫累计确认或者累计应答

    • 窗口大小由哪一方决定?
      窗口的实现其实是操作系统开辟的一个缓存空间(可看成数组),若窗口内的都确认ACK了,就清除缓存。TCP头有一字段叫Window,即窗口大小,用于接收方告诉发送方自己剩余多少缓冲区,让发送方根据接收方处理能力来发送。 因此窗口的大小由接收方确认

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

      滑动窗口大小 + 三个指针 来表示四个传输类别(区域):

      [已ACK区,[ [已发送未ACK区],[正在发送且未超过窗口大小的区域] ],未发送且超过窗口大小的区域]

      • SND.WND:表示发送窗口的大小(大小是由接收方指定的);
      • SND.UNA:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号。
      • SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号。
      • 指向[未发送且超过窗口大小的区域]是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量
    • 接受方窗口的表示?
      接受方就比较简单了,只有3个部分,只需要2个指针即可,即 RCV.NXT 绝对指针加上 RCV.WND 大小的偏移量就可以表示相对指针。

    • 接收窗口和发送窗口的大小是相等的吗?
      不一定,只是约等于的关系,取决于网络延时,有时候接收方快,则TCP头windows字段可能很快空下来。

  • 流量控制
    发送方不能无脑的发数据给接收方,要考虑接收方处理能力,否则会引起不必要的重传浪费。因此引入流量控制。

    • 收缩窗口?
      接受方可以根据自己的处理能力,通过控制窗口大小的变化来控制流量。 接收方操作系统可能资源紧张而需要减少缓存,但注意!!!接收方不能先减少缓存再减小窗口,否则可能会引起丢包!!!因为你实际可用缓存大小装不下你之前承诺给发送方的可接受大小,那就只能丢弃剩下的报文!!! 所以应该先减小窗口再减少缓存

    • 窗口关闭?
      接收端非常繁忙,应用层来不及取缓存中的数据,此时可用窗口大小会慢慢变成0,即窗口关闭。发送方会一直等到非0大小的窗口才会继续发送!

    • 窗口关闭存在的风险?
      可能存在死锁的风险。 若窗口关闭后,接收方处理完了,可以接受新数据了,可是由于网络问题 非0窗口大小的ACK 通知不到接收方,那么发送方一直等待非0的窗口大小通知,接收方一直等待发送方发送,双方一直僵持下去。

    • 解决窗口关闭的死锁问题?
      只要 TCP 连接一方收到对方的零窗口通知,就启动一个计时器。若计时器超时就会发送窗口探测 ( Window probe ) 报文,对方在收到这个报文时需要给出自己现在的接受窗口大小! 窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接。

    • 糊涂窗口综合症?
      由于流量控制可能会收缩窗口,收缩到最后,窗口小的甚至只剩下1、2个字节,发送方会傻乎乎的真的发送1、2个字节数据过去,要知道TCP + IP 头就有40个字节了, 你数据段才这一点点字节就要发送,非常浪费网络资源!!!

    • 解决糊涂窗口综合症?

      • 怎么让接收方不通告小窗口呢?

        ​ 定一个阈值MSS: MSS=min(MSS, 缓存空间/2),若窗口小于阈值MSS,那就把窗口关闭,即通告窗口大小为0。等到接收方处理完腾出空间后,才打开窗口。

      • 怎么让发送方避免发送小数据呢?

        ​ 使用 Nagle 算法,该算法的思路是延时处理,它满足以下两个条件中的一条才可以发送数据:

        1. 要等到窗口大小 >= MSS 或是 数据大小 >= MSS;
        2. 收到之前发送数据的 ack 回包
TCP粘包现象

​ TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾,接收端难以分辨。

  • 原因:

    • TCP传输的包都是大小不等的数据块,TCP把这些数据块看成一串字节流,没有边界。
    • 在TCP的首部没有表示数据长度的字段。
    • 比如TCP采用Nagle算法,为了避免发送的包过小浪费,会累积一定数量的包一起发送,因此产生了粘包现象。
  • 解决:根据产生的原因,可以:封装成大小固定的包;手动添加长度;设置边界。

  • UDP不会产生粘包现象? 因为UDP是基于报文发送的,在UDP首部采用了16bit来指示UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。

拥塞控制

流量控制保证两端的传输可靠,但互联网上可不止有两端,而是有N端,传输过程不确定因素多,流量大时可能出现网络拥堵,若出现了拥堵之后还不停的重传,可能加重拥堵,甚至将这个恶性循环放大,因此TCP需要做拥塞控制,避免发送方将数据填满整个网络!

  • 什么是拥塞窗口?和发送窗口有什么关系呢?

    拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。发送窗口swnd = min(cwnd, 接收窗口rwnd)。 cwnd变化规则:

    • 只要网络中没有出现拥塞,cwnd 就会增大;
    • 但网络中出现了拥塞,cwnd 就减少;
  • 怎么知道当前网络是否出现了拥塞呢?

    ​ 发生了超时重传,就会认为网络出现了用拥塞。

  • 拥塞控制有哪些控制算法?

    • 慢启动

      ​ 初始化 cwnd = 1,表示可以传一个 MSS 大小的数据。当发送方每收到一个 ACK,拥塞窗口cwnd的大小就会翻2倍,因此慢启动拥塞窗口大小呈指数性的增长

      最大可以增长到慢启动门限 ssthresh (slow start threshold)。

      • cwnd < ssthresh 时,使用慢启动算法。
      • cwnd >= ssthresh 时,就会使用「拥塞避免算法」。
    • 拥塞避免

      ​ 每当收到一个 ACK 时,cwnd 增加 1/cwnd。因此慢启动拥塞窗口大小呈线性的增长。此时增加的速度放缓,当再触发了重传机制,也就进入了「拥塞发生算法」。

    • 拥塞发生

      改变ssthreshcwnd

      • ssthresh 设为 cwnd/2
      • cwnd 重置为 1

      cwnd 一下被置为1,可见这种方式非常激进!若发生的是「快速重传」,可以不这么激进:

      • cwnd = cwnd/2 ,也就是设置为原来的一半;
      • ssthresh = cwnd;
      • 进入快速恢复算法
    • 快速恢复

      快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。

      • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);
      • 重传丢失的数据包;
      • 如果再收到重复的 ACK,那么 cwnd 增加 1;
      • 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;
TCP性能提升参数
  • TCP 三次握手的性能提升

    • 发送端调低重试次数

      ​ 发送端第一次握手发syn,重试次数为5次,重发的次数由 tcp_syn_retries 参数控制,每次超时的时间是上一次的 2 倍,可知总耗时是 1+2+4+8+16+32=63 秒。因此可以调低重试次数

    • 接收端提升容量数量

      ​ 接收端收到SYN后维护一个半连接队列保存未握手的连接,SYN 攻击,攻击的是就是这个半连接队列。第三次ACK接收端收到后握手成功,接收端内核建立一个成功连接队列,也叫全连接队列,这里面的成功连接等待应用层accept()去处理。增大半、全连接队列可以提高连接容量,方法是增大 tcp_max_syn_backlogsomaxconn。同样也可以减小tcp_synack_retries 接收端重试次数;开启syncookies设为1可以应对 SYN 攻击(syncookies 功能:服务器根据当前状态计算出一个值,放在己方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时,取出该值验证,如果合法,就认为连接建立成功,不会那么容易在SYN 半连接队列已满时被丢弃连接) ;tcp_abort_on_overflow 为 0 可以应对突发流量

  • 如何绕过三次握手?

    • TCP Fast Open

      ​ 即接收端和发送端都开启cookie机制,把cookie保存在本地,若有cookie,发送端请求时带cookie,服务端验证后,发完SYN+ACK直接响应数据,不必等第三次握手的发送端ACK,这就减少了握手带来的 1 个 RTT 的时间消耗。 TCP Fast Open 功能需要客户端和服务端同时支持,才有效果。

  • 等待后续总结

TCP抓
  • tcpdump

    # -i eth1表示抓取eth1网口数据; 仅icmp协议; host表示过滤主机、port端口; nn表示不解析IP地址和端口名称 -w表示 输出到ping.pcap文件
    $ tcpdump -i eth1 icmp and host 183.213.25.26 and port 80 -nn -w ping.pcap
    
  • Wireshark

    ​ 可以载入tcpdump的文件进行分析,可以清楚的看到各种协议(icmp)、源和目标mac地址、IP地址、IP头、TCP头。 TCP头中可以看到各种标志位:SYN、ACK、FIN等可以用来分析TCP的连接过程。

    ​ 点击 统计 (Statistics) -> 流量图 (Flow Graph),然后,在弹出的界面中的「流量类型」选择 「TCP Flows」,还可以清楚的看到图形化的TCP流

  • 抓包实验

    • TCP 第一次握手的 SYN 丢包了,会发生了什么?、TCP 第二次握手的 SYN、ACK 丢包了,会发生什么?TCP 第三次握手的 ACK 包丢了,会发生什么? 丢包重传嘞

      ​ 可以通过配置防火墙规则如iptables -I INPUT -s 183.213.25.26 --tcp-flag ACK ACK -j DROP来丢掉发送、接受端的连接。用crul发起TCP连接。用tcpdump得到dump文件丢进Wireshark分析。

      • 进来的顺序 Wire -> NIC -> tcpdump -> netfilter/iptables
      • 出去的顺序 iptables -> tcpdump -> NIC -> Wire
    • 那会重传几次? 5次

    • 超时重传的时间 RTO 会如何变化? RTO 的值是指数增长的,所以持续了好长一段时间,客户端的 telnet 才报错退出了,此时共重传了 15 次。和TCP 的 保活机制有关:大概保活7200 秒(2小时)。

    • 在 Linux 下如何设置重传次数?

      $ echo 2 > /proc/sys/net/ipv4/tcp_synack_retries
      $ echo 1 > /proc/sys/net/ipv4/tcp_syn_retries
      
posted @ 2021-04-20 20:38  i%2  阅读(175)  评论(0)    收藏  举报