考虑多跳通信信道,有这些差错种类:
最直接处理分组丢失、比特差错(无法自动纠正的那种)的方法:重发分组直到正确接收。
前提是发送方需要能够判断:
接收方因此需要给发送方一个信号来表明已接收到一个分组,这个方法就是ACK。但有几个问题:
对应解决办法:
对于分组复制、分组乱序问题,解决方法则很简单:每个分组加序列号。
目前为止,上面这套简单ACK机制,就可以实现可靠通信了,然而效率不高。需要允许多个分组进入网络来提高吞吐量:引出窗口的概念。
分组窗口:已被发送方注入但还没完成确认的分组的集合。
窗口大小:窗口中的分组数量。
滑动窗口(sliding window)协议:
流量控制:在接收方跟不上时会强迫发送方慢下来。
两种方式:
拥塞控制: 中间网络出现瓶颈 ,发送方速率可能超过某个路由器的能力,导致丢包,需要降低发送方速度。
流量控制方式:
TCP 提供了一种面向连接的(connection-oriented)、可靠的字节流服务。
报文段:由TCP传给IP的块称为报文段。
可靠性保障关键点:
ISN(c)
(主动打开);FIN
字段的TCP报文 ,带有当前序列号K;这中间如果被动关闭端仍然在发送数据,则称为半关闭。主动关闭端不能再发送数据,但是仍然需要发送ACK ;
FIN
(转变为主动关闭端),序列号为L;FIN
。close、shutdown双方都可以调用,close是全关闭,shutdown能实现半关闭。
TCP 需要 seq 序列号来接受数据(排序、去重)和可靠重传 ,因此两端需要可靠地交换彼此的初始序列号(ISN),通过两对SYN和ACK,其中被动方发出的SYN和ACK可以合并,因此需要三次握手。
因为tcp是全双工通信,断开通信等于要关闭2个方向的数据流。 关闭一个方向需要一对FIN和ACK,关闭两个方向则需要两对。如果第二次和第三次挥手之间没有数据需要发送,则两者有可能合并(延迟确认)。
TCP主动关闭端发送FIN,并接受ACK响应后,另一端仍然可以继续发送数据,此时处于半关闭状态。应用程序只需要调用shutdown()函数来代替基本的close()函数,就能实现上述操作。然而,绝大部分应用程序仍然会调用close()函数来同时关闭一条连接的两个传输方向。
如果使用close()则没有半关闭状态,此时不会确认(ACK)接收到的数据。
同时打开十分少见,通信双方在接收到来自对方的SYN之前必须先发送一个SYN;两个SYN必须经过网络送达对方。该场景还要求通信双方都拥有一个IP地址与端口号,并且将其告知对方。一旦发生,可称其为同时打开。
一个同时打开过程需要交换4个报文段,比普通的三次握手增加了一个。同时关闭同理。
初始序列号需要仔细选择以应对:
上次连接中延迟的失效报文被重新建立的连接当成有效数据;
恶意攻击:只要四元组一样,序列号合适,就能伪造TCP报文段。
现代操作系统通常采用半随机的方法选择初始序列号,比如:
ISN = M + F(localhost, localport, remotehost, remoteport)
M是一个32位计数器,这个计时器每隔4毫秒加1,以防止ISN重叠。
F是一个Hash算法,根据源IP、目的IP、源端口、目的端口生成一个随机数值。散列函数每隔5分钟改变一次。
注:tcp数据长度,是没有在tcp头显式保存的,而是通过ip层的分组长度来算出,tcp数据长度 = ip分组长度 - tcp头长度。
因为TCP头部长度字段以4字节为单位,因此长度应该是四字节的倍数,必要时需要用NOP选项填充。
MSS:
指TCP协议所允许的从对方接收到的最大报文段,通信两端各自于SYN报文段指定。
最大段大小,记录数据(不包括头部)长度,占2个字节。默认为536字节(任何主机至少能处理536字节),IPv4的典型值是1460字节(IPv6为1440字节)
MTU:最大传输单元。路径MTU为1500字节。ipv4:1460+40字节,ipv6:1440+60字节。
65535的MSS是特殊值,ipv6网络中超长数据包会用到,表示无限大。但实际MSS仍受路径MTU限制,所以MSS值为MTU-(40 IPv6头+20 tcp头)
SACK和SACK-Permitted:
WSOPT(或WSCALE):
时间截选项(记作TSOPT或TSopt)要求发送方在每一个报文段中添加2个4字节的时间戳数值。
当使用时间戳选项时,发送方将一个32位的数值填充到时间戳数值字段(称作TSV或TSval)作为时间戳选项的第一个部分;而接收方则将收到的时间戳数值原封不动地填充至第二部分的Timestamp Echo Retry字段(称作TSER或TSecr)。
时间戳只是一个单调增加的数值,两端时间戳并不需要同步。
作用:
MTU指经过两台主机之间路径的所有网络报文中最大传输单元的最小值。
注:一条连接的两个方向的TCP中的路径最大传输单元是不同的 。
当报数据包大小大于MTU时,会在传输过程中被分片,影响吞吐量。因此需要使用路径MTU发现来找到合适的数据包大小。
路径MTU发现过程:
连接建立时,TCP根据对端声明的MSS(没声明则默认536字节)或对外接口的最大传输单元来选择最大段大小。
一旦选定了最大段大小初始值,该方向所有IPv4数据包(IPv6不支持中间节点分片)都会将DF(Don’t Fragment)位置位。分组大小超过某链路MTU时,发送端会收到ICMP PTB(Packet Too Big)消息(携带推荐的MTU信息),得以修改路径MTU。可能需要重复多次。
路由是动态变化的 ,因此每隔10min需要尝试一个更大的数值。
ESTABLISHED是通信双方双向传输数据的状态。从LISTEN到SYN_SENT的状态转换在TCP协议中是合法的,但却不被伯克利套接字所支持,因此比较少见。
TIME_WAIT状态也称为2MSL等待状态。在该状态中,TCP将会等待两倍于最大段生存期(Maximum Segment Lifetime,MSL,常设为30s、1min或2min)的时间。
作用:
TIME_WAIT状态只是限制短时间内与另一端在相同端口上建立新连接,只有一个端口会有该状态。并且只有主动关闭端的端口会有TIME_WAIT状态,因为主动关闭端通常时客户端,使用的是临时端口,不影响使用其他临时端口重新连接。服务端使用的是知名端口,不应该被浪费。
许多实现与API都提供了绕开这一约束的方法。在伯克利套接字API中,SO_REUSEADDR套接字选项就支持绕开操作,即使有TIME_WAIT状态的端口仍然可以使用,不过,TCP的规则仍会防止该端口号被处于2MSL等待状态的同一连接的其他实例重新使用(比如通过时间戳、序列号来防止)。
非半关闭状态下FIN_WAIT_2无限等待问题:
主动关闭方发送FIN并收到ACK就进入了FIN_WAIT_2;
处于FIN_WAIT_2时需要等另一方发送FIN(永久等待);
全关闭时会自动启动定时器,60秒(net.ipv4.tcp_fin_timeout)后若连接处于空闲,则强制进入CLOSED状态。
当发现一个到达的报文段对于相关连接而言是不正确的时, TCP就会发送一个重置报文段(RST位置位的报文段)。
重置报文段通常会导致TCP连接的快速拆卸。
UDP协议规定,当一个数据报到达一个不能使用的目的端口时就会生成一个ICMP目的地不可达(端口不可达)的消息,TCP协议则使用重置报文段来代替完成相关工作(提示Connection refused)。
重置报文的ACK位置位,并且确认号为接收到的SYN序列号+1,以防止伪造的重置报文段攻击。
发送FIN终止连接,叫做有序释放,FIN是在所有排队数据发送完后才被发送出去,不会丢失数据。
但在任意时刻,可以发送重置报文代替FIN来终止连接,称为终止释放,任何排队数据都被抛弃,重置报文段被立即发送出去。
套接字API将SO_LINGER套接字选项数值设为0来实现终止释放。
如果一端在未告知另一端情况下关闭或终止连接(比如主机崩溃),那么该条TCP处于半开状态。
只要不尝试通过半开连接传输数据,正常工作的一端将不会检测出另一端已经崩溃(除非使用了keepalive选项)。
再次传输数据时,会接收到重置响应。
处于LISTEN状态的本地节点(监听套接字)会独自地运行。它用于为并行服务器接收未来可能出现的请求。当有新的连接请求到达并被接收时,操作系统中的TCP模块创建处于ESTABLISHED状态的新节点。其端口号与LISTEN状态时相同。
只有处于LISTEN状态的节点能接受SYN报文段,接受进入的连接请求。
TCP允许一台服务器为一个完全指定的外部节点被动打开,不过普通的伯克利套接字API没有提供这一实现。
在被用于应用程序之前新的连接可能会处于下述两个状态:
因此在内部操作系统通常会使用两个不同的连接队列分别对应上述两种不同的情况。
在现代Linux内核中,套接字API(通过backlog)可以控制第二种状况下的连接数目(ESTABLISHED状态的连接),因此,应用程序能够限制完全形成的等待处理的连接数目。在Linux中,将会适用以下规则:
在借助伯克利套接字实现的TCP中,当应用程序被告知一条连接已经到达时,TCP的三次握手过程已经完成。上述行为也意味着一个TCP服务器无法让一个客户端的主动打开操作失败。如果此后服务器决定不向该客户端提供服务,那它只能关闭(发送一个FIN)或者重置这条连接(发送一个RST)。
SYN Flood是一种TCP拒绝服务攻击(Dos),恶意攻击者发送大量伪造源IP的SYN报文给服务器,使其浪费资源维持大量半打开连接,导致拒绝后续合法连接请求。
一种解决方法为SYN cookie:
服务器不会为SYN报文分配任何资源,而是用四元组以及一些仅有该服务器知道的参数进行散列,作为SYN+ACK报文段的初始序列号。
如果客户端合法,则返回ACK报文(确认号为ISN(s)+1)。服务器收到后,用四元组和参数的哈希验证ACK报文确认号是否正确。
SYN Cookies 是根据以下规则构造的初始序号:
初始 TCP 序号,也就是所谓的SYN cookie,按照如下算法得到:
缺陷:MSS大小有限,服务端无法保存TCP选项,因此只能丢弃选项。所以SYN Cookie并未作为默认设置。
攻击者伪造一个ICMP PTB消息,消息包含一个非常小的MTU值(如68字节),就迫使受害者的TCP尝试采用非常小的数据包来填充数据,大大降低了性能。禁用最大MTU发现功能就能抵御这个攻击。
就是对已建立的连接插入攻击数据的攻击。解决办法应该是用校验码。
破坏会改变现有TCP连接的行为。
例如伪造RST报文段:前提是序列号要在该连接的序列号范围内。
抵御方式:认证每一个报文段(TCP-AO);要求RST报文段拥有一个特殊的序列号而不只是在范围内的序列号、要求时间戳选项具有特定的数值。
TCP根据接收端返回至发送端的一系列确认信息来判断是否出现丢包。当数据段或确认信息丢失,TCP启动重传操作,重传尚未确认的数据。
TCP拥有两套独立机制来完成重传,一是基于时间,二是基于确认信息的构成。第二种方法通常比第一种更高效。
每次重传间隔时间加倍称为二进制指数回退。
TCP拥有两个阈值来决定如何重传一个报文段:
R1和R2分别至少设为三次重传和100s。SYN除外,至少设为3min。
Linux中,对于一般数据,R1 R2可以通过应用程序,或使用系统变量(单位都是重传次数)设置。
tcp_retries1(R1):连接已经建立后,基本重传次数。次数达到后,会先尝试让网络层更新路由再继续发包。默认为3次。
tcp_retries2(R2):连接已经建立后,最大重传次数。次数达到后,会断开连接。默认为15,对应13~20min。该值又称为TcpMaxDataRetransmissions,是最主要的值。
对于SYN报文段,变量net.ipv4.tcp_syn_retries和net.ipv4.tcp_synack_retries限定重传次数,默认值为5(约180秒)。
RTT的测量可以采用两种方法:
方法1无法分辨是原始数据包还是重传数据包,而时间戳可以。
TCP超时和重传的基础是怎样根据连接的RTT设置RTO。
# 采用如下公式得到平滑的RTT估计值(称为SRTT):
SRTT ← α(SRTT)+(1-α)RTTs (s为下标)
# SRTT是基于现存值和新的样本值RTTs得到的更新结果,常量α是平滑因子,推荐值为0.8~0.9,这个方法被称为指数加权移动平均(EWMA)或者低通过滤器。
# 采用以下公式设置RTO:
RTO = min(ubound,max(1bound,(SRTT)β))
# β为时延离散因子,推荐值为1.3~2.0,ubound为RTO的上边界(可设定建议值,如1分钟),lbound为RTO的下边界(可设定建议值,如1秒)。它使得RTO的值设置为1秒或约2倍的SRTT。
相对于稳定的RTT来说,这种方法能取得不错的性能,然而若TCP运行于RTT变化较大的网络中,则无法获得期望的效果。
结合了平均值和平均偏差来进行估算,更适应RTT变化幅度较大的情况。
srtt ← (1 - g)(srtt) + (g)M
rttvar ← (1 - h)(rttvar) + (h)(|M - srtt|)
RTO = srtt + 4(rttvar)
# srtt代替了之前的SRTT,且rttvar为平均偏差的EWMA,M代替了之前的RTTs。
便于计算机计算的形式:
Err = M - srtt
srtt ← srtt + g(Err)
rttvar ← rttvar + h(|Err| - rttvar)
RTO = srtt + 4(rttvar)
# srtt为均值的EWMA,rttcar为绝对误差|Err|的EWMA,Err为测量值M与当前RTT估计值srtt之间的偏差。
增量g为新RTT样本M占srtt估计值的权重,取1/8,增量h为新平均偏差样本(新样本M与当前平均值srtt之间的绝对误差)占偏差估计值rttvar的权重,取1/4。
TCP时钟一个"滴答"的时间长度称为粒度,通常该值相对较大(约500ms),标准方法适用于该粒度。但近期实现的时钟使用更细的粒度(如linux的1ms)。
根据[RFC2018],RTO的初始值设为1s,第一个SYN报文段采用的超时间隔为3s。当得到首个RTT样本值M后,来根据下面的式子初始化srtt和rttvar,然后算出RTO。
srtt <-- M
rttvar <-- M/2
TCP并非对其接收到的每个报文段都返回ACK,例如,当传输大批量数据时,TCP通常采取每两个报文段返回一个ACK的方法。
另外,当数据出现丢失、失序或重传成功时,TCP的累积确认机制表明报文段与其ACK之间并非严格的一一对应关系。
为解决这些问题,使用时间截选项的TCP(大部分的Linux和Windows版本都包含)采用如下算法来测量RTT样本值:
TCP发送端在其发送的每个报文段的TSOPT的TSV部分携带一个32比特的时间戳值。该值包含数据发送时刻的TCP时钟值。
接收端记录接收到的TSV值(名为TsRecent的变量)并在对应的ACK中返回,并且记录其上一个发送的ACK号(名为LastACK的变量)。
当一个新的报文段到达(接收端)时,如果其序列号与LastACK的值吻合(即为下一个期望接收的报文段),则将其TSV值存入TsRecent。
接收端发送的任何一个ACK都包含TSOPT,TsRecent变量包含的时间戳值被写入其TSER部分。
发送端接收到ACK后,将当前TCP时钟减去TSER值,得到的差即为新的RTT样本估计值。
FreeBSD,Linux以及近期的Windows版本都默认启用时间戳选项。在连接初始化过程中,TCP通信的一方使用时间戳,则另一方默认启用。
与标准方法稍微有点差别,因为标准方法发布的时候,时钟粒度普遍为500ms,而linux的时候,时钟粒度为1ms。linux采用更频繁的RTT测量与更细的时钟粒度,这会提高RTT的准确度。但是,
为了解决这两个问题。linux新增了两个新的变量,mdev和mdev_max,mdev记录瞬时平均偏差估计值,mdev_max记录测量过程中最大的mdev,它的下限是50ms。
rttvar需要定期更新保证其不小于mdeg_max,第1个问题解决了。
用标准方法得到初始srtt rttvar RTO后,采用下面的方法估计RTO:
m为得到的RTT。
// 计算srtt,与标准方法一样
srtt = (7/8)*srtt + (1/8)*m
// 若m小于RTT的下界,就减小它的权重。
if(m < (srtt - mdev)
mdev = (31/32)*mdev + (1/32)*|srtt-m|
else
mdev = (3/4)*mdev +(1/4)*|srtt-m|
mdev_max = max(mdev_max, mdev)
// 计算rttvar
rttvar = mdev_max
// 得到RTO,这里与标准方法一样。
RTO = srtt + 4*rttvar
转自:https://www.cnblogs.com/K-Cream/p/14088076.html
设定计时器,记录被计时的报文段序列号,若及时收到该报文段的ACK,那么计时器被取消。之后发送新数据报时,设定新计时器,并记录新序列号。如此重复。(同一时间只有一个计时器运行)
若在设定的RTO内,没收到ACK,就会触发超时重传。
TCP将超时重传视为相当重要的事件,当发生这种情况时,它通过降低当前数据发送率来对此进行快速响应。实现方法:
当网络无法正常传输时,重传计时器为TCP连接提供了“最后一招的重新启动”。但大多数情况下,计时器超时并触发重传是不必要的,会导致网络利用率下降。快速重传更加高效。
当接收到失序报文段时,TCP需要立即生成确认信息(重复ACK),并且失序情况表明接收端缓存出现了空缺。
当发送端收到重复ACK的次数(不包括第一个非重复ACK和窗口更新ACK)达到重复ACK阈值(dupthresh,通常为3),就会重传对应的分组。 当然也可以同时发送新数据。
不采用SACK时,在接收到有效ACK前至多只能重传一个报文段。采用SACK,ACK可包含额外信息,使得发送端在每个RTT时间内可以填补多个空缺。
下图可通过Wireshark的“统计ITCP流图1时间序列图"(Statistics | TCP Stream Graph | Time-Sequence Graph)功能得到。
NewReno算法:
恢复点:发送端在执行重传前发送的最大序列号。
当收到新的ACK,如果ACK值小于恢复点(称为部分ACK),不足以到达恢复点,那么TCP发送端立即发送可能丢失的报文段,直到ACK到达或超过恢复点,重传结束,恢复阶段结束。(不需要多次重复发送重复ACK)
接收端在TCP连接建立期间(SYN报文段)收到SACK许可选项即可生成SACK。
当采用SACK选项时,一个ACK可包含三四个告知失序数据的SACK信息。每个SACK信息包含2个32位的序列号,代表接收端存储的失序数据的起始至最后一个序列号(加1)。
SACK选项指定n个块的长度为8n+2字节,并且通常SACK会与TSOPT一同使用,因此需要额外的10个字节,意味着SACK在每个ACK中只能包含三个块。包含一个或多个SACK块的ACK有时也简单称为"SACK"。
因为SACK可能丢失,所以多个SACK中可能会出现重复的块信息。
0.967s时刻到达的SACK包含两个块:[28001,29401]和[25201,26601]。该SACK为序列号23801的重复ACK,表明接收端现在需要起始序列号为23801和26601的两个报文段。发送端立即响应,启动快速重传,但由于拥塞控制机制,发送端只重传了一个报文段23801,随着另外的两个ACK的到达,发送端被允许发送第二个重传报文段26601。
SACK快速重传不需要三次重复ACK,TCP更早地启动了重传,但恢复的出口本质上是一致的。
发送端采用SACK并不能百分百地提高整传输性能。但在RTT较大,丢包严重的情况下,SACK的优势就能很好地体现出来,因为在这样的环境下,一个RTT内能填补多个空缺显得尤为重要。
在很多情况下,即使没有出现数据丢失也可能引发重传。这种不必要的重传称为伪重传(spurious retransmission),其主要造成原因是伪超时(spurious timeout),即过早判定超时,其他因素如包失序、包重复,或ACK丢失也可能导致该现象。在实际RTT显著增长,超过当前RTO时,可能出现伪超时。
处理伪超时问题的方法通常包含检测(detection)算法与响应(response)算法。检测算法用于判断某个超时或基于计时器的重传是否真实,一旦认定出现伪超时则执行响应算法,用于撤销或减轻该超时带来的影响。
检测方法有: D-SACK、Eifel检测算法、F-RTO。
回退N步(GBN,go-back-N):连续发了n个报文段,如果网络突然缓慢,最前面的那个超时了,会触发重传,此时后面的n-1个包也没被确认,那么n个报文段都重传(回退)。
在接收端采用DSACK(重复SACK),并结合通常的(开启了)SACK发送端,可在第一个SACK块中告知接收端收到的重复报文段序列号。DSACK的主要目的是判断何时的重传是不必要的,并了解网络中的其他事项。因此发送端至少可以推断是否发生了包失序、ACK丢失、包重复或伪重传。
原理:SACK接收端的变化在于,允许包含序列号小于(或等于)累积ACK号字段的SACK块。
利用了TCP的TSOPT来检测伪重传。
发生超时重传时,Eifel算法等待接收下一个ACK,若为针对第一次传输(即原始传输)的ACK(用时间戳判断),则判定该重传是伪重传。
F-RTO是检测伪重传的标准算法,它不需要任何TCP选项。该算法只检测由重传计时器超时引发的伪重传,对之前提到的其他原因引起的伪重传则无法判断。
F-RTO会修改TCP的行为,在超时重传后收到第一个ACK时,TCP会发送新的数据,之后再响应后一个到达的ACK。如果其中有一个为重复ACK,则认为此次重传没问题,如果两个都不是重复ACK,则表示该重传是伪重传。
如果新数据的传输得到了相应的ACK,就使得接收端窗口前移。如果新数据的发送导致了重复ACK,那么接收端至少有一个或更多的空缺。
说白了就是更新srtt和rttvar。因为伪超时导致临时修改了srtt和rttvar(RTO变了),检测发现伪超时,那就得恢复到正常的srtt、rttvar。
一旦判断出现伪重传,则会引发一套标准操作,即Eifel响应算法。原则上超时重传和快速重传都可使用Eifel响应算法,但目前只针对超时重传做了相关规定。
Eifel响应算法根据是否能尽早或较迟检测出伪超时的不同而有所区别。前者称为伪超时,通过检查ACK或原始传输来实现;后者称为迟伪超时,基于有超时而引发的重传所返回的ACK来判定。
在重传计时器超时后,它会查看srtt和rttvar的值,并按如下方式记录新的变量srtt_prev和rttvar_prev:
srtt_prev = srtt + 2(G)
rttvar_prev = rttvar
在任何一次计时器超时后,都会指定这两个变量,但只要在判定出现伪超时才会使用它们,用于设定新的RTO。在上式中,G代表TCP时钟粒度。srtt_prev设为rtt加上两倍的时钟粒度是由于srtt的值过小,可能会出现伪超时。
完成srtt_prev和rttvar_prev的存储后,就要触发某些检测算法。运行检测算法后可得到一个特殊的值,称为伪恢复。如果检测到一次伪超时,则将伪恢复置为SPUR_TO。如果检测到迟伪超时,则将其置为LATE_SPUR_TO。
若伪恢复为SPUR_TO,TCP可在恢复阶段完成之前进行操作,通过将下一个要发送报文段(称为SND.NXT)的序列号修改为最新的未发送过的报文段(称为SND.MAX)。这样就可在首次重传后避免不必要的"回退N"行为。
如果检测到一次迟伪超时,此时已生成对首次重传的ACK,则SND.NXT不变。在以上两种情况下,都要重新设置拥塞控制状态(第16章介绍)。一旦接收到重传计时器超时后发送的报文段的ACK,就按如下方式更新srtt、rttvar、RTO:
srtt ← max(srtt_prev,m)
rttvar ← max(rttvar_prev,m/2)
RTO ← srtt + max(G,4(rttvar))
m是一个RTT样本值,它是基于超时后收个发送数据收到的ACK计算得到的。
包传输异常有:失序、重复、丢失。TCP需要区分。
失序原因:
失序问题可能存在于TCP连接的正向或反向链路中:
互联网中严重的失序并不常见,且通过将dupthresh设为相对较小值(3)就能处理大部分情况,或者一些方法可以动态调整dupthresh值,如Linux的TCP实现。
链路层重传时会生成重复副本,TCP可能出现混淆,导致接收端生成一系列重复的ACK,触发伪快速重传。
DSACK能简单地忽略这个问题,因为重复ACK没有包含失序信息,意味着ACK是重复数据。
TCP能不断“学习”发送端与接收端之间的链路特征。学习的结果显示为发送端记录一些状态变量,如srtt和rttvar。一些TCP实现也记录一段时间内已出现的失序包的估计值。
较新的TCP实现将这些数据保存了下来。当创立一个新的连接时,首先查看数据结构中是否存在与该目的端的先前通信信息。如果存在,则选择较近的信息,据此为srtt,rttvar以及其他变量设置初值。
当TCP超时重传,它并不需要完全重传相同的报文段。TCP允许执行重新组包(repacketization),发送一个更大的报文段来提高性能。(不能超过接收端通告的MSS,也不能大于路径MTU)
低速率DoS攻击:
与DoS相关但不同的一种攻击:减慢受害TCP的发送,使RTT估计值过大(过分被动),导致丢包后不会立即重传。
相反的攻击:攻击者在数据发送完成但还未到达接收端时伪造ACK。这样攻击者就能使受害TCP认为连接的RTT远小于实际值,导致过分发送而造成大量的无效传输。
TCP流量研究表明,通常90%或者更多的TCP报文段都包含大批量数据(如Web、文件共享、电子邮件、备份),其余部分则包含交互式数据(如远程登录、网络游戏)。批量数据段通常较大(1500字节或更大),而交互式数据段则会比较小(几十字节的用户数据)。
交互式TCP连接指该连接需要在客户端和服务器之间传输用户输入信息,如按键操作、短消息、操作杆或鼠标操作。对于这些操作,如果用较小的报文段来承载信息,则传输协议需要耗费很高的代价;反之采用较大的报文段则会引入更大的延时,对延迟敏感类应用造成负面影响。因此需要权衡相关因素来找到适合的方法。
以ssh(安全外壳)应用为例,对一个ssh连接,每个交互按键通常都会生成一个单独的数据报(每个按键独立传输)。另外,ssh会在远程系统调用shell,对客户端的输入字符做出回显。
在许多情况下,TCP并不对每个到来的数据包都返回ACK,利用TCP的累积ACK字段可能实现这个功能。累积确认可以允许TCP延迟一段时间发送ACK,以便将ACK和相同方向上需要传的数据结合发送。主机需求RFC[RFC1122]指出,TCP实现ACK延迟的时延应小于500ms,实践中最大取200ms。
采用延时ACK的方法会减少ACK传输数目,减轻网络负载。对于批量数据传输通常为2 : 1的比例。
延迟发送ACK的最大时延可以动态配置,Linux使用了一种动态调节算法,可以在每个报文段返回一个ACK(称为“快速确认”模式)与传统延时ACK模式间相互切换。
ssh连接中,微型数据包内有效数据所占比例甚微,会加重广域网的拥塞。因此,Nagle算法被提出。
Nagle算法要求:
特点:
对比图:
简单来说就是会死锁:
因为客户端最终会发出ACK(延时ACK计时器超时),所以死锁可解。但最终性能变差。
禁用Nagle算法:
流量控制通过可变滑动窗口实现。
每个TCP头部的窗口大小字段表明接收端可用缓存空间的大小,以字节为单位。该字段长度为16位,但可以通过窗口缩放选项来扩大长度。当进程处理数据较慢时,可用存储空间就会减小,窗口大小随之减小。
TCP连接的收发数据量是通过一组窗口结构来维护的。每个TCP活动连接的两端都维护一个发送窗口结构和接收窗口结构。
TCP发送窗口结构图:
窗口运动描述术语:
当得到的ACK号增大而窗口大小保持不变时,称为窗口向前滑动;随着ACK号增大窗口却减小,则左右边界距离减小,当左右边界相等时,称为零窗口。零窗口时,发送端不能再发送新数据,且TCP发送端开始探测对方窗口以伺机增大提供窗口。
接收端也维护一个窗口结构:
该窗口结构记录了已接收并确认的数据,以及它能够接收的最大序列号。该窗口可以保证其接收数据的正确性,特别是接收端希望避免存储重复的已接收和确认的数据以及避免存储不应接收的数据。
零窗口可以有效组织发送端发送数据。当接收端重新获得可用空间时,会给发送端传输一个窗口更新(window update),告知其可继续发送数据。这样的窗口更新通常都不包含数据(为“纯ACK"),不能保证其传输的可靠性。因此TCP必须有相应措施能处理这类丢包,否则会造成死锁。
为防止这种死锁的发生,发送端会采用一个持续计时器间歇性地询问接收端的窗口。当发送方收到了窗口为0的确认,就会启动一个持续计时器,持续计时器超时后会触发窗口探测(window probe)的传输,强制要求接收端返回ACK(其中包含了窗口大小字段)。RFC[RFC1122]建议在一个RTO之后发送第一个窗口探测,随后以指数回退。窗口探测包含一个字节的数据,因此被TCP可靠传输。
基于窗口的流量控制机制,尤其是不使用大小固定的报文段的情况(如TCP),可能会出现称为糊涂窗口综合征(SWS)的缺陷。指当发送端应用进程产生数据很慢、或接收端应用进程处理接收缓冲区数据很慢,或二者兼而有之;就会使应用进程间传送的报文段很小,特别是有效载荷很小。因此耗费资源,传输效率很低。
SWS出现的原因:
避免SWS需遵循的规则:
对于接收端来说,不应通告小的窗口值。[RFC1122]描述的接收算法中,在窗口增至一个全长的报文段(接收端MSS)或者接收端缓存空间的一半(取两者中较小值)之前,不能通告比当前窗口(可能为0)更大的窗口值。可能有两种情况会用到该规则:当应用程序处理接收到的数据后使得可用缓存增大;TCP接收端需要强制返回对窗口探测的响应。
对于发送端来说,不应发送小的报文段,而且需要由Nagle算法控制何时发送。为避免SWS问题,只有指示满足以下条件之一时才能传输报文段:
例外:有时如果不通告小窗口,可能会造成右窗口左移(之前通告过大窗口,接收端接收了大部分数据后,剩下小窗口)。此时,需要违反如上规则,发送小窗口,避免右窗口左移。(避免窗口收缩优于避免SWS)
接受缓存、发送缓存较小会导致TCP应用的吞吐性能差。
这个问题非常严重,因此很多TCP协议栈中上层应用不能指定接收缓存大小。在多数情况下,上层应用指定的缓存会被忽视,而由操作系统来指定一个较大的固定值或者动态变化的计算值。
在Windows和linux中,支持接收窗口自动调优。自动调优连接 的在传输数据值 需要不断被估算,通告窗口值不能小于这个值。这种方法使得TCP达到其最大可用吞吐量,而不必提前在发送端或接收端设置过大的缓存。
自动调优受制于缓存大小。Windows系统中,默认自动设置接收端缓存,Linux可以手动配置。
socket套接字选项MSG_OOB可以将数据标记为“紧急数据”(URG置位)。TCP把这个数据放置在该套接字发送缓冲区的下一个可用位置,并把该连接的TCP紧急指针(urgent pointer)设置成再下一个位置。如果发送多个字节,只有最后一个字节会被标记为紧急数据。
因为窗口大小原因,可能不能立即发出这个字节,但是TCP会知道已经进入紧急状态,所有发出去的包都打开了URG标志。
当有新的紧急指针到达时,接受进程被通知到。内核会给接受套接字的属主进程发送SIGURG
信号,或者接收端使用select()
函数返回异常条件。
接收端的带外数据字节会有两个存储方式:一个是和普通数据一样的在线留存,另外一个是独立的单字节带外缓冲区。
MSG_OOB
调用recv
,recvfrom
,recvmsg
。SO_OOBINLINE
选项(默认关闭)的socket接收数据时,带外数据和普通数据同一通道。详细介绍:《UNP 卷1》24.2
https://blog.csdn.net/qq_40586164/article/details/108717753
拥塞:路由器无法处理高速率到达的流量而被迫丢弃数据信息的现象 。
本地拥塞:在发送端系统产生的丢包称为本地拥塞,产生原因在于,TCP产生数据包的速度大于下层队列的发送速度。
拥塞控制是TCP通信的每一方需要执行的一系列行为,由特定算法规定,用于防止网络因为大规模的通信负载而瘫痪。其基本方法是当有理由认为网络即将进入拥塞状态(或已由于拥塞而出现路由丢包情况)时减缓TCP传输。
注:有线网络中,出现在路由器或交换机中的拥塞是造成丢包的主要原因。而在无线网络中,传输和接收错误是导致丢包的重要因素。
当接收速率和网络传输速率过慢时,便需要降低发送速率。可以在发送端引入一个窗口控制变量,确保发送窗口大小不超过接收端接收能力和网络传输能力。
反映网络传输能力的变量称为拥塞窗口,记作cwnd。因此发送端实际可用窗口W就是接收端通告窗口awnd和拥塞窗口cwnd的较小值:
W = min(cwnd,awnd)
W的值不能过大或过小,我们希望其接近带宽延迟积(BDP),也称为最佳窗口大小。W反映网络中可存储的待发数据量大小,其计算值等于RTT与链路中最小通行速率的乘积。
在传输初始阶段,由于未知网络传输能力,需要缓慢探测可用传输资源,防止短时间内大量数据注入导致拥塞。慢启动算法正是针对这一问题而设计,在数据传输之初或者重传计时器检测到丢包后,需要执行慢启动 [RFC5682]。
TCP以发送一定数目的数据段开始慢启动(在SYN交换之后),称为初始窗口(IW)。IW的值初始设为一个SMSS,但在[RFC5682]中设为一个稍大的值,计算公式如下:
IW = 2*(SMSS)且小于等于2个数据段(当SMSS > 2190字节)
IW = 3*(SMSS)且小于等于3个数据段(当 2190 ≥ SMSS > 1095字节)
IW = 4*(SMSS)且小于等于4个数据段(其他)
SMSS: 发送方能够发送的最大段大小 = min(接收方MSS,路径MTU)
每接收到一个"正确的ACK"(即没出现丢包情况正确返回的数据接收响应ACK),慢启动算法会以min(N,SMSS)来增加cwnd值,N指的是在未经确认的传输数据中能通过"正确的ACK"确认的字节数。
为简单起见,以TCP连接初始的cwnd = 1 SMSS为例。
如果接收端开启了ACK时延,接收端就每接收两个数据报响应一个ACK,那么增速变慢。
cwnd会随着RTT呈指数增长,直至慢启动阈值。
慢启动实例见书16.5.1章。
慢启动一旦达到阈值,便是意味着连接可以进入"下个阶段"了。"下个阶段"指的是为了得到更多的传输资源而不致影响其他连接传输的数据传输,这个阶段TCP实现拥塞避免算法。
拥塞避免阶段,cwnd每次的增长值近似于成功传输的数据段大小, cwnd不再翻倍,而是线性增长。每接收一个新的ACK,cwnd会做以下的更新:
c w n d t + 1 = c w n d t + S M S S ∗ S M S S / c w n d t cwnd_{t+1} = cwnd_t + SMSS * SMSS/cwnd_t cwndt+1=cwndt+SMSS∗SMSS/cwndt
在通常操作中,某个TCP连接总是选择运行慢启动和拥塞避免中的一个,不会出现两者同时进行的情况。
使用慢启动还是拥塞避免 由慢启动阈值(ssthresh)和cwnd的关系决定。
慢启动和拥塞避免之间的区别:当新的ACK到达时,cwnd怎样增长。
慢启动阈值随时间改变,它的主要目的:在没有丢包发生的情况下,记住上一次“最好的”操作窗口估计值。换言之,它记录TCP最优窗口估计值的下界。
当有重传情况(超时、快速)发生,ssthresh会按下式改变:
ssthresh = max(在外数据值/2, 2 * SMSS)
注意,在微软最近(“下一代”)TCP/IP协议中,上述等式变为 ssthresh = max(min(cwnd,awnd)/2, 2 * SMSS)[NB08]。
TCP拥塞控制于4.2版本BSD被提出。
Tahoe(4.2版本的BSD,伯克利软件版本)包含了一个TCP版本,它在连接之初处于慢启动阶段,若检测到丢包,不论由于超时还是快速重传,都会重新进入慢启动状态。缺点是导致带宽利用率低下。
Reno(4.3版本的BSD)中的快速恢复机制就是为解决Tohoe的带宽利用率低下问题而提出的。针对不同的丢包情况,重新考虑是否需要重回慢启动状态。若是由快速重传检测的丢包,cwnd值将进行特殊操作(见下一节)。
总结[RFC5681]中的结合算法:
在TCP连接建立之初首先是慢启动阶段(cwnd = IW), ssthresh通常取一较大值(至少为awnd)。当接收到一个好的ACK (表明新的数据传输成功), cwnd会相应更新:
当收到三次重复ACK (或其他表明需要快速重传的信号)肘,会执行以下行为:
以上第2步和第3步构成了快速恢复。
步骤2设置cwnd大小,首先cwnd通常会被减为之前值的一半,考虑到每接收一个重复ACK,就意味着相应的数据包已成功传输,cwnd值会相应的暂时增大。步骤3维持cwnd的增大过程,使得发送方可以继续发送新的数据包(在不超过awnd的情况下)。步骤4假设TCP已完成恢复阶段,所以对cwnd的临时膨胀进行消除。
以下两种情况总会执行慢启动:新连接的建立以及出现重传超时。当发送方长时间处于空闲状态,或者有理由怀疑cwnd不能精确反映网络当前拥塞状态(参见16.3.5节)时,也可能引发慢启动。在这种情况下,cwnd的初始值将被设为重启窗口(RW),在文献[RFC5681]中,推荐RW值=min(w,cwnd),其他情况下,慢启动中cwnd初始设为IW。
Reno中的快速恢复机制存在问题:当一个传输窗口出现多个数据包丢失时,一旦其中一个包重传成功,发送方就会收到一个好的的ACK,这样快速恢复阶段中cwnd窗口的暂时膨胀就会停止,而事实上丢失的其他数据包可能并未完成重传。导致出现这种状况的ACK称为局部ACK。
NewReno对快速恢复做了改进,它记录了上一个数据传输窗口的最高序列号,仅当接收到序列号不小于恢复点的ACK,才停止快速恢复阶段。
TCP采用SACK机制后,发送方可以知晓多个数据段的丢失情况。
因为需要避免由于触发重传而导致短时间内大量数据被注入网络,SACK TCP强调拥塞管理和选择重传机制的分离(传统TCP则将两者结合)。
一种实现分离方法是,除了维护窗口,TCP还负责记录注入网络的数据量。[RFC3517]称其为**管道(pipe)**变量,以字节为单位,是对外在数据的估计值,记录传输和重传情况。假设awnd值比较大,只要不等式 cwnd - pipe ≥ SMSS成立,在任何时候SACK TCP均可发送数据。
速率减半算法:能够在检测出丢包后使拥塞窗口逐步而不是快速地减小。
[RFC3042]描述了其对TCP做出了微小改进,采用限制传输策略,TCP发送方每接收两个连续的重复ACK,就能发送一个新数据包。这就使得网络中的数据包维持一定数量,足以触发快速重传,TCP因此也可以避免长时间等待RTO而导致吞吐量性能下降。
拥塞窗口校验:在发送操作持续一段时间后,cwnd值可能会增至一个较大值,若此时发送端需要暂停发送且暂定的时间足够长,则之前的cwnd可能无法准确反映路径中的拥塞状况。[RFC2861]提出了一种拥塞窗口校验机制(CWV)。
CWV算法如下:当需要发送新数据时,首先看距离上次发送操作是否超过一个RTO。如果超过,则
对于应用受限阶段(非空闲阶段),执行相似的操作:
若TCP出现突发的延时,可能出现伪重传,造成TCP调整ssthresh和cwnd,进入慢启动状态。这种伪重传现象的发生可能由于链路层的某些变化(如蜂窝转换),也可能是由于突然出现严重拥塞造成RTT大幅增长。
针对上述问题,在14章提出了一些检测方法:DSACK、Eifel、F-RTO,用于检测是否伪重传。结合响应算法(比如Eifel响应算法),就能"还原"TCP对拥塞控制变量的操作。
Eifel响应算法:
在所有情况下,若因RTO而需改变ssthresh值,在修改前需要记录一个特殊变量:pipe_prev = min(在外数据值,ssthresh)
。然后需要运行一个检测算法(即之前提到的检测方法中的某个)来判断RTO是否真实,如果出现伪重传,则当到达一个ACK时,执行以下步骤:
步骤1针对待ECN标志位的ACK的情况,这种情况下撤销ssthresh修改会引入不安全因素,所以算法终止。
步骤2将cwnd设置为一定值,允许不超过IW的新数据进入传输通道,因为即使在未知链路拥塞与否的状况下,发送IW的新数据也被认为是安全的。
步骤3在真正的RTO发生前重置ssthresh,至此撤销操作完成。
Wireshark分析显示了从连接建立起传输的数据包和ACK号。发送端每接收一个ACK就会发送两三个新数据包,这是慢启动的典型行为。
在发送端系统产生的丢包有时称为本地拥塞,产生原因在于,TCP产生数据包的速度大于下层队列的发送速度。
本地拥塞是Linux TCP实行拥塞窗口缩减(Congestion Window Reducing,CWR)策略[SK02]的原因之一。
首先,将ssthresh值设置为cwnd/2,并将cwnd设为min(cwnd,在外数据值+1)
,在CWR阶段,发送方每接收两个ACK就将cwnd减1,直到cwnd达到新的ssthresh值或者由于其他原因结束CWR(如出现丢包)。若TCP发送端接收到TCP头部中的ECN-Echo也会进入CWR状态(参见16.11节)。
在许多情况下,新连接可能会用到相同主机之间的其他连接的信息,包括已关闭的连接或者正处于活动状态的其他连接。这就是相同主机间多个连接共享拥塞状态信息。
[RFC2140]描述了相关内容,其中注意区分了暂时共享(temporal sharing,新连接与已关闭连接间的信息共享)和总体共享(ensemble sharing,新连接与其他活动连接间的信息共享)。
Linux在包含路由信息的子系统中实现了信息保存(目的度量),即当一个TCP连接关闭前,需要保存以下信息:RTT测量值(包 括srtt和rttvar)、重排估计值以及拥塞控制变量cwnd和ssthresh。当相同主机间的新连接建 立时,就可以通过这些信息来初始化相关变量 。
TCP作为最主要的网络传输协议,在传输路径中经常会出现几个TCP链接共享一个或多个路由的情况,然而它们并非均匀的共享带宽资源,而是根据其他连接动态地调节分配。为避免多个TCP连接对传输资源的恶性竞争,提出了一种基于计算公式的速率控制方法,限制特定环境下TCP连接对带宽资源的使用,该方法被称为TCP友好速率控制(TFRC),它基于连接参数和环境变量实现速率限制。
TFRC使用如下公式来决定发送率:
这里的X指吞吐率限制(字节/秒),s为包大小(字节,包含头部),R是RTT(秒),p为丢包率[0,0.1], t R T O t_{RTO} tRTO为重传超时(秒),b指一个ACK能确认的最大包个数。建议 t R T O t_{RTO} tRTO设为4R,b设为1。
在拥塞避免阶段,如何根据接收的无重复ACK来调整窗口大小?使用拥塞避免算法时,每接收一个正确的ACK,cwnd就会增加1/cwnd,而每当出现一次丢包,cwnd就会减半,这被称为和氏增加/积式减少(AIMD)拥塞控制。通过将1/cwnd和1/2替换为a和b,得到AIMD等式:
c w n d t + 1 = c w n d t + a / c w n d t cwnd_{t+1} = cwnd_{t} + a/cwnd_{t} cwndt+1=cwndt+a/cwndt
c w n d t + 1 = c w n d t − b / c w n d t cwnd_{t+1} = cwnd_{t} - b/cwnd_{t} cwndt+1=cwndt−b/cwndt
根据[FHPW00]给出的结果,上述等式得出的发送率为(以包个数/RTT为单位):
对于传统TCP,a = 1,b = 0.5,简化得出 T = 1.2 / p T = 1.2/\sqrt{p} T=1.2/p。
在BDP较大的高速网络中(如1Gb/s或者更大的无线局域网),传统TCP可能不能表现出很好的性能。因为它的窗口增加算法(特别是拥塞避免算法)需要很长一段时间才能使窗口增至传输链路饱和。产生这一问题的原因主要在于拥塞避免算法中的增量为固定值。
带宽时延积(BDP)是一种网络性能指标, 是一个数据链路的能力(每秒比特)与来回通信延迟(单位秒)的乘积。其结果是以比特(或字节)为单位的一个数据总量,等同在任何特定时间该网络线路上的最大数据量——已发送但尚未确认的数据。
HSTCP是为在大BDP网络中实现高吞吐量的一种TCP修改方案。它兼顾了在普通环境下与传统TCP的公平性,但在特殊环境中能够达到更快的发送速度。
为建立一种可扩展的TCP及解决RTT公平性问题,提出了BIC-TCP算法。BIC TCP算法的主要目的在于,即使在拥塞窗口非常大的情况下(需要使用高带宽的连接),也能满足线性RTT公平性。线性RTT公平性是指连接得到的带宽与其RTT成反比,而不是一些更复杂的函数。
该算法使用了两种算法来修改标准TCP发送端,二分搜索增大和加法增大。
二分搜索增大算法操作过程如下:当前最小窗口是最近一次在一个完整RTT中没有出现丢包的窗口大小,最大窗口是最近一次出现丢包时的窗口大小。预期窗口位于这两个值之间,BIC-TCP则使用二分搜索选择中点作为试验窗口,依次递归。如果这个点依然会发生丢包,那么将它设置为最大窗口,然后继续重复二分搜索试验。反之依然。直到最大窗口和最小窗口的差小于一个预先设置好的阈值时才停止,这个阈值称为最小增量。
加法增大算法操作过程则如下:当使用二分搜索增大算法时,可能会出现当前窗口大小与中间点之间差距很大。由于可能出现突然大量数据注入网络的情况,所以在一个RTT内将窗口增大到中间点可能并不是一个好方法。这种情况下,就需要采用加法增大算法。此时增量被限制为每个RTT增加 S m a x S_{max} Smax,这一增量被称为窗口夹。一旦中间点距离试验窗口比距离 S m a x S_{max} Smax值更近时,则转换为使用二分搜索增大算法。
总的来说,当检测到丢包现象,窗口会使用乘法系数β来减小,而窗口增大时,首先使用加法增大算法,之后一旦确认加法增量小于 S m a x S_{max} Smax时就转为使用二分搜索增大算法。这种组合算法称为二进制增长,或者BI。
CUBIC为Linux系统的默认拥塞控制算法(还有Reno供选择)。是BIC-TCP的改进版本:使用一个高阶多项式函数(具体来说是一个三次方程)来控制窗口的增大。三次方程的曲线既有凸的部分也有凹的部分,这就意味着在一些部分(凹的部分)增长比较缓慢,而在另一些部分(凸的部分)增长比较迅速。
CUBIC(立方)算法中,这个特殊的窗口增长函数如下所示:
W ( t ) = C ( t − K ) ³ + W m a x W(t) = C(t - K)³ + W_{max} W(t)=C(t−K)³+Wmax
W(t)代表在时刻t的窗口大小,C是一个常量(默认为4),t是距离最近一次窗口减小所经过的时间(以秒为单位),K是在没有丢包情况下窗口从W增长到 W m a x W_{max} Wmax所用的时间, W m a x W_{max} Wmax是最后一次调整前的窗口大小。其中K可依据以下表达式计算:
当发生快速重传时, W m a x W_{max} Wmax被设置为cwnd,新的cwnd值和ssthresh值被设置 β ∗ c w n d β*cwnd β∗cwnd。CUBIC算法中的 β β β默认为0.8, W ( t + R T T ) W(t+ RTT) W(t+RTT)值是下一个目标窗口的值。当在拥塞避免阶段,每收到一个ACK,cwnd值增加 ( W ( t + R T T ) − c w n d ) / c w n d (W(t+ RTT) - cwnd)/cwnd (W(t+RTT)−cwnd)/cwnd。
下图是CUBIC窗口增长函数的三次方程式:
除了三次方程之外,CUBIC还有"TCP友好性"策略。当窗口太小使得CUBIC不能获得比传统TCP更好的性能时,它将开始工作。根据t可以得到标准TCP的窗口大小 W t c p ( t ) W_{tcp}(t) Wtcp(t):
当在拥塞避免阶段有一个ACK到达时,如果cwnd值小于 W t c p ( t ) W_{tcp}(t) Wtcp(t),那么CUBIC将cwnd值设置为 W t c p ( t ) W_{tcp}(t) Wtcp(t),这种方法确保了CUBIC在一般的中低速网络中的TCP友好性。
当发送端不断地向网络中发送数据包,不断增长的RTT值也可以作为拥塞形成的信号。新到达的数据包没有被发送而是进入了等待队列,这就造成了RTT值不断增大(直到数据包最终被丢弃)。一些根据这种情况提出的拥塞控制技术成为基于延迟的拥塞控制算法,与我们至今为止看到的基于丢包的拥塞控制算法相对。
Vagas算法于1994年被提出[BP95],它是TCP协议发布后的第一个基于延迟的拥塞控制方法,并经过了TCP协议开发组的测试。该算法首先估算了一定时间内网络能够传输的数据量,然后与实际传输能力进行比较。若本该传输的数据并没有被传传输,那么它有可能被链路上的某个路由器挂起,如果这种情况持续不断发生,那么Vages发送端将降低发送速率。
网络中的路由设备,其缓冲区的大小不是越大越好,庞大的缓冲区会导致TCP性能下降,这一问题被称为“缓冲区膨胀”。
缓冲区过小:
很容易就被写满,丢包率变高,导致传输效率差。
缓冲区过大:
如果路由器接收速率大于发送速率,就会有大量数据在路由器排队,延迟很大,此时还不算是丢包。丢包要等到发送端超时才算,然后又往路由器塞入了重复报文段。
不是所有的网络设备中都存在缓冲区膨胀的问题。实际上,主要问题是缓冲区端用户接入设备过满。使用缓冲区大小可动态改变的接入设备或积极队列管理可以解决该问题。
路由器通常不会把拥塞信息发给tcp两端,不利于拥塞控制。路由器默认应用FIFO和尾部丢弃机制,先到的包会发出去,超负荷时丢弃数据包。
应用FIFO和尾部丢弃以外的调度算法和缓存管理策略被认为是积极的,路由器用来管理队列的相应方法称为积极队列管理(AQM)机制。
相关的很多RFC都描述了显式拥塞通知( Explicit Congestion Notification, ECN),它对经过路由器的数据包进行标记(设置IP头中的两个ECN标志位),以此得到拥塞状况。
随机早期检测(RED)网关[FJ93]机制能够探测拥塞情况的发生,并且控制数据包标记。这些网关实现了一种衡量平均占用时间的队列管理方法,如果占用队列的时间超过最小值(minthresh),并且小于最大值(maxthresh),那么这个数据包将被标记一个不断增长的概率值。如果平均队列占用时间超过了maxthresh,数据包将被标记一个可配置的最大概率值(MaxP),MaxP可设置为1.0,RED也可以将数据包丢弃而不是标记它们。
注意:RED算法有多种版本,很多路由器和交换机都支持。
当数据包被接收时,其中的拥塞标记表明这个包经过了一个拥塞的路由器。不过,需要接收端通过向发送端返回一个ACK数据包来通知拥塞状况,以降低发送端发送速率。
ECN机制主要在IP层进行操作,其过程如下:
一种复杂、常见的攻击方式是基于接收端的不当行为。这里将描述三种攻击形式,它们都可以使TCP发送端以一个比正常状态更快的速率进行数据发送。
ACK分割攻击:将原有的确认字节范围拆分成多个ACK信号并返回给发送端,使得发送端的cwnd会比正常情况更快速增长。可通过计算每个ACK能确认的数据量来判断是否为正确的ACK来解决这一问题。
重复ACK欺骗攻击:该攻击可以使发送端在快速恢复阶段增长它的拥塞窗口。这种攻击会比正常情况更快地生成多余的重复ACK。使用时间戳选项可以解决这一问题,然而最好的方法是限制发送端在恢复阶段的在外数据。
乐观响应攻击:对那些还没有到达的报文段产生ACK,导致发送端计算出的RTT比实际的小,从而比正常情况下更快的做出反应。为防范这类攻击,可通过定义一个可累加的随机数使得发送数据段大小可随时间动态改变,以此来更好地匹配数据段和它的对应ACK。当发现得到的ACK和数据段不匹配时,发送端就可以采取相应的行为。
TCP保活机制是为了解决两个问题而设计的:
保活机制是一种在不影响数据流内容的情况下探测对方的方式。它是由一个保活计时器实现的。当计时器被激发,连接一端将发送一个保活探测报文,另一端接收报文的同时会发送一个ACK作为响应。
保活机制并不是TCP规范中的一部分,对此[RFC1122]给出三个理由(保活机制的缺陷):
不过,很多应用需要用到保活机制,因此,现在所有主流TCP版本都实现了保活功能。
该功能在默认情况下是关闭的,是一个可选择激活的功能。TCP连接的任何一端都可以请求打开这一功能,且可以只设置在连接的一端。
组成:
keepalive time:保活时间,即发送保活探测的计时器的timeout时间。一般为无数据传输后2小时。
keepalive interval:保活时间间隔,第一个探测发送后,如果没收到回包,就需要紧跟着再次发送探测。一般为75秒。
keepalive probe:保活探测数,探测多少次后才认为对端不可达,中断连接。
保活探测报文:一个空报文段或带一个字节(垃圾数据)的报文段。序列号等于对端发送的ACK最大序列号减1,因为这个序列号已经被成功接收,所以不会对对端造成影响。探测及其响应报文都不包含任何新的有效数据(它是“垃圾”数据),当它们丢失时也不会进行重传,因此需要保活探测数。
保活时间、保活时间间隔和保活探测数的设置通常是可以变更的。
探测前后,对端可能的四种状态: