全部博文(356)
分类: LINUX
2018-12-06 15:19:28
原文地址:畅谈linux下TCP(下) 作者:raochaoxun
上一篇 http://blog.chinaunix.net/uid-27105712-id-5793734.html介绍了TCP在连接和关闭时候的一些知识点。这篇介绍TCP在连接建立以后,传输中的重要特性。
先认识一下TCP包头, 常规TCP包头为20个字节。
可以通过TCP OPTION 扩展包头内容。TCP OPTION 是一个比较灵活的TLV结构,length表示TLV长度,不是仅仅V的长度。如上一篇提到的 TimeStamp
TCP Option - Timestamps: TSval 791379335, TSecr 4104752551
Kind: Time Stamp Option (8)
Length: 10
Timestamp value: 791379335
Timestamp echo reply: 4104752551
包头各个字段意思很明显了,这里就不老调重弹了。只说一下你能从包头看出一些有意思的信息吗:
window size 大小为16个bit。 限制料 传输窗口只有64k大小。这个在高速网络下是个性能缺陷。如何解决呢。还是通过 TCP Option - Window scale 来解决。Window scale 表示要把 window size 放大 多少倍,计算公式为: window_size = window_size * ( 2 ^ Window_scale ) . 看下面这个实例:28960*(2^7) = 144800 字节
都说TCP是 有序,可靠的协议。关键是靠seq 和 ack 这两个字段:
至于如何利用这两个字段来保证TCP 有序,可靠的特性。需要用 TCP滑窗来解释
前面说到过,TCP包头里面有个window size 字段。用来高速对方,自己的接收能力。 滑窗机制就是利用这两个字段。来达到这个目录。
3.1、分包、丢包、乱序TCP是基于IP协议的。 用户发送一个数据,对于TCP来说,会存在下列几种情况:
包丢了怎么办,重传呗,这里涉及到1、什么时机重传,2、重传多少问题:
3.2.1、什么时候重传关于RTT计算问题:
但是假如这个包重传了一次, 收到ACK。 这个ACK到底是前一个包的延迟到达呢,还是后一个重发包的应答呢,无法分辨这个ACK是谁的? 如下图,有多种计算方法, 如果随意取其中一个,会带来比你想象要大很多的偏差结果。
如果系统支持Timestamps的话, 可以通过上一章中提到的 TCP Option Timestamps。 因为每个ACK都带有发送者的timestamp。这样就很明确计算RTT了。但是不是每个系统都支持这种 Timestamps
TCP Option - Timestamps: TSval 791379335, TSecr 4104752551
Kind: Time Stamp Option (8)
Length: 10
Timestamp value: 791379335
Timestamp echo reply: 4104752551
注解:TCP Option Timestamps 是 表示系统启动以来时间。单位为毫秒。 (2^32/1000/3600/24 = 49 天一个轮回周期)
如果不支持的话,就取最早的那次传输时间计算RTT(不按重传计算).
最后linux是通过一个公式计算RTO,使用这个作为重传超时时间。这个公式能消除RTT的取值误差影响,至于为什么能做到,我只知道实际TCP验证如此,经过检验出来的。
重传不是只有一次,会有多次。通过 tcp_retries1, tcp_retries2 这两个参数来控制。默认情况下 tcp_retries1 =3,tcp_retries2 =15。
net.ipv4.tcp_retries1 net.ipv4.tcp_retries2
超过tcp_retries1 后 会更新路由,选择一条新的路由,避免路由问题导致丢包或者延迟。另外也不完全是由这两个参数控制。还有一个总的超时时间值,根据初始RTO计算出来。如果这个值比较小,可能不到重试 tcp_retries2 次数就结束了。总体来说取两者最小。如果最终还是收不到应答。就会直接放弃重传,关闭TCP连接。
3.2.3、重传多少的问题如图五所示, 发送方在发送完#9号包后,这时候已经收到连续3个ack=3的ACK报文,在快速重传算法下。开始重传。此时只是重传#3报文,还是重传{ #3,#4,#5,#6,#7,#8,#9} 。 只重传#3报文,效率有点低,后面又要同样等待重传#6号报文。重传后面所有报文。会导致已经收到的报文又重发,造成网络交通更加拥堵。 最好的发式是 只重传 丢失的报文 {#3,#4,#6,#8}. 但是发送方无法知道这么多信息。于是乎。SACK方案被提出来,解决这个问题.
TCP Option - SACK permitted Kind: SACK Permitted (4) Length: 2
这里可以看到,通过SACK选项,解决了按需重传的要求。发送方通过SACK,知道该重传那些报文,避免重复重传。
在三次握手的时候,会在双方的syn包中携带本地是否启动sack扩展。 Sack-Permitted选项就是说明本地启用sack
由于TCP包头长度有限(60),所以SACK的片段个数也是有限的,最多4个. 如果有TCP Option其他选项,会小于4个。大家可以自己计算.
TCP SACK Option: Kind: 5 Length: Variable
+--------+--------+ | Kind=5 | Length |
+--------+--------+--------+--------+
| Left Edge of 1st Block |
+--------+--------+--------+--------+
| Right Edge of 1st Block |
+--------+--------+--------+--------+
| |
/ . . . /
| |
+--------+--------+--------+--------+
| Left Edge of nth Block |
+--------+--------+--------+--------+
| Right Edge of nth Block |
+--------+--------+--------+--------+
如果SACK的第一个段的范围被ACK所覆盖,那么就是DSACK 如果SACK的第一个段的范围被SACK的第二个段覆盖,那么就是DSACK
发送端和接收端都有缓冲区,但是缓冲区大小是由限的。假如接收端比较繁忙,没有来得及取走缓冲区的数据,发送端如果还一直发送,就会有问题了。需要有种机制,接收方通知发送方自己接收能力,让发送方调整发送速度。
TCP头里有一个字段叫Window Size,又叫Advertised-Window,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
发送方有个发送缓冲区,发送缓冲区的布局如下:
图七中各段含义
- Category#1已收到ack确认的数据。
- Category#2发还没收到ack的。
- Category#3在窗口中还没有发出的(接收方还有空间)。
- Category#4窗口以外的数据(接收方没空间)
Category#2 + Category#3就是发送者的TCP滑动窗口
图八生动的演示了滑窗的移动和大小伸缩的变化。
4.3、流量控制如果发送者发送数据过快,接收者来不及接收,那么就会有分组丢失。为了避免分组丢失,控制发送者的发送速度,使得接收者来得及接收,这就是流量控制。流量控制根本目的是防止分组丢失,它是构成TCP可靠性的一方面。
如何实现流量控制?由滑动窗口的存在。既保证了分组无差错、有序接收,也实现了流量控制。主要的方式就是接收方返回的 ACK 中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送。
拥塞控制是防止传输数据的联络层网络出拥塞时数据大量丢失的情况。和流量控制 不同的是;流量控制主要是参考接收方的能力指标(rwnd),进行发送速度调整。 拥塞控制主要是参考网络延迟来调整发送速度。因为光依赖接收方和发送方的信息参考,并不完整。需要考虑传输网络状况。于是诞生了几个参考网络状态调整发送速度的算法:
拥塞控制主要算法有: 慢启动、拥塞避免、快重传、快恢复。其中快速重传 前面已经介绍过。
5.1、慢启动 5.1.1、MTU(Maximum Transmission Unit)L2 层的限制。 MTU(Maximum Transmission Unit)最大传输单元, 这个是由以太网链路层决定的长度,提供给其上层最大一次传输数据的大小。如果上层是 IP 协议的话, 缺省MTU=1500。意思是 一个 链路层 IP报文的Payload (包含ip 头,tcp头) 不能超过1500个字节。对于tcp应用层来说 max = 1500 - 20 -20 = 1460 , max = 1500-20-60= 1420 。
TCP应用层一般会发送几K或几M字节。链路层会按照MTU大小切分一段段发送出去。
PS: 链路层协议有很多,不同的链路层协议,其MTU大小也不一样。 1500只是以太网络下的最大MTU大小。
5.1.2、MSS(Maximum Segment Size )而且,这里计算TCP包在 [1420-1460] 之间,其实是没有考虑到第三层IP协议包头的扩展。其实IP包头也不是20个字节固定,也是可以扩展。
L4层的限制。 MSS(Maximum Segment Size ) 最大TCP分段大小,不包含TCP头和 option,只包含TCP Payload ,TCP用来限制自己每次发送的最大分段尺寸. 应用层发送数据,都要按照这个大小切片,发出去。
MSS 缺省是1460字节,不过这个在TCP握手阶段协商的。也是通过TCP Option来协商. 发送方和接收方不一定要一样。
发送方: TCP Option - Maximum segment size: 1460 bytes Kind: Maximum Segment Size (2) Length: 4 MSS Value: 1460 接收方: TCP Option - Maximum segment size: 1300 bytes Kind: Maximum Segment Size (2) Length: 4 MSS Value: 1300
可用通过 setsockopt 借口设置 TCP_MAXSEG 选项来设置MSS
5.1.3、拥塞窗口cwnd(congestion window)PS: 从MTP和MSS定义可以看出他们关系 MSS 永远要小于MTU。 目的是尽量避免在IP层再次分片。
拥塞窗口是发送方维持的发送窗口大小,, 注意前面有个rwnd,表示接收方的能力。 真正的发送者发送窗口=min(rwnd, cwnd)。cwnd 的 计量单位是 MSS。 即 cwnd=1 表示 一个 MSS大小。 cwnd=2 表示2个 MSS大小。
cwnd大小也是根据网络状态,动态变化的。
慢启动算法的思路就是: 不要一开始就发送大量的数据,这样很容易导致网络中路由器缓存空间耗尽,从而发生拥塞,而是先由小到大逐渐增加发送窗口的大小,探测一下网络的拥塞程度。
那么cwnd如何增长呢? 增长到什么程度停下来呢? 这里有个关键指标 慢启动门限ssthresh。
慢启动的算法如下: (cwnd全称Congestion Window):
1、连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。
2、每当收到一个ACK,cwnd++; 呈线性上升
3、每当过了一个RTT,cwnd = cwnd*2; 呈指数让升
4、还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”(后面会说这个算法)
可以看出,慢启动其实并不慢,基本上是以指数级增长的。
5.2、拥塞避免 (Congestion Avoidance)PS:如果出现超时,则执行下面拥塞避免算法。 cwnd=1重新开始。
拥塞避免算法可以简单归纳成一句话:“加法增加, 乘法减少”。
前面说过,有一个ssthresh(slow start threshold),是cwnd的上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”。一般来说ssthresh的值是64k(65535,单位是字节),当cwnd达到这个值时后,算法如下:
1、收到一个ACK时,cwnd = cwnd + 1/cwnd
2、当每过一个RTT时,cwnd = cwnd + 1
这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。基本上是以线性增长的。
当出现重传的时候,算法又会分成l有两种情况:
1)等到RTO超时,重传数据包。TCP认为这种情况太糟糕,反应也很强烈。
1、sshthresh = cwnd /2 (乘法减少)
2、cwnd 重置为 1
3、进入慢启动过程
2)遇到快速重传情况,也就是在收到3个duplicate ACK时就开启重传,使用 快速恢复算法——Fast Recovery
可以看出,整个拥塞控制过错可以用 乘法减小(Multiplicative Decrease)和加法增大(Additive Increase)来概括, 简称MDAI。
快速恢复算法依赖前面讲的快速重传算法。即当出现连续三个重复ACK时候。不启用拥塞避免算法,而是
1、sshthresh = sshthresh /2 (乘法减少)
2、cwnd = sshthresh
3、进入加法增加, 乘法减少的 拥塞避免算法
比较前面拥塞避免算法, 可以看出最大的区别是没有一下子把cwnd降到1,而是降到一般开始重新尝试。因此 快速恢复算法相对来说比较乐观激进一些。它的主要思路是“认为如果连续收到3个重复的ACK,网络拥塞状况没有想象那么糟糕,可以尝试适当减缓一下发送速度”。
在实际发送数据中,可能会存在一种情况。发送方几个字节几个字节的发送数据给对端。这样就会造成资源极大的浪费。网络中传输的数据,报文大部分是协议的包头(IP+TCP 包头40个字节)。另外还会引起大量ACK恢复。对网络拥塞危害很大。为了避免这种情况。出现了 Nagle算法.
Nagle算法 是 避免发送大量的小包,防止小包泛滥于网络。把多次发送的小包合并成一次发送。
(1)如果包长度达到MSS,则允许发送;
(2)如果该包含有FIN,则允许发送;
(3)设置了TCP_NODELAY选项,则允许发送;
(4)未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
(5)上述条件都未满足,但发生了超时(一般为200ms),则立即发送。
问题:
1、从上面第4个条件可以看到,如果能快速收到 接收方回复ACK,其实Nagle算法基本上失效了;
2、因为Nagle算法 会合并包。所以就会导致TCP常见 "粘包",“并包” 现象。在某些场景可能会出问题。后面会在TCP_NODELAY 中提到;
Nagle算法 在LINUX系统中,缺省时开启的。如果要禁止的话,使用TCP_NODELAY 选项关闭
看个例子,下面例子中, 使用nagle算法 可能会节省时间。
因为在linux系统中,agle算法 缺省是开启的,说明实际网络中,前一种情况很常见。大部分能节省时间。
Nagle算法 是针对发送方的,进行小报文合并。 Delayed ACK是 另外一个角度。针对接收方,对连续ACK进行合并。
(1)如果有数据回复对方,会捎带上ACK;
(2)如果有有2个连续ACK未回复,则合并,则发送ACK;
(3)Delayed ACK 有超时定时器(缺省是200ms),超时则发送ACK;
Delayed ACK 缺省时打开的,如果想要关闭,使用TCP_QUICKACK
int off = 0;
setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &off, sizeof(off));
有时候,开启 Delayed ACK,反而会增加传输时间,如图15所示
如果配合Nagle 算法,则有可能导致更长的时间,如图16所示
首先,TCP_CORK则是Linux系统所独有的。其它*nix系统是没有这个选项。
cork就是塞子的意思,TCP_CORK 是禁止小报文发送,合并成一个大报文(>=MSS)发送出去。初看起来,TCP_CORK 的 描述和用途经常会和Nagle算法搞混淆,两者都是会合并小报文为一个大报文,一次发出去。但是两者又有不同。
(1)如果待发送数据包大小超过MSS, 则发送出去;
(2)如果不足MSS,则会在超时时间内(200 ms)发送出去;
TCP_CORK其实是更新激进的Nagle算法,完全禁止小包发送,而Nagle算法没有禁止小包发送,只是禁止了大量的小包发送。 在很多时候,我们鼓励禁止Nagle,因为ack回的快的话,相当于Nagle失效,ACK回的慢的话,Nagle要等待所有的ACK都应答后才传输,往往浪费掉很多时间。 而是使用TCP_CORK 代替, TCP_CORK 效率可能更高。
int state = 1;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &state, sizeof(state));
-TCP_NODELAY 表示禁止延迟发送,也就是会禁止 Nagle 算法 。
-TCP_CORK 和 TCP_NODELAY Linux 2.5.71 之前版本不能一起使用。之后一起使用的话,TCP_CORK会覆盖TCP_NODELAY 。
-TCP_QUICKACK 是 和 Delayed ACK 互斥的。
TCP博大精深,非常复杂,我这里只是管中窥豹。最后献上镇楼图一张 TCP 状态图TCP Finite State Machine (FSM)