25.TCP协议-超时与重传

TCP 超时与重传应该是 TCP 最复杂的部分之一了;

Windows 和 Linux 对这部分的实现有所不同,但是算法基本上还是差不多的;

超时重传是 TCP 保证可靠传输的基础;

当 TCP 在发送数据时,数据和 ack 都有可能会丢失;

因此,TCP 通过在发送时设置一个定时器来解决这种问题。如果定时器溢出还没有收到确认,它就重传数据;

无论是 Windows 还是 Linux,关键之处就在于超时和重传的策略,需要考虑两方面:

  • 超时时间设置
  • 重传的频率(次数)

目前来说,在 Linux 较高的内核版本中,比如 3.15 中,已经有了至少 9 个定时器:超时重传定时器,持续定时器,ER延迟定时器,PTO定时器,ACK延迟定时器,SYNACK定时器,保活定时器,FIN_WAIT2定时器,TIME_WAIT定时器。

这实在是太多了,对初学者来说,我们重点掌握以下 4 个:

  • 超时重传定时器(retransmit)
  • 持续定时器(persist)
  • 保活定时器(keepalive,这和 HTTP 协议中的 keepalive 不是同一个概念)
  • TIME_WAIT 定时器

一个超时重传的例子

这里写图片描述
图1 超时重传 

本实验所使用的程序路径为 unp/program/echo/processzombie/echo.cc,你可以直接使用 make 命令进行编译。

  • 服务器端启动方式
$ ./echo -s -h flower // 或者你可以这样写 ./echo -s -h 192.168.166.47,flower 是我其中一台 linux 主机的名字
  • 客户端启动方式
$ ./echo -h flower // 或者你可以写 ./echo -h 192.168.166.47

当客户端连接成功后,发送一行数据'helloworld',对方回射回来,一切正常,接下来,将服务器主机 flower 断网,然后客户端再次发送数据 'hehe'

大约等等了 16 分钟左右(图2),客户端返回一个错误:No route to host.

这里写图片描述 
图2 客户端等待约 16 分钟后返回错误 
这里写图片描述
图3 第 9 次重传后,主机亲自发送 ARP 协议询问对方 MAC 地址 

因为在同一个网段,主机 sun 发送了17×3=51 次 ARP 请求,每发 3 次 ARP 就等 50 s 左右。

《详解》中的第 21 章的例子(图 21-1),是在经历了 12 次重传后放弃(约 9 分钟),向对方发送 RST 段。

往返时间(RTT)

超时重传时间(Retransmission TimeOut, RTO)要怎么设置呢?

数据包过去,到 ack 返回,这个时间一般约等于 RTT 时间,如果一个 RTT 时间内没有收到 ack,很可能对方就没有收到数据,或者回送的 ack 丢失。

所以,最直观的想法是,RTO 应该比 RTT 稍稍大一点;比如: RTO=RTT+Δt

RTT 测量

可是,在公式中,RTT 是如何测量呢?

在 TCP 中,每一次数据包传送过去到接收到对方的 ack 这个时间差,就会被 TCP 记录,然后保存到一个变量 RTTs中去;

在局域网中,我们的网络一般是很稳定的,每次重新计算一个 RTT,基本上变化不太大,但是在广域网中,网络就会变得异常复杂,这一次的 RTT 为 100ms,说不定下一次就变 800 ms 了,这时候,采用实时计算的 RTT 就会不合理,在 RFC 中,使用了加权的 RTT。它的公式如下:

RTTs=(1α)×RTTs+α×RTTnew

RFC 2988 建议 α=0.125。上面这个公式说明,我想使用旧的RTTs 中的 87.5% 的成分,新的 RTT 样本中 12.5% 的成分,这样就不至于让RTTs 发生剧烈抖动;

一个直观的例子就是,某些同学的成绩太差,拉低了班级的整体水平,为了防止这种现象,可以在计算平均值的时候动些手脚。

举个例子,当前RTTs=200ms,最新一次测量的RTT=800ms,那么更新后的 RTTs=200×0.875+800×0.125=275ms;

Δt 怎么定义

在前面,我们臆想了一个公式: RTO=RTT+Δt

现在我们将其更新为:RTO=RTTs+Δt

RTTS 的计算方法我们在上节中已经介绍过了,现在是 Δt 是什么,它怎么算?我们只知道,它应该很小。RFC 2988 规定:

RTO=RTTs+4×RTTD

因此,按照上面的定义,Δt=4×RTTD

而 RTTD 计算公式如下:RTTD=(1−β)×RTTD+β×|RTTs−RTT|

实际上,RTTD 就是一个均值偏差,它就相当于某个样本到其总体平均值的距离;

这就好比你的成绩与你班级平均成绩差了多少。

RFC 推荐 β=0.25.

指数退避

假设在某一次发送数据的时候,数据丢失了,根据前面的公式,我们计算出了一个 RTO 值;

如果重传后,还是没有等到对方的 ack,那么 RTO 的值就会翻倍;

只要重传的的数据没有 ack,那么 RTO 就会一直翻倍。

则第 n 次重传的 RTOn 值为:RTOn=2n1×RTO1

Karn 算法

假设一个分组被发送,经过若干次重传后,收到了对方的 ack,则新的 RTT 如何计算呢?实际上,我们根本没有办法知道这个 ack 是对哪一次重传数据的确认,因此,Karn 算法规定:此时不更新 RTTs 的值

下一次再发生重传,使用退避后的 RTO 的值。

回到图 1

在图 1 中,我们发现,第一次重传的RTTs=0.225s,而第二次,并没有使用指数退避算法,到了第 3 次重传才使用了指数退避算法,后面依次是 0.451, 0.902, 1.804, 3.612, 7.215, 14.416, 28.865。

另一方面,如果我们按照前面的RTTs 计算公式,计算出来的 RTTs 肯定是非常小的,大概就只有几毫秒,如果根据这个来计算 RTO,肯定也几毫秒的样子。可是为什么重传超时时间的RTO达到了约200ms?

现代的 Linus 内核中,规定了 RTO 有一个最大值和最小值,最大值是是 120 秒,最小是 200 ms:

#define TCP_RTO_MAX ((unsigned)(120*HZ))  // 120秒

#define TCP_RTO_MIN ((unsigned)(HZ/5))    // 0.2s

另一方面,为什么 Linux 中的 TCP 重传 8 次后就在那停住,还得去看内核到底是怎么实现的了。相信大家在掌握了基本的 TCP 超时原理后,一定会找出这个答案的。

猜你喜欢

转载自blog.csdn.net/regemc/article/details/80808483