上一篇介绍了TCP在连接和关闭时候的一些知识点。这篇介绍TCP在连接建立以后,传输中的重要特性。
三、TCP传输阶段
1 、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
包头各个字段意思很明显了,这里就不老调重弹了。只说一下你能从包头看出一些有意思的信息吗:
- 端口为16个bit,说明最多只能用65535个端口。协议上已经限制死了。即使你修改linux系统,扩大端口上限,也是没有用的。
- 序列号为32个bit, 说明跑一圈 2^32=4G数据,就要重复了。 这个问题后面会提到,通过 TCP Option - Timestamps 来配合解决这个限制问题。
-
window size 大小为16个bit。 限制料 传输窗口只有64k大小。这个在高速网络下是个性能缺陷。如何解决呢。还是通过 TCP Option - Window scale 来解决。Window scale 表示要把 window size 放大 多少倍,计算公式为: window_size = window_size * ( 2 ^ Window_scale ) . 看下面这个实例:28960*(2^7) = 144800 字节
- Data offset 为4个bit 最大为15。因为单位是4个字节, 所以tcp 包头最大长度为 15*4 = 60 个字节
- TCP Option 填充体: No-Operation (NOP) 长度为1, 内容为 0x01的数据,可以填充任意个。
2 、seq 和 ack
都说TCP是 有序,可靠的协议。关键是靠seq 和 ack 这两个字段:
- seq (Sequence number) : 发送数据包的编号, 用来保证数据的顺序,数据包可能会先发送的,但是后到达。给每个数据包在发送时按顺序编上号码,接收者按照seq 重新排序。就保证了正确性。
- ack (Acknowledgment number):期望接收下一个数据包的编号。 用来保证包不丢失。发送者通过这个确认对方到底是否收到包,决定是否丢失,进行重传.
至于如何利用这两个字段来保证TCP 有序,可靠的特性。需要用 TCP滑窗来解释
3、TCP重传
前面说到过,TCP包头里面有个window size 字段。用来高速对方,自己的接收能力。 滑窗机制就是利用这两个字段。来达到这个目录。
3.1、分包、丢包、乱序
TCP是基于IP协议的。 用户发送一个数据,对于TCP来说,会存在下列几种情况:
- 分包:tcp会对应用层发送过来的数据包进行分包
- 丢包 :因为网络故障原因,在路途中丢失了
- 延迟到达: 因为网络拥堵,等了很久才到达
- 乱序:同样由于网络拥堵。 先发的包,比后发送的包还要后达到接收方.
3.2、重传机制
包丢了怎么办,重传呗,这里涉及到1、什么时机重传,2、重传多少问题:
3.2.1、什么时候重传
- A、设置一个重传计时器(RTO): 到达超时时间,就开始重传。但是难在这超时时间设置多大。内网可能 10ms 就可以算超时,外网可能100ms才算超时。如果设置成 50ms超时,对内网来说太慢,降低传输速度。对外网来说太短,可能包只是延迟而已,刚重传玩就到了,造成重复传输,对本来已经拥堵的网络造成更大负担,积累下去就是恶性循环。
那如何解决定时器时长的问题呢? 就是超时时间不是静态不变的,根据网络状况动态调整。是一套比较复杂的公式(对公式感兴趣的自行去了解)。基于RTT(Round Trip Time) 计算 RTO((Retransmission TimeOut)), 使用RTO作为超时时间,这个值是动态变化的。 RTT是收到ACK的应答时间戳 减 这个包的发送时间戳。
关于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验证如此,经过检验出来的。
- B、快速重传机制(Fast Retransmit): 快重传要求 接收方在收到报文段后就立即发出确认,例如 图五 中,收到#5报文,立即发送(后面会提到delay-ack,ack有时候会随着后续数据报文一起捎带出去,注意这里是不延迟,立即发送) ack=3, 为的是使发送方及早知道#3报文没有到达。发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段(下图接收方收到#5, #7,#9号个报文,都是重复回应ack=3),这时候,发送方就知道#3报文丢失,开始重传#3报文,而不必继续等待设置的重传计时器时间到期才开始重传。 (这种算法,据说可提高网络吞吐量约20%)
3.2.2 重传次数问题
重传不是只有一次,会有多次。通过 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方案被提出来,解决这个问题.
- Selective Acknowledgment (SACK): 同样通过Tcp Option 扩展。在TCP头里加一个SACK的东西,ACK携带的ack num 还是的 快速重传得 ack num。但是还会带上 已经收到的 报文段落。
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 |
+--------+--------+--------+--------+
- DSACK(Duplicate SACK): 因为网络延迟原因,有些延迟报文会被当成丢失报文,让接收方通过SACK告诉发送方。还是会导致有些报文重复接收问题。于是发送方通过SACK 可以判断出哪些报文重复传输了,进而调整自己的RTO或拥塞控制。
DSACK没有单独额外去定义TCP Option. 而是共用了SACK的 TCP Option。 他使用 了 SACK的一个段位置。那么接收方如何判断 这个第一个段是 SACK还是DSACK的呢。 按照下面逻辑去判断
如果SACK的第一个段的范围被ACK所覆盖,那么就是DSACK
如果SACK的第一个段的范围被SACK的第二个段覆盖,那么就是DSACK
4、流量控制
4.1 滑动窗口(Sliding Window)
发送端和接收端都有缓冲区,但是缓冲区大小是由限的。假如接收端比较繁忙,没有来得及取走缓冲区的数据,发送端如果还一直发送,就会有问题了。需要有种机制,接收方通知发送方自己接收能力,让发送方调整发送速度。
TCP头里有一个字段叫Window Size,又叫Advertised-Window,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
发送方有个发送缓冲区,发送缓冲区的布局如下:
图七中各段含义
- Category#1已收到ack确认的数据。
- Category#2发还没收到ack的。
- Category#3在窗口中还没有发出的(接收方还有空间)。
- Category#4窗口以外的数据(接收方没空间)
Category#2 + Category#3就是发送者的TCP滑动窗口
4.2 滑窗是动态伸缩&前进
- 这个窗口大小时动态变化的。当接收方在TCP包头里面通知增大windows-size时候(rwnd), 这个窗口就是增加,当通知减小windows-size时候,这个窗口就减小。所以这个窗口的大小很大程度实际是由接收端控制(实际上是 Min [rwnd, cwnd], 至于cwnd的定义,后面拥塞控制会提到)。
- 这个窗口是不断向前移动的,随着ack的确认,不停的向前移动。
图八生动的演示了滑窗的移动和大小伸缩的变化。
4.3、流量控制
如果发送者发送数据过快,接收者来不及接收,那么就会有分组丢失。为了避免分组丢失,控制发送者的发送速度,使得接收者来得及接收,这就是流量控制。流量控制根本目的是防止分组丢失,它是构成TCP可靠性的一方面。
如何实现流量控制?由滑动窗口的存在。既保证了分组无差错、有序接收,也实现了流量控制。主要的方式就是接收方返回的 ACK 中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送。
5、拥塞控制
拥塞控制是防止传输数据的联络层网络出拥塞时数据大量丢失的情况。和流量控制 不同的是;流量控制主要是参考接收方的能力指标(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大小。
而且,这里计算TCP包在 [1420-1460] 之间,其实是没有考虑到第三层IP协议包头的扩展。其实IP包头也不是20个字节固定,也是可以扩展。
5.1.2、MSS(Maximum Segment Size )
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
PS: 从MTP和MSS定义可以看出他们关系 MSS 永远要小于MTU。 目的是尽量避免在IP层再次分片。
5.1.3、拥塞窗口cwnd(congestion window)
拥塞窗口是发送方维持的发送窗口大小,, 注意前面有个rwnd,表示接收方的能力。 真正的发送者发送窗口=min(rwnd, cwnd)。cwnd 的 计量单位是 MSS。 即 cwnd=1 表示 一个 MSS大小。 cwnd=2 表示2个 MSS大小。
cwnd大小也是根据网络状态,动态变化的。
5.1.4、慢启动算法
慢启动算法的思路就是: 不要一开始就发送大量的数据,这样很容易导致网络中路由器缓存空间耗尽,从而发生拥塞,而是先由小到大逐渐增加发送窗口的大小,探测一下网络的拥塞程度。
那么cwnd如何增长呢? 增长到什么程度停下来呢? 这里有个关键指标 慢启动门限ssthresh。
慢启动的算法如下: (cwnd全称Congestion Window):
1、连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。
2、每当收到一个ACK,cwnd++; 呈线性上升
3、每当过了一个RTT,cwnd = cwnd*2; 呈指数让升
4、还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”(后面会说这个算法)
可以看出,慢启动其实并不慢,基本上是以指数级增长的。
PS:如果出现超时,则执行下面拥塞避免算法。 cwnd=1重新开始。
5.2、拥塞避免 (Congestion Avoidance)
拥塞避免算法可以简单归纳成一句话:“加法增加, 乘法减少”。
前面说过,有一个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。
5.3、快速恢复算法(Fast Recovery)
快速恢复算法依赖前面讲的快速重传算法。即当出现连续三个重复ACK时候。不启用拥塞避免算法,而是
1、sshthresh = sshthresh /2 (乘法减少)
2、cwnd = sshthresh
3、进入加法增加, 乘法减少的 拥塞避免算法
比较前面拥塞避免算法, 可以看出最大的区别是没有一下子把cwnd降到1,而是降到一般开始重新尝试。因此 快速恢复算法相对来说比较乐观激进一些。它的主要思路是“认为如果连续收到3个重复的ACK,网络拥塞状况没有想象那么糟糕,可以尝试适当减缓一下发送速度”。
5、TCP其它相关算法
5.1、Nagle算法(纳格算法)
在实际发送数据中,可能会存在一种情况。发送方几个字节几个字节的发送数据给对端。这样就会造成资源极大的浪费。网络中传输的数据,报文大部分是协议的包头(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算法 缺省是开启的,说明实际网络中,前一种情况很常见。大部分能节省时间。
5.2、Delayed ACK
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所示
5.3、TCP_CORK
首先,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));
5.4、TCP_NODELAY
-TCP_NODELAY 表示禁止延迟发送,也就是会禁止 Nagle 算法 。
-TCP_CORK 和 TCP_NODELAY Linux 2.5.71 之前版本不能一起使用。之后一起使用的话,TCP_CORK会覆盖TCP_NODELAY 。
5.5、TCP_QUICKACK
-TCP_QUICKACK 是 和 Delayed ACK 互斥的。
6、结语
TCP博大精深,非常复杂,我这里只是管中窥豹。最后献上镇楼图一张 TCP 状态图TCP Finite State Machine (FSM)