TCP和UDP都是传输层的协议,但二者具有不同的特性,也适用于不同的场景。如下表所示:
TCP |
UDP |
|
|---|---|---|
| 可靠性 | 可靠 | 不可靠 |
| 连接性 | 面向连接 | 无连接 |
数据经过传输之后可能是无序的,TCP协议会将这些无序的数据重新进行组装成有序的,供给上层调用者使用 |
无序 | |
| 报文 | 面向字节流 | 面向报文 |
| 效率 | 传输效率低 | 传输效率高 |
| 双工性 | 全双工 | 一对一、一对多、多对一、多对多 |
| 流量控制 | 滑动窗口 | 无 |
| 拥塞控制 | 慢开始、拥塞避免、快重传、快恢复 | 无 |
| 无,若是接收端缓存区足够大,可将发送端多次发送的数据一次性接收,然后传给上层应用 | 有,发送端发送一次,接收端就要接收一次,发送多少次就要接收多少次 | |
| 传输速度 | 慢 | 快 |
| 重量级,数据报报头大小为20个字节 | 轻量级,数据报报头大小为8个字节 | |
| 应用场景 | 对效率要求低、对可靠性要求高或者要求有连接的场景,如文件传输,邮件发送等 | 对效率要求高、对准确性要求低,如即时通信,QQ,视频通话等 |
TCP是面向连接的协议,UDP是无连接的协议。因而,当使用TCP协议传输数据时,客户端与服务端之间必须通过三次握手来建立连接。
TCP提供交付保证,有许多机制用于保证消息的可靠性。
TCP直接丢弃,不会进行确认;TCP会启动一个定时器,等待对端确认这个数据包。如果在指定时间内没有确认,则会进行重传,再等待一段时间,往复几次,直到重传次数超过一定次数之后,就会丢弃这个包;TCP协议会根据报头的序列号将传输过来的无序数据整理成有序的,而UDP不会。
TCP比较慢,而UDP比较快。因为TCP必须要先建立连接,以保证消息的可靠性和有序性,需要进行的内部操作比UDP多很多。TCP适合大量数据的传输,UDP适合少量数据的传输。
TCP是重量级协议,而UDP是轻量级协议。一个TCP数据报的报头至少为20个字节,UDP数据报报头固定是8个字节。如下所示:
UDP报头
TCP是面向字节流的协议,无边界记录。而UDP发送的每个数据是记录型的数据报,所谓记录型数据报就是接收进程可以识别到接收到的数据报的记录边界。
因为发送窗口(接收主机能够接收的数据量),拥塞窗口(对网络拥塞的估计),路径上的最大传输单元(传输的最大数据量),慢启动。所以不能确定TCP的分包个数与大小。
1,定长报文,读取报文中的固定字节
2,可以用结束标记(回车、换行)来分隔记录
TCP报头分析
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP 报文段的报头有 10 个必需的字段和 1 个可选字段。报头至少为 20 字节。报头后面的数据是可选项。
各占2个字节字节,分别是源端口号和目的端口号
4个字节,使用mod计算,TCP协议是面向字节流的,在TCP连接中传输的字节流的每一个字节都是按照顺序编号的。
4个字节,表示期望收到对方下一个报文段的第一个数据字节的序号。若确认号为N,则表示到序号N-1为止的所有数据都已经确认收到。
4位,TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远,即首部长度。 由于 TCP 报头的长度随 TCP 选项字段内容的不同而变化,因此报头中包含一个指定报头字段的字段。TCP报头最小20个字节,最大60个字节。
6位,目前还未使用,待以后使用,目前都是0。
6位
当URG=1时表示紧急指针字段有效。这个时候发送方TCP就会把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍然是普通数据。
当ACK=1时,确认字段有效。当ACK=0,确认字段无效。**TCP规定,在连接建立后所有传送的报文段都必须置为1。 **
接收方TCP收到PUSH=1的报文段时,就会尽快交付给上层应用程序,而不是等到整个缓存区都填满了之后再交付。
当RST=1时,表明TCP连接出现严重差错,必须释放连接,然后再重新建立连接。(先释放连接,再重新建立连接)。
在建立连接时用来同步序号。当SYN=1,ACK=0时,表明这是一个用来建立连接的请求报文。对方若是同意建立连接,则会回应一个SYN=1,ACK=1的报文。故SYN=1,表明这是一个连接请求或者是连接接收报文。
用于释放连接。当FIN=1时,表明发送方的数据已发送完毕,并要求释放连接。
2个字节,此字段用来进行流量控制,这个值是本机期望一次接收的字节数,告诉对方在不等待确认的情况下,可以发来多大的数据。这里表示的最大长度是2^16 - 1 =65535,如需要使用更大的窗口大小,需要使用选项中的窗口扩大因子选项。指发送本报文段的一方的接收窗口(而不是自己的发送窗口)。
源主机和目的主机根据TCP报文段以及伪报头的内容计算校验和。伪报头中存放着来自IP报头以及TCP报文段长度信息。与UDP一样,伪报头并不在网络中传输,并且在校验和中包含伪报头的目的是为了防止目的主机错误地接收存在路由的错误数据报。
伪首部,又称伪报头:指在TCP的分段或者是UDP的数据报格式中,在数据报首部增加,源IP地址,目的IP地址,IP分组协议字段,TCP或者UDP数据报的总长度,构成的扩展首部结构,共12个字节。伪首部是个临时结构,不向下也不向上传递,仅仅是为了保证校验套接字的正确性。
其数据结构如下所示:
//伪头部:用于TCP/UDP计算CheckSum
//填充字段值来自IP层
typedef struct tag_pseudo_header
{
u_int32_t source_address; //源IP地址
u_int32_t dest_address; //目的IP地址
u_int8_t ?placeholder; //必须置0,用于填充对齐
u_int8_t ?protocol; //协议号(IPPROTO_TCP=6,IPPROTO_UDP=17)
u_int16_t tcplength; //UDP/TCP头长度(不包含数据部分)
}PseudoHeader_S;
伪首部的结构示意图如下所示:


仅在 URG = 1 时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据),即指出了紧急数据的末尾在报文中的位置,注意:即使窗口为零时也可发送紧急数据。例如,**如果报文段的序号是 1000,前 8 个字节都是紧急数据,那么紧急指针就是 8 **。紧急指针一般用途是使用户可中止进程。
可选项的格式如下所示:

常用的可选项有以下几种:
timestamp 时间戳MSS 允许接收的最大报文段SACK选择确认选项Window Scale 窗口缩放因子可选项的长度可变,最长可达 40 字节,当没有使用可选项时,TCP 首部长度是 20 字节。
用于保证选项大小为32位的整数倍。
真正需要进行传输的数据。
UDP报头
UDP 是 User Datagram Protocol 的简称, 中文名是用户数据报协议,是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
2个字节,发送方端口号
2个字节,接收方端口号
2个字节,UDP用户数据报的总长度,以字节为单位。
2个字节,检测UDP用户数据报在传输过程中是否出错,出错直接丢弃。

真正需要传输的数据。需要注意的是,UDP 的数据部分如果不为偶数需要用 0 填补,就是说,如果数据长度为奇数,数据长度加“1”。
TCP协议其实是一个传输控制协议,顾名思义,就是用来传输数据的。如果从socket角度来看TCP的话,就是下面这样的:

发送端发送的数据放入发送缓存区Send Buffer中,接收端要把接收到的数据放入接收缓存区Receive Buffer中,上层应用程序们不停地塞取数据,完成数据的传输。
在这个传输过程中,有个很明显的问题,就是Send Buffer和Receive Buffer都是有限的,万一满了溢出了怎么办。这个时候就是流量控制派上用场的时候了。
流量控制做的事情就是,如果Receive Buffer接收缓存区满了之后,发送端此时就要停止发送数据,以便接收端及时处理完已接收的数据。那发送端如何知晓接收端的缓冲区已满了呢?
为了控制发送端的发送速率,接收端会告知发送端自己的接收窗口大小,即Receive Buffer接收缓存区中空闲的部分,如下所示:

接收端在每次接收完发送端发送的数据之后,都会应答一个ACK报文,在这个报文中,接收端会告知发送端此时自己的接收窗口。
TCP中的窗口并不是固定的,由于报头、网络阻塞等影响,TCP中窗口的大小一般会变化。这就是滑动窗口的由来,可以根据实际环境因素,动态地调整。滑动窗口分为两种,发送窗口和接收窗口。
TCP报文中的数据,发送端的数据根据状态不同可以分为以下四种:

ACK的数据包;ACK的数据包,如果在一段时间内都没有收到接收端的ACK,则会进行重传;发送窗口表示某个时刻,发送端能拥有的最大的可以发送但允许未被确认的数据包大小,也是发送端被允许发送的最大的数据包大小,就是上图中2与3的和。
可用窗口是指发送端还能发送的最大数据包大小,等于发送窗口的大小减去已发送但未被确认的数据包大小,即上图中3的部分,未发送但可以被接收端接收的数据包大小。
对于发送窗口来说,其左边界为成功发送且已经被接收方确认的最大字节序号,窗口的右边界是发送方当前可以发送的最大字节序号,滑动窗口的大小即为右边界减去左边界。

