赞
踩
12.1.1 ARQ和重传
考虑多跳通信信道,有这些差错种类:
最直接处理分组丢失、比特差错(无法自动纠正的那种)的方法:重发分组直到正确接收。
前提是发送方需要能够判断:
接收方因此需要给发送方一个信号来表明已接收到一个分组,这个方法就是ACK。但有几个问题:
对于分组复制、分组乱序问题,解决方法则很简单:每个分组加序列号。
目前为止,上面这套简单ACK机制,就可以实现可靠通信了,然而效率不高。
主要问题是停等问题:每次发送一个分组后都会停止继续发分组并且等待这个分组的ACK,网络的使用率很低。吞吐量约等于 M(分组大小)/R(RTT),若有丢包,只会更低。
解决停等问题关键是,允许往网络注入更多的分组。但这又会引起大量的问题:
下面的机制就是为了解决这些问题存在的。
12.1.2 分组窗口和窗口滑动
滑动窗口协议:
12.1.3 流量控制和拥塞控制
流量控制:
拥塞控制:
12.2.1 TCP服务模型
12.2.2 TCP中的可靠性
这一节正式谈及TCP的可靠性,12.1讲的只是一个概念,和真实的TCP区别很大。
组包(packetization):
报文段:由TCP传给IP的块称为报文段
可靠性保障关键点:
tcp头部:
头部结构:
关键词:
要记住的:
重要性能问题:重传超时时间
建立连接:
断开连接:
两次半关闭:两边都要发FIN,才能变成完全关闭
半关闭:close、shutdown双方都可以调用,close是全关闭,shutdown能实现半关闭
建立连接为什么要三次握手:
这个问题最好用逆向思维来思考。假设只做一次握手,那么就是C发SYN给S后,就开始发数据了,根本不管S是否收到SYN甚至不管S是否存在,这肯定是不行的,都没确定对方是否存在就发数据,既不能保证数据送到对方,也浪费了带宽;接着假设只做二次握手,那么就是C发SYN给S,S发ACK给C,S马上就开始发送数据,这就有个问题,S发出的针对C的SYN的ACK,可能会丢失,导致客户端没收到SYN的ACK,就开始接收数据,因为缺少ISN(s),所以C不能知道究竟有没丢数据(并不能根据第一个收到的数据报文段来判断)。
断开连接为什么要四次握手:
因为tcp是全双工通信,断开通信等于要关闭2个方向的数据流,即要做2次半关闭(2个FIN),一次半关闭就是2次握手。半关闭可能是自然发生的,例如客户端发了FIN,服务端收到FIN时,可能还有一些数据在tcp发送缓冲区,于是只能发送ACK而不能发FIN,就进入了半关闭状态;半关闭也可能是用户用shutdown强制要求的,例如服务端收到FIN后,还是可以继续往发送缓冲区继续填入数据,无法知道服务端什么时候会停止发送数据。综上,半关闭的情况不可避免。不过有可能第二个FIN和第一个FIN的ACK是一起发送的(前提是本地的缓存队列的数据都发送出去了不然不能发FIN),就变成了三次握手(本质上还是四次握手)。
13.2.3 初姑序列号ISN
13.2.5 连接建立超时:
13.2.6 连接与转换器
NAT通过探查TCP的头部来跟踪连接的建立情况,主要就是通过flags的SYN ACK FIN位。NAT还可以修改报文段,但是有更复杂的问题。
题外话:tcp数据长度,是没有在tcp头显式保存的,而是通过ip层的分组长度来算出,tcp数据长度 = ip分组长度 - tcp头长度
MSS:
(上面三个是最基本的)
SACK和SACK-Permitted:
WSOPT窗口缩放因子:
TSOPT时间戳选项:
TIME_WAIT:
TIME_WAIT作用:
TIME_WAIT状态其实不做什么事情,就是限制1分钟内这条连接的其中一端不能发起对另一端的新连接,这也是为什么只有一端进入TIME_WAIT而另一端可以直接CLOSED的原因,对端相信TIME_WAIT一端不会立即重新连接,
FIN_WAIT_2无限等待问题:
同时打开同时关闭:
当发现一个到达的报文段对于相关连接而言是不正确的时, TCP就会发送一个重置报文段。
重置报文段通常会导致TCP连接的快速拆卸。
发送时机:
最后一种情况TIME_WAIT错误是一种需要避免的错误,TIME_WAIT状态需要忽略RST,否则就会过早地进入CLOSED。
强制终止方法:SO_LINGER设0(socket逗留选项),含义是,不会为了确定数据是否到达另一端而逗留任何时间。
- struct linger l;
- l.l_onoff = 1;
- l.l_linger = 0;
- sock_setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &l, sizeof(l));
接收到的RST,必然要带ACK,且序列号要在对端窗口范围内,否则可能会遭到伪造RST攻击。
发送RST,不需要一个RSTACK。对端收到RST就自然重置连接了(不过可能对端会丢失RST)。
UDP没有RST,但是有ICMP目的地不可达消息。
主要就是backlog。
SYN flood
其实都写在RFC里了。
超小MTU攻击
攻击者伪造一个ICMP PTB消息,消息包含一个非常小的MTU值(如68字节),就迫使受害者的TCP尝试采用非常小的数据包来填充数据,大大降低了性能。禁用最大MTU发现功能就能实现这个攻击。
序列号/劫持攻击
就是对已建立的连接插入攻击数据的攻击。解决办法应该是用校验码。
欺骗攻击
例如伪造RST报文段:前提是序列号要在该连接的序列号范围内
抵御方式:认证每一个报文段(TCP-AO);要求RST报文段拥有一个特殊的序列号而不只是在范围内的序列号、要求时间戳选项具有特定的数值。
tcp_retries1:连接已经建立后,基本重传次数。次数达到后,会先尝试让网络层更新路由再继续发包。一般为3次.
tcp_retries2:连接已经建立后,最大重传次数。次数达到后,会断开连接。
重传超时<RTT:对网络引入不必要的重复数据
重传超时>RTT:网络利用率(吞吐量)下降
RTT是每个tcp连接独立计算的。
超时重传(RTO):在发送数据时设置一个定时器,若计时器超时仍未收到ACK,则会引发相应的超时/重传操作。
快速重传:若TCP累积确认无法返回新的ACK,或者当ACK包含的选择确认信息(SACK)表明出现失序报文段时,快速重传会推断出现丢包。
经典方法:
SRTT = a(SRTT) + (1-a)RTT,a一般为0.8~0.9,称为指数加权移动平均/低通过滤器(EWMA)
RTO = min(ubound, max(lbound, (SRTT)b)),b为时延离散因子,推荐值1.3 ~ 2.0
ubound:1分钟,lbound:1秒。
适用于稳定RTT的网络。
标准方法:
srtt = (1-g)srtt + (g)M rttvar = (1-h)rttvar + (h)(|M - srtt|) RTO = srtt + 4(rttvar)
M就是经典方法里的RTT;srtt和SRTT等价;|M - srtt|为平均偏差,所以rttvar为平均偏差的EWMA。
g:新样本M占srtt估计值的权重,1/8
h:新平均偏差样本|M-srtt|占rttvar的权重,1/4
最终,RTO为srtt加上4倍rttvar,4是一个研究出来的常量值。
当M变化时,|M-srtt|越大,rttvar也越大,RTO增长越快
时钟粒度:
Linux计时器时钟粒度为1ms,设为G。
RTO = srtt + max(G,4(rttvar))
Linux RTO默认RTO最小值200ms
收到第一个RTT测量值时再次初始化:
srtt = M
rttvar = M/2
RTO = srtt + 4(rttvar) = 3M
重传二义性:
发生重传时,若收到ACK,并不能知道是对第一次还是第二次发包的确认,所以无法计算RTT,需要跳过。(Karn算法第一部分)
若用了时间戳选项则可以处理二义性,因为ACK包附带了发包的时间戳,就可以知道ACK是对第一次还是第二次发包的确认。
退避系数(backoff factor):每当重传计时器出现超时,退避系数加倍,直到接收到非重传数据时重设为1。
需要记录被计时的报文段序列号,若及时收到该报文段的ACK,那么计时器被取消。
没丢包的话计时器不会超时。
若在设定的RTO内,没收到ACK,就会触发超时重传。
此时需要降低发送速率:
基于定时器的重传不是好东西,会导致网络利用率下降。
基于对端的反馈信息来引发重传。(和基于计时器的重传的本质区别)
更加及时。
tcp一般都实现了基于计时器的重传和快速重传。
重点:接收端收到失序报文段时,需要立即生成重复ACK并立即发送,失序情况表明出现了丢段(接收缓存出现空洞)。
重复ACK:这个ACK可以表明是哪一个分组没有收到。但因为是用了tcp的seq段,所以一个RTT内只能填补一个空缺。
立即发送重复ACK是为了让发送端尽早得知有失序报文段,并告知空缺在哪。
当接收端收到当前期望序列号的后续分组时,当前期望的包可能丢失了,也可能是延迟到达,2者无法区分。
因此需要等待一定数目的重复ACK(重复ACK阈值 dupthresh,一般为3,也有动态的),才能判断数据是否丢失并快速重传。
总之,发送端收到至少3个重复ACK后,马上重传可能丢失的分组,而不必等计时器超时。
http://packetlife.net/blog/2010/jun/17/tcp-selective-acknowledgments-sack/
SACK是指已接收的失序报文段,并不是指丢失的报文段。
SACK会重复发,例如收到连续的2个报文段3和4,返回2个ACK,ACK都为1(duplicate ACK),但第一个包带了SACK=3,第二个包带了SACK=3、4。发送端收到这2个ACK时,就会知道报文段2丢失,那么重传2。
一个tcp头的SACK最多3块。
收到SACK并重传了包,也不能清空重传缓存的该包(食言问题)。只有接收端发来的普通tcp ACK号大于发送端最大序列号值才可清除。
RTT较大,丢包严重时,SACK的优势就能体现出来。因为一个RTT可以填补多个空缺很重要。
GBN:连续发了n个报文段,如果网络突然缓慢,最前面的那个超时了,会触发重传,此时后面的n-1个包也没被确认,那么n个报文段都重传(回退)。
伪重传:没有丢包也发生了重传,叫伪重传。原因:网络延迟、包失序、包重复、ACK丢失。
RTT增加,超过当前RTO时,就有可能出现伪超时(重传)。
重要任务:检测出伪重传,几种方法:D-SACK、F-RTO、Eifel检测
D-SACK:重复SACK,一个操作系统的选项。
开启后,可在第一个SACK块中告知接收端收到的重复报文段序列号。
允许tcp一端开启D-SACK而另一端没有D-SACK,不需要对称,没有开启D-SACK的一端不能使用D-SACK。
原理是,SACK接收端允许了包含seq<=累计ACK的SACK块。
和通常的SACK的区别:DSACK只包含在单个ACK中,并且不会在多个SACK中重复。鲁棒性比SACK低。
Eifel检测算法:发生超时重传时,Eifel算法等待接收下一个ACK,若为针对第一次传输(即原始传输)的ACK(用时间戳判断),则可以知道该重传是伪重传(利用TSOPT来检测伪重传)。
Forward RTO-recovery(路由转发延迟导致的RTO的恢复机制,简称F-RTO):
用上面的任意检测算法检测到伪重传后,要应用Eifel响应算法:
说白了就是更新srtt和rttvar。因为伪超时导致临时修改了srtt和rttvar(RTO变了),检测发现伪超时,那就得恢复到正常的srtt、rttvar。
包的问题有三种:丢失、失序、重复。tcp需要区分。
失序:
区分丢失和失序不是重要问题;互联网中严重的失序并不常见,dupthresh设3就足够了。
重复:
链路层重传时会生成重复副本。会导致接收端生成一系列重复的ACK,触发伪快速重传。
但DSACK能处理这种情况,因为重复ACK没有包含失序信息,意味着ACK是重复数据。
即操作系统在tcp断开连接后依然缓存了该条路径的rtt之类的信息。方便下次和该地址建立连接时,初始化srtt rttvar。
当发生重传,并不需要完全重传相同的报文段,而可以重新组包,发送一个更大的报文段来提高性能。
低速率DoS攻击:
每次受害tcp重传时,就发一堆数据给它并导致重传超时,进而导致对方减小发送速率、退避发送,最终导致无法正常使用网络带宽。
预防方法是,随机随选RTO,使得攻击者无法预知确切的重传时间。
减慢受害tcp的发送并使RTT估计值过大(过分被动):
导致丢包后不会立即重传。
伪造ACK使受害tcp的RTT估计值过小(过分积极):
导致过分发送,造成大量的无效传输。
算法要求:
特点:
在高延迟(拥塞)网络中,需要减少报文数。算法使得单位时间内发送的报文段数目更少。
简单来说就是会死锁:
因为客户端最终会发出ACK,所以死锁可解。但等待过程中,连接处于空闲状态(明明有事忙),性能变差。
禁用Nagle:
发送窗口:
窗口运动术语:
Note:左边界不可能左移。
接收窗口:类似发送窗口。但窗口中间因为SACK选项,可能会有被ACK的序列号(未确认的就是空洞)。但必须RCV.NXT接收了,窗口才能右移。
零窗口导致的问题:
左右边界相等时,叫零窗口。此时发送端不能发数据。
接收端重新获得可用空间时,会给发送端发送一个窗口更新(window update),当然还是用通告窗口。但这个发包不带数据(因为这是接收端,若发了数据就变成全双工的发送端了),所以是一个典型的ACK消息。这个ACK是可能丢失的。
如果丢失了,就会进入死锁状态。发送端不知道接收端已经可以接收数据。
所以发送端会定时探测probe对方窗口,用来伺机增大发送窗口,接收端收到时必须返回ACK(包含了窗口大小字段)。
探测时机为一个RTO超时后,然后指数间隔发送。
probe是带有一个字节的数据的(用户的数据),所以tcp会可靠传输它。不过如果接收端依然没有可用缓存空间,就会丢掉这个包。
糊涂窗口综合征(silly window syndrome,SWS):
百度百科解释:
糊涂窗口综合症是指当发送端应用进程产生数据很慢、或接收端应用进程处理接收缓冲区数据很慢,或二者兼而有之;就会使应用进程间传送的报文段很小,特别是有效载荷很小; 极端情况下,有效载荷可能只有1个字节;传输开销有40字节(20字节的IP头+20字节的TCP头) 这种现象。
解决方案:
发送端需满足以下条件才能发送:
第三点反过来说就是:如果启用了Nagle或者有未确认的在传数据,那么不应该发送小包
大容量缓存与自动调优
这2个问题很关键,并且因为第二个问题,很多tcp协议栈中应用层是不能指定缓存大小的。操作系统会自己定一个定值或弄成动态值。
窗口大小的自动调优:只能按类型选,没有具体值,disabled、 highlyrestricted、 reStricted、 normal、experimental。
缓存大小的动态调整:通过估算发送方的拥塞窗口的大小,来动态设置TCP接收缓存的大小。
虽然tcp的紧急机制调用用了一个叫MSG_OOB的flag,但是其实并不是带外数据,这个紧急数据一样是在用户的数据流中传输,只是优先级更高。
发送的紧急数据只能是一个字节,并放进当前缓冲区末尾。因为用了tcp的可靠传输,所以只需要插入一次。
因为窗口大小原因,可能不能立即发出这个字节,但是tcp会知道已经进入紧急状态,所有发出去的包都打开了URG标志。
对接收端来说,收到URG的包头,不等于该报文段里就有紧急数据(还在收到)。要用紧急指针来判断。在收到紧急数据前可能有多个包头,包头里的紧急指针都一样。
为了让用户及时recv拿到紧急数据,需要用信号SIGURG的方式通知。SIGURG在第一次收到URG包头时触发一次。
带外数据缓冲区:到达的紧急数据不能混在用户数据缓冲区,所以另外用这个来存,等用户来读取。
send(sockfd, 'x', 1, MSG_OOB);
recv(sockfd, &ch, 1, MSG_OOB);
recv:在紧急状态下,带外数据仍未到达,函数返回EWOULDFBLOCK;非紧急状态下,调用上述函数,返回EINVAL。
回顾:流量控制机制是基于通告窗口大小字段来实现,明确地告诉了发送端,接收端的缓存大小,避免了接收端缓存溢出。发送端降低了发送速率。
对路由器而言:超负荷时,降低发送速率或丢弃部分数据。原因是,即使路由器能缓存一部分数据,然后慢慢发出去,但源源不断的数据到达,到达速率高于发出速率,任何容量都得溢出。(排队理论!)
拥塞:路由器无法处理高速率到达的流量而被迫丢弃数据信息的现象
拥塞控制机制:是为了缓解拥塞情况,tcp连接两端都要进行
回顾:对于丢包,tcp采取首要机制是重传:超时重传和快速重传。
但当网络拥塞时,重传会导致火上浇油。所以要避免这个情况。
当拥塞情况出现时的处理措施:
然而很难做到:因为对于tcp发送方而言,没有一个准确的方法去知晓路由器的状态。
只能用一些信息来推断:
检测出拥塞后,就是对拥塞的处理。其实就是when减速和how减速、how恢复速率。
拥塞窗口:反映网络传输能力的变量,cwnd
接收端通知窗口:awnd
发送端实际可用窗口:W = min(cwnd, awnd)
在外数据大小(flight size):发送端发送的数据中,未收到ACK的数据不能多于W(字节)。
cwnd无法拿到准确值:缺乏显示拥塞的信号
W、cwnd、awnd需要根据经验设定并动态调节。
所以,W的值不能过大或过小,应接近BDP(bandwidth-delay prodcut),带宽延迟积,也称作最佳窗口大小(optimal window size)。
W反映网络中可存储的待发送数据量大小。
实际计算方法:W = RTT * 链路中最小通行速率
W越接近BDP,网络资源利用得越高效。
确定BDP是难点。
同时只运行一个,可以互相切换。
基于包守恒。
慢启动
原因:由于未知网络传输能力,需要缓慢探测可用传输资源,防止短时间内大量数据注入导致拥塞。
作用:
时机:
原理:
SMSS: 发送方的最大段大小 = min(接收方MSS,路径MTU)
IW:初始窗口,一开始发送的数据段大小(SYN交换后)
初始cwnd = IW = 1 * SMSS(简单起见,设1)
每次收到ACK,cwnd会慢慢增加:cwnd += min(N, SMSS)
N:未ACK的数据,通过这一”好的ACK“能确认的数据大小
好的ACK:新收到ACK大于之前收到的ACK
这样设计是因为:如果每次收到ACK都直接+=SMSS,可能会遭到“ACK分裂”攻击,通过发送小ACK导致发送方加速发送。
如果N大于SMSS,说明正在发送大量数据,那么就只+=SMSS,cwnd = 2 * SMSS.
此时就可以发送2个数据段,如果继续接收ACK成功(2个ACK),则2变4、4变8,指数增加。
k:k轮后,W = 2k,k = log2(W),需要k个RTT时间,窗口才能达到W。
指数增长看似快,但还是比一开始就以最大速率(接收方最大窗口)慢(W不会超过awnd)。
另外,如果接收端开启了ACK时延,接收端就不会发2个ACK,而是合并1个,那么增速变得更慢。
非交互式tcp流:其实就是指单方发送大数据的情况,不是短消息的一问一答(http)。
对于非交互式tcp流来说,delayed ACK是不好的,此时接收端是不会发数据的,所以没可能在数据包里带上ACK。此时可以使用Quick ACK机制。
通过QuickACK,接收端recv后可以立即发送ACK,没有delay。
但tcp本身很难知道是不是非交互式的流。可以这样做:
http://www.jauu.net/2010/10/02/tcp-quick-ack-versus-packet-overhead/
拥塞避免
当指数增长到一定程度,就会开始丢包。
此时设置一个慢启动阈值(ssthresh),公式下面说;当前拥塞窗口大小(cwnd)减到一半(不一定,只是经典做法)。
只留一半是避免占满全部带宽,导致路由器其他连接丢包。
拥塞避免:
当确立了慢启动阈值,tcp就进入拥塞避免阶段。cwnd不再翻倍,而是线性增大。这就可以得到更多的传输资源而不至于影响其他连接。
cwndt+1=cwndt+SMSS∗SMSS/cwndtcwndt+1=cwndt+SMSS∗SMSS/cwndt
其实就是每次收到ACK就增加(1/k)倍 (次线性)
慢启动和拥塞避免的选择
慢启动和拥塞避免之间的区别:当新的ACK到达时,cwnd怎样增长。
ssthresh在整个连接中不是保持不变的。没有丢包时,记住上一次最好的窗口估计值。有丢包时,按下面公式更新:
ssthresh=max (cwnd/2, 2*SMSS)
Tahoe、 Reno以及快速恢复算法
Tahoe:有丢包时,cwnd直接变1,重新慢启动。会导致带宽利用率低下。
解决办法:
Reno快速恢复:
因为丢包进入的慢启动阶段,可以快速恢复,方法是每接收到一个ACK(重复ACK),cwnd就增长1 SMSS(急速),直到接收到一个”好的ACK“。
可以小结出一套标准算法:
在TCP连接建立之初首先是慢启动阶段(cwnd = IW), ssthresh通常取一较大值(至少为awnd)。当接收到一个好的ACK (表明新的数据传输成功), cwnd会相应更新。
当收到三次重复ACK (或其他表明需要快速重传的信号)肘,会执行以下行为:
以上第2步和第3步构成了快速恢复。
NewReno:
因为Reno需要收到重复ACK才能快速恢复,但如果先收到了好的ACK(局部ACK)(但还有别的包已发送未确认),导致了窗口膨胀过早结束,此时传输通道就会很空闲。且如果重复ACK不足三个(网络中没有足够的数据表在传输就会这样),不会进入快速重传,而确实又有丢包,那么最终就是RTO超时,进行超时重传,并慢启动。
Reno记录了一个最高序列号来解决。效果就是避免过早结束膨胀。
(其他改进之后再学习)
其实就是操作系统的本地优化。新连接可以利用旧连接的信息,来优化。
net.ipv4.tcp_no_metrics_save = 0,默认开启
每个tcp关闭前,保存信息:srtt、rttvar、重排估计值、cwnd、ssthresh
就是多条tcp连接对资源的竞争问题。有一条很复杂的公式。篇幅很短,应该不是重点。
网络中的路由设备,其缓冲区的大小不是越大越好,过大的缓冲区反而会导致网络拥塞。
缓冲区过小:
很容易就被写满,丢包率变高,导致传输效率差
缓冲区过大:
如果路由器接收速率大于发送速率,就会有大量数据在路由器排队,延迟很大,此时还不算是丢包。丢包要等到发送端超时才算,然后又往路由器塞入了重复报文段。
拥塞信号反馈过慢。
https://blog.csdn.net/zerooffdate/article/details/77688460
积极队列管理和ECN:
一般,路由器没有义务把拥塞信息发给tcp发送端。就不利于拥塞控制。
路由器默认只有FIFO和尾部丢弃机制,先到的包会发出去,后到的包如果塞不下了,就丢弃。
应用FIFO和尾部丢弃以外的调度算法和缓存管理策略被认为是积极的。
AQM:积极队列管理
ECN:非常依赖路由器、交换机的拥塞通知机制。
路由器会给发送中的报文打上ECN标识,报文送到接收端后,接收端再通过ACK包告诉发送端ECN。发送端就可以降低发送速率。
这一章很短,不是很重点。
keepalive是一种不怎么纳入标准的技术。因为keepalive是有问题的:对于一个长连接,如果发送保活探测的时候,刚好这段时间中间网络出了点问题(例如路由器重启),就会导致好的连接被中断掉。
保活功能是为服务器提供的。服务器需要知道客户端是否崩溃或离开,才能释放连接资源。
一般,长时间交互式服务就不期望保活功能(ssh);短时间非交互式服务就期望保活功能(Web服务器)。
默认关闭。
非对称,两端都可以各自做keepalive。
组成:
探测报文:
一个空报文段或带一个字节(垃圾数据)的报文段。序列号等于对端发送的ACK最大序列号减1,因为这个序列号已经被成功接收,所以不会对对端造成影响。
探测报文返回的响应可以确定连接是否在工作。
响应报文也是不带数据或者只带垃圾数据。
2种报文丢失了都不会重传。(所以需要重复探测)
探测前后,对端可能的四种状态:
Note:对端正常关闭然后重启,是没问题的。对端会发送FIN,正常断开连接。
欺骗攻击:因为保活报文不包含用户数据,加密强度有限。容易伪造。导致连接一直保持,浪费服务端资源。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。