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
) 才有此数据,
可选项的格式:
一个字节的
Kind
No-Operation
: 无操作。
一个字节的
Kind
, 一个字节的option-length
,和真实的option data
MSS
:最大段大小选项,是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
时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;