当上图的可用窗口的6个字节数据(46~51)发送出去,可用窗口大小就变成了0,这个时候除非接收到来自接收端的ACK,否则发送端不再接收数据。
每次成功发送数据之后,发送窗口就会在发送缓存区中按顺序移动,将新的数据包含到窗口中准备发送。
TCP`报文中的数据,接收端的数据根据状态不同可以分为以下三种:

ACK的数据包;当收到数据包后,将窗口向前移动一个位置,并发回ACK,若收到的数据落在接收窗口之外,则一律丢弃。
这里我们不用太复杂的例子,以一个最简单的来回来模拟一下流量控制的过程,方便大家理解。
首先双方三次握手,初始化各自的窗口大小,均为 200 个字节。
假如当前发送端给接收端发送 100 个字节,那么此时对于发送端而言,SND.NXT 当然要右移 100 个字节,也就是说当前的可用窗口减少了 100 个字节,这很好理解。
现在这 100 个到达了接收端,被放到接收端的缓冲队列中。不过此时由于大量负载的原因,接收端处理不了这么多字节,只能处理 40 个字节,剩下的 60 个字节被留在了缓冲队列中。
注意了,此时接收端的情况是处理能力不够用啦,你发送端给我少发点,所以此时接收端的接收窗口应该缩小,具体来说,缩小 60 个字节,由 200 个字节变成了 140 字节,因为缓冲队列还有 60 个字节没被应用拿走。
因此,接收端会在 ACK 的报文首部带上缩小后的滑动窗口 140 字节,发送端对应地调整发送窗口的大小为 140 个字节。
此时对于发送端而言,已经发送且确认的部分增加 40 字节,也就是 SND.UNA 右移 40 个字节,同时发送窗口缩小为 140 个字节。
这也就是流量控制的过程。尽管回合再多,整个控制的过程和原理是一样的。
前面说的流量控制一般发生在发送端和接收端之间,并没有考虑整个网络环境的影响。当网络特别差时,容易发生丢包现象,此时发送端就要进行一定的控制,防止过多的数据注入到网络中。这个时候,拥塞控制就派上用场了。
对于发送端来说,它需要维护两个状态变量,拥塞窗口(Congestion Window,cwnd)和慢启动阈值(Slow Start Threshold)。
拥塞窗口是指发生端目前还能传输的数据量大小。它与之前介绍的接收窗口有所不同:
在之前介绍发送窗口的时候,我们也提到过,发送窗口会根据接收端返回的ACK信息进行调整,那么这两个窗口发送窗口(swnd)呢?答案是取二者之中最小的那个,如下所示:
swnd=min(rwnd , cwnd)
拥塞控制就是用来控制拥塞窗口cwnd的大小。拥塞控制过程中涉及到几种算法,分别是:
当发送端和接收端通过三次握手建立连接后,接下来就要开始传输数据了。由于发送端此时不知道现在的网络处于什么状况,如果一下子发送大量的数据,则可能会大量丢包。因此,拥塞控制首先采用一种名为慢启动的算法来试探网络的拥塞情况。主要原理就是,当主机开始发送数据时,由小到大逐渐增大拥塞窗口数值(即 发送窗口数值),从而逐渐增大发送报文段,流程如下:
MSS;ACK,则将拥塞窗口大小加倍。就是说,每经过一个轮次,即RTT(route-trip time),拥塞窗口cwnd加倍;如下图所示:

需要注意的是,这里的慢并不是指传输慢或者是增长速率慢,而是指一开始发送报文段时拥塞窗口cwnd设置的比较小,使得发送方在开始时只发送一个报文段,主要是试探一下网络的拥塞情况。
难道一直会一直翻倍下去?肯定不会啊,所以慢启动阈值就派上用场了。当拥塞窗口的大小达到这个阈值之后,拥塞避免就要开始上场了。
拥塞避免算法可以使得拥塞窗口按线性规律缓慢增长。慢启动阶段每一个轮次拥塞窗口加倍,但到了拥塞避免阶段,拥塞窗口每一个轮次都只增加1,如下图所示:

需要注意的是,拥塞避免并不能避免拥塞,只是将拥塞窗口的增长减缓,减少网络当中的数据数量。
借助于下图进行慢启动和拥塞避免的具体分析。

过程分析:
cwnd小于慢启动阈值ssthres时,使用慢启动算法;cwnd大于慢启动阈值ssthres时,停止使用慢启动,开始使用拥塞避免算法;ssthres设置为出现拥塞时发送窗口的一半,但必须大于等于2;把拥塞窗口重新设置为1;cwnd小于慢启动阈值ssthres,开始使用慢启动算法;cwnd大于慢启动阈值时,停止使用慢启动,开始使用拥塞避免算法;这两种算法结合可以在出现网络拥塞时迅速减少主机发送到网络中的分组数,使得发送拥塞的路由器有足够的时间把队列中积压的分组处理完毕。
注
二者合并为AIMD算法,即加法增大,乘法减小。
在TCP传输数据的过程,如果发生了丢包,即接收端发现数据段不是按序到达的,此时快重传就要上场了,以便及时提醒发送端存在丢失报文段的现象,提高整个网络的吞吐量。
原理:
快重传的过程如下所示:

在传输的过程中,接收端在接收了M1、M2后,紧接着收到了M4,则M4就是失序报文段;
此时接收端就接着发送对M2的重复确认;
接着M5,M6又到达了接收端,则接收端继续发送两个对M2的重复确认;
至此,发送端收到四个对M2的确认,后三个都是重复确认;
接着发送端就会立即重传接收端尚未收到的M3,而不必等到设定的重传计时器到期;
好了,上面说到如果出现丢失报文段,发送端就会进行重传,那么重传哪段呢?
在上面快重传的流程分析过程中,当M3丢失之后,接收端会告知发送端进行重发,但重发哪一段呢?答案是只会重传丢失的那一段,如何实现呢?
接收端发送的重复确认的报文中,在报文首部中有个SACK选项,通过left edge和right edge这两个值来告知发送端已经接收到了哪些区间的数据报。因此,即使M3报文丢失了,当收到M4、M5、M6后,接收端会告知发送端,这几个报文正常接收到了,剩下M2这个报文没到,就只需要重传M3这个报文即可。这个过程也称之为**选择性重传(SACK,Selective Acknowledgment),解决如何重传的问题。
当发送端连续收到3个重复确认之后,发现出现丢包现象,觉得现在的网络已经有些拥塞,自己会进入快恢复阶段,如下所示:

流程如下:
TCP连接刚刚建立,使用慢启动算法;注:快恢复算法中,慢启动的应用场景,TCP连接建立和网络超时两种情况。

快重传和快回复是对慢启动和拥塞避免的改进。
流量控制过程中,也要考虑到传输效率的问题。
Negle算法主要是为了避免网络中存在大量的小包(TCP报头占比过大)造成拥塞。Negle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。Negle算法要求一个TCP连接上最多只有一个未被确认的未完成的小分组,在该组的确认未到达之前不能发送其他的小分组。
Negle算法主要是为了提高网络的吞吐量,总结如下:
第一次发送数据时,不用管包大小,不用等待,直接发送;
后面满足下面的条件之一就可以继续发送:
之前所有的包都ACK
数据包的大小达到最大段大小(MSS);
等待一定的时间(一般为200ms);
紧急发送;
设想一种场景:当接收端的缓存已满,但是上层的应用程序每次只读取一个字节的数据,然后向发送端回应一个ACK,并将接收窗口设置为1个字节。于是发生端接着发送了一个字节的数据过来,接收端继续确认,循环往复,致使传输的效率很低。
解决办法:让接收端等待一段时间,使其满足下面条件之一:
只有满足其中一个条件,接收端就可以回应ACK,向发送端通知自己当前的接收窗口大小。另外,发送端也不要发送太小的数据包,而是把数据积累成足够大的报文段,或者是达到接收端缓冲区一半大小。
试想这样一个场景,当我收到了发送端的一个包,然后在极短的时间内又接收到了第二个包,那我是一个个地回复,还是稍微等一下,把两个包的 ACK 合并后一起回复呢?
延迟确认(delayed ack)所做的事情,就是后者,稍稍延迟,然后合并 ACK,最后才回复给发送端。TCP 要求这个延迟的时延必须小于500ms,一般操作系统实现都不会超过200ms。
不过需要主要的是,有一些场景是不能延迟确认的,收到了就要马上回复:
tcp_in_quickack_mode设置);TCP 层面也是有keep-alive机制,而且跟应用层不太一样。
试想一个场景,当有一方因为网络故障或者宕机导致连接失效,由于 TCP 并不是一个轮询的协议,在下一个数据包到达之前,对端对连接失效的情况是一无所知的。
这个时候就出现了 keep-alive, 它的作用就是探测对端的连接有没有失效。
在 Linux 下,可以这样查看相关的配置:
sudo sysctl -a | grep keepalive
// 每隔 7200 s 检测一次
net.ipv4.tcp_keepalive_time = 7200
// 一次最多重传 9 个包
net.ipv4.tcp_keepalive_probes = 9
// 每个包的间隔重传间隔 75 s
net.ipv4.tcp_keepalive_intvl = 75
不过,现状是大部分的应用并没有默认开启 TCP 的keep-alive选项,为什么?
站在应用的角度:
因此是一个比较尴尬的设计。
由于TCP连接是无边界的,这就导致数据在传输过程中出现粘包问题。什么是粘包?TCP报文粘连是指,本来发送的多个TCP报文,但是在接收端收到的却是一个报文,把多个报文合并成了一个。由于UDP传输的报文是有边界的(两段消息间存在界限),所以其不会出现粘包现象。粘包有可能发生在发送端,也有可能发生在接收端。TCP产生粘包的原因有两种,下面详细解释一下。
接收端会把接收到的数据存放在缓存区当中,然后通知上层应用去取数据。当应用层由于某些原因不能及时的把数据取走,则会造成接收缓存区存放了多条报文,由此产生报文粘连现象。
上面介绍到的Negle算法时说到,Negle算法是为了解决网络中存在大量的小包(数据报首部占比过大),从而导致的网络拥塞。因此,当开启Negle算法时,当有数据需要发送的时候,先不发送,而是稍微等待一会,看看在这一小段时间内,还有没有其他需要发送的数据,然后再把需要发送的数据一次性发送出去。这样虽然可以提高网络的吞吐量和利用率,但是当发送端缓存区存放着几条报文时,就有可能产生报文粘连的现象。
总结下来就是,发送端没有及时发送,接收端没有及时清除,这样才导致了粘包现象。
Negle算法,在SOCKET选项中,TCP_NODELAY表示关闭Negle算法;TCP每次建立连接的时候都要进行三次握手,很是麻烦,于是后面出现了TCP握手流程的优化,即TCP 快速打开(TCP Fast Open, TFO)。
首先客户端发送SYN给服务端,服务端接收到。
注意哦!现在服务端不是立刻回复 SYN + ACK,而是通过计算得到一个SYN Cookie, 将这个Cookie放到 TCP 报文的 Fast Open选项中,然后才给客户端返回。
客户端拿到这个 Cookie 的值缓存下来。后面正常完成三次握手。
首轮三次握手就是这样的流程。而后面的三次握手就不一样啦!
在后面的三次握手中,客户端会将之前缓存的 Cookie、SYN 和HTTP请求(是的,你没看错)发送给服务端,服务端验证了 Cookie 的合法性,如果不合法直接丢弃;如果是合法的,那么就正常返回SYN + ACK。
重点来了,现在服务端能向客户端发 HTTP 响应了!这是最显著的改变,三次握手还没建立,仅仅验证了 Cookie 的合法性,就可以返回 HTTP 响应了。
当然,客户端的ACK还得正常传过来,不然怎么叫三次握手嘛。
流程如下:

注意: 客户端最后握手的 ACK 不一定要等到服务端的 HTTP 响应到达才发送,两个过程没有任何关系。
TFO 的优势并不在与首轮三次握手,而在于后面的握手,在拿到客户端的 Cookie 并验证通过以后,可以直接返回 HTTP 响应,充分利用了1 个RTT(Round-Trip Time,往返时延)的时间提前进行数据传输,积累起来还是一个比较大的优势。
timestamp是 TCP 报文首部的一个可选项,一共占 10 个字节,格式如下:
kind(1 字节) + length(1 字节) + info(8 个字节)
其中 kind = 8, length = 10, info 有两部分构成: timestamp和timestamp echo,各占 4 个字节。
那么这些字段都是干嘛的呢?它们用来解决那些问题?
接下来我们就来一一梳理,TCP 的时间戳主要解决两大问题:
RTT(Round-Trip Time)在没有时间戳的时候,计算 RTT 会遇到的问题如下图所示:

如果以第一次发包为开始时间的话,就会出现左图的问题,RTT 明显偏大,开始时间应该采用第二次的;
如果以第二次发包为开始时间的话,就会导致右图的问题,RTT 明显偏小,开始时间应该采用第一次发包的。
实际上无论开始时间以第一次发包还是第二次发包为准,都是不准确的。
那这个时候引入时间戳就很好的解决了这个问题。
比如现在 a 向 b 发送一个报文 s1,b 向 a 回复一个含 ACK 的报文 s2 那么:
a 向 b 发送的时候,timestamp 中存放的内容就是 a 主机发送时的内核时刻 ta1。b 向 a 回复 s2 报文的时候,timestamp 中存放的是 b 主机的时刻 tb, timestamp echo字段为从 s1 报文中解析出来的 ta1。 a 收到 b 的 s2 报文之后,此时 a 主机的内核时刻是 ta2, 而在 s2 报文中的 timestamp echo 选项中可以得到 ta1, 也就是 s2 对应的报文最初的发送时刻。然后直接采用 ta2 - ta1 就得到了 RTT 的值。现在我们来模拟一下这个问题。
序列号的范围其实是在0 ~ 2 ^ 32 - 1, 为了方便演示,我们缩小一下这个区间,假设范围是 0 ~ 4,那么到达 4 的时候会回到 0。
| 第几次发包 | 发送字节 | 对应序列号 | 状态 |
|---|---|---|---|
| 1 | 0 ~ 1 | 0 ~ 1 | 成功接收 |
| 2 | 1 ~ 2 | 1 ~ 2 | 滞留在网络中 |
| 3 | 2 ~ 3 | 2 ~ 3 | 成功接收 |
| 4 | 3 ~ 4 | 3 ~ 4 | 成功接收 |
| 5 | 4 ~ 5 | 0 ~ 1 | 成功接收,序列号从0开始 |
| 6 | 5 ~ 6 | 1 ~ 2 | ??? |
假设在第 6 次的时候,之前还滞留在网路中的包回来了,那么就有两个序列号为1 ~ 2的数据包了,怎么区分谁是谁呢?这个时候就产生了序列号回绕的问题。
那么用timestamp就能很好地解决这个问题,因为每次发包的时候都是将发包机器当时的内核时间记录在报文中,那么两次发包序列号即使相同,时间戳也不可能相同,这样就能够区分开两个数据包了。
TCP 具有超时重传机制,即间隔一段时间没有等到数据包的回复时,重传这个数据包。
那么这个重传间隔是如何来计算的呢?
今天我们就来讨论一下这个问题。
这个重传间隔也叫做超时重传时间(Retransmission TimeOut, 简称RTO),它的计算跟上一节提到的 RTT 密切相关。这里我们将介绍两种主要的方法,一个是经典方法,一个是标准方法。
经典方法引入了一个新的概念——SRTT(Smoothed round trip time,即平滑往返时间),每产生一次新的 RTT 就根据一定的算法对 SRTT 进行更新,具体而言,计算方式如下(SRTT 初始值为0):
SRTT = (α * SRTT) + ((1 - α) * RTT)
其中,α 是平滑因子,建议值是0.8,范围是0.8 ~ 0.9。
拿到 SRTT,我们就可以计算 RTO 的值了:
RTO = min(ubound, max(lbound, β * SRTT))
β 是加权因子,一般为1.3 ~ 2.0, lbound 是下界,ubound 是上界。
其实这个算法过程还是很简单的,但是也存在一定的局限,就是在 RTT 稳定的地方表现还可以,而在 RTT 变化较大的地方就不行了,因为平滑因子 α 的范围是0.8 ~ 0.9, RTT 对于 RTO 的影响太小。
为了解决经典方法对于 RTT 变化不敏感的问题,后面又引出了标准方法,也叫Jacobson / Karels 算法。
一共有三步。
第一步: 计算SRTT,公式如下:
SRTT = (1 - α) * SRTT + α * RTT
注意这个时候的 α跟经典方法中的α取值不一样了,建议值是1/8,也就是0.125。
第二步: 计算RTTVAR(round-trip time variation)这个中间变量。
RTTVAR = (1 - β) * RTTVAR + β * (|RTT - SRTT|)
β 建议值为 0.25。这个值是这个算法中出彩的地方,也就是说,它记录了最新的 RTT 与当前 SRTT 之间的差值,给我们在后续感知到 RTT 的变化提供了抓手。
第三步: 计算最终的RTO:
RTO = μ * SRTT + ? * RTTVAR
μ建议值取1, ?建议值取4。
这个公式在 SRTT 的基础上加上了最新 RTT 与它的偏移,从而很好的感知了 RTT 的变化,这种算法下,RTO 与RTT变化的差值关系更加密切。
原文:https://www.cnblogs.com/reecelin/p/13396496.html