TCP 基本认识
TCP 是 面向连接的、可靠的、基于字节流的传输层通信协议。
1 | 0 1 2 3 |
源端口(Source Port)
源端口,16 位
目标端口(Destination Port)
目标端口,16 位
序列号 (Sequence Number)
TCP 是面向字节流的协议,通过 TCP 传输的字节流的每个字节都分配了序列号,序列号(Sequence number)指的是本报文段第一个字节的序列号。
序列号加上报文的长度,就可以确定传输的是哪一段数据。序列号是一个 32 位的无符号整数,达到 2^32-1 后循环到 0。
在 SYN 报文中,序列号用于交换彼此的初始序列号,在其它报文中,序列号用于保证包的顺序。
因为网络层(IP 层)不保证包的顺序,TCP 协议利用序列号来解决网络包乱序、重复的问题,以保证数据包以正确的顺序组装传递给上层应用。
初始序列号 (Initial Sequence Number, ISN)
在建立连接之初,通信双方都会各自选择一个序列号,称之为初始序列号。在建立连接时,通信双方通过 SYN 报文交换彼此的 ISN
序列号回绕
确认号 (Acknowledgment Number)
TCP 使用确认号(Acknowledgment number, ACK)来告知对方下一个期望接收的序列号,小于此确认号的所有字节都已经收到
头部长度 (Data Offset ):
只有 4 位, 取值范围 0~15, 表示 TCP头长度为 32位(4字节)的数量。TCP 头(甚至包括选项)的长度是 32 位的整数倍。例如 DOffset的值为 0111, 则该TCP包头的长度为 7 * 4 = 28
TCP Flags
SYN(Synchronize):用于发起连接数据包同步双方的初始序列号ACK(Acknowledge):确认数据包RST(Reset):这个标记用来强制断开连接,通常是之前建立的连接已经不在了、包不合法、或者实在无能为力处理FIN(Finish):通知对方我发完了所有数据,准备断开连接,后面我不会再发数据包给你了。PSH(Push):告知对方这些数据包收到以后应该马上交给上层应用,不能缓存起来
窗口大小 (Window Size)
窗口大小字段是接收方用来控制发送方数据流量的机制。它表示接收方当前允许发送方发送但尚未确认的数据字节数。通过动态调整窗口大小,TCP 可以有效地进行流量控制,防止发送方数据发送过快导致接收方缓存溢出。
TCP 协议窗口大小只有 16 位,最大窗口大小为 65535 字节(64KB), 在今天显然不够用,所以 TCP 协议引入了 TCP 窗口缩放 选项,作为窗口缩放的比例因子,比利因子的值在 0~14,其中 0 表示不缩放,最大值为 14。比例因子可以将窗口扩大到原来的 2 的 n 次方,比如窗口大小缩放前为 1050,缩放因子为 7,则真正的窗口大小为 1050 * 128 = 134400
窗口缩放选项
由于窗口大小字段只有 16 位,最大值为 65535 字节,这对于高带宽-延迟产品 (BDP) 的网络可能不够。为了解决这个问题,TCP 窗口缩放选项 (Window Scale Option) 被引入,用于扩展窗口大小的范围。
窗口缩放选项在 TCP 连接建立时通过 SYN 包交换协商。窗口缩放因子 (Window Scale Factor) 表示允许窗口大小值左移的位数,从而有效地扩展窗口大小的范围。
计算实际窗口大小
实际的窗口大小计算如下:
$$Actual Window Size = Window Size \times 2 ^{Window Scale Factor}$$
示例(带窗口缩放)
假设窗口大小字段的值是 8192(十进制),窗口缩放因子是 3:
$Actual Window Size = 8192 \times 2 ^ 3 = 8192 \times 8 = 65536 $ bytes
这表示接收方可以接受最多 65536 字节的未确认数据。
可选项 (Options)
只有 (DOffset > 5) 才有此数据,
可选项的格式:
一个字节的
KindNo-Operation: 无操作。
一个字节的
Kind, 一个字节的option-length,和真实的option dataMSS:最大段大小选项,是TCP允许的从对方接收的最大报文段SACK:选择确认选项Window Scale:窗口缩放选项TCP连接状态图
1 | +---------+ ---------\ active OPEN |
如何唯一确定一个 TCP 连接?
TCP 四元组可以唯一的确定一个连接,四元组包括如下:
- 源地址
- 源端口
- 目的地址
- 目的端口
TCP 连接建立
三次握手
- 防止历史连接
- 避免资源浪费
- 同步双方初始序列号
1 | TCP A TCP B |
重传机制
TCP 针对数据包丢失的情况,会用重传机制解决。
超时重传
RTT(Round-Trip Time 往返时延)指的是数据发送时刻到接收到确认的时刻的差值,也就是包的往返时间。
超时重传时间是以 RTO (Retransmission Timeout)表示。
精确的测量超时时间 RTO 的值是非常重要的,这可让我们的重传机制更高效。
- 当超时时间
RTO较大时,重发就慢,丢了老半天才重发,没有效率,性能差; - 当超时时间
RTO较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
根据上述的两种情况,我们可以得知,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。因为网络也是时常变化的,所以 RTO 是一个动态变化的值。
每当遇到一次超时重传的时候,都会将下一次 RTO 设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
快速重传
当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传一个,还是重传所有的问题。为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 方法。
SACK
SACK(Selective Acknowledgment),选择性确认。在 TCP 头部「选项」字段里加一个 SACK ,它可以将已收到的数据的信息发送给 「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以 只重传丢失的数据。
在
Linux下可以通过net.ipv4.tcp_sack参数开启/关闭这个功能(Linux 2.4后默认打开)。
D-SACK
D-SACK (Duplicate SACK) ,其主要使用了 SACK 来告诉 「发送方」有哪些数据被重复接收了。
D-SACK 有这么几个好处:
- 可以让发送方知道,是发出去的包丢了,还是接收方回应的
ACK包丢了 - 可以知道是不是发送方的数据包被网络延迟了
- 可以知道网络中是不是把发送方的数据包给复制了
在
Linux下可以通过net.ipv4.tcp_dsack参数开启/关闭这个功能(Linux 2.4后默认打开)。
滑动窗口
窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值
发送窗口
接收窗口
流量控制
发送方不能无脑的发数据给接收方,要考虑接收方处理能力。
如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。
为了解决这种现象发生,**TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。**
操作系统缓冲区和滑动窗口的关系
窗口关闭
如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。
糊涂窗口综合症
拥塞控制
慢启动
慢启动就是一点一点的提高发送数据包的数量, 当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1, 发包的个数是指数性的增长
- 慢启动门限
ssthresh(slow start threshold)状态变量,默认65535大小。cwnd<ssthresh时,使用慢启动算法。cwnd>=ssthresh时,就会使用拥塞避免算法。
拥塞避免
每当收到一个 ACK 时,cwnd 增加 1/cwnd, 发包的个数是线性的增长
拥塞发送
当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:
- 超时重传
- 快速重传
超时重传
ssthresh设为cwnd/2,cwnd重置为10(是恢复为cwnd初始化值,Linux默认的cwnd初始化值10)- 进入慢启动
接着重新开始慢启动,慢启动是会突然减少数据流的, 这种方式太激进了,反应也很强烈,会造成网络卡顿。
快速重传
当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。
TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下:
cwnd = cwnd/2,也就是设置为原来的一半ssthresh = cwnd- 进入快速恢复算法
快速恢复
前面已讲进入快速恢复之前,cwnd 和 ssthresh 已被更新了:
cwnd = cwnd/2,也就是设置为原来的一半ssthresh = cwnd
然后,进入快速恢复算法如下:
- 拥塞窗口
cwnd = ssthresh + 3(3的意思是确认有3个数据包被收到了); - 重传丢失的数据包;
- 如果再收到重复的
ACK,那么cwnd增加1; - 如果收到新数据的
ACK后,把cwnd设置为第一步中的ssthresh的值,原因是该ACK确认了新的数据,说明从duplicated ACK时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;