0%

TCP 协议

TCP 基本认识

TCP面向连接的、可靠的、基于字节流的传输层通信协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

源端口(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。比例因子可以将窗口扩大到原来的 2n 次方,比如窗口大小缩放前为 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
                             +---------+ ---------\      active OPEN
| CLOSED | \ -----------
+---------+<---------\ \ create TCB
| ^ \ \ snd SYN
passive OPEN | | CLOSE \ \
------------ | | ---------- \ \
create TCB | | delete TCB \ \
V | \ \
+---------+ CLOSE | \
| LISTEN | ---------- | |
+---------+ delete TCB | |
rcv SYN | | SEND | |
----------- | | ------- | V
+---------+ snd SYN,ACK / \ snd SYN +---------+
| |<----------------- ------------------>| |
| SYN | rcv SYN | SYN |
| RCVD |<-----------------------------------------------| SENT |
| | snd ACK | |
| |------------------ -------------------| |
+---------+ rcv ACK of SYN \ / rcv SYN,ACK +---------+
| -------------- | | -----------
| x | | snd ACK
| V V
| CLOSE +---------+
| ------- | ESTAB |
| snd FIN +---------+
| CLOSE | | rcv FIN
V ------- | | -------
+---------+ snd FIN / \ snd ACK +---------+
| FIN |<----------------- ------------------>| CLOSE |
| WAIT-1 |------------------ | WAIT |
+---------+ rcv FIN \ +---------+
| rcv ACK of FIN ------- | CLOSE |
| -------------- snd ACK | ------- |
V x V snd FIN V
+---------+ +---------+ +---------+
|FINWAIT-2| | CLOSING | | LAST-ACK|
+---------+ +---------+ +---------+
| rcv ACK of FIN | rcv ACK of FIN |
| rcv FIN -------------- | Timeout=2MSL -------------- |
| ------- x V ------------ x V
\ snd ACK +---------+delete TCB +---------+
------------------------>|TIME WAIT|------------------>| CLOSED |
+---------+ +---------+

如何唯一确定一个 TCP 连接?

TCP 四元组可以唯一的确定一个连接,四元组包括如下:

  • 源地址
  • 源端口
  • 目的地址
  • 目的端口

TCP 连接建立

三次握手

  • 防止历史连接
  • 避免资源浪费
  • 同步双方初始序列号
1
2
3
4
5
6
7
8
9
10
11
    TCP A                                                TCP B

1. CLOSED LISTEN

2. SYN-SENT --> <SEQ=100><CTL=SYN> --> SYN-RECEIVED

3. ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED

4. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK> --> ESTABLISHED

5. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK><DATA> --> ESTABLISHED

重传机制

TCP 针对数据包丢失的情况,会用重传机制解决。

超时重传

RTTRound-Trip Time 往返时延)指的是数据发送时刻到接收到确认的时刻的差值,也就是包的往返时间。

超时重传时间是以 RTORetransmission Timeout)表示。

精确的测量超时时间 RTO 的值是非常重要的,这可让我们的重传机制更高效。

  • 当超时时间 RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差;
  • 当超时时间 RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。

根据上述的两种情况,我们可以得知,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。因为网络也是时常变化的,所以 RTO 是一个动态变化的值。

每当遇到一次超时重传的时候,都会将下一次 RTO 设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。

快速重传

当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传一个,还是重传所有的问题。为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 方法。

SACK

SACKSelective 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, 发包的个数是指数性的增长

  • 慢启动门限 ssthreshslow start threshold)状态变量,默认 65535 大小。
    • cwnd < ssthresh 时,使用慢启动算法
    • cwnd >= ssthresh 时,就会使用拥塞避免算法

拥塞避免

每当收到一个 ACK 时,cwnd 增加 1/cwnd, 发包的个数是线性的增长

拥塞发送

当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:

  • 超时重传
  • 快速重传

超时重传

  • ssthresh 设为 cwnd/2
  • cwnd 重置为 10 (是恢复为 cwnd 初始化值,Linux 默认的 cwnd 初始化值 10
  • 进入慢启动
    接着重新开始慢启动,慢启动是会突然减少数据流的, 这种方式太激进了,反应也很强烈,会造成网络卡顿。

快速重传

当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。

TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthreshcwnd 变化如下:

  • cwnd = cwnd/2 ,也就是设置为原来的一半
  • ssthresh = cwnd
  • 进入快速恢复算法

快速恢复

前面已讲进入快速恢复之前,cwndssthresh 已被更新了:

  • cwnd = cwnd/2 ,也就是设置为原来的一半
  • ssthresh = cwnd

然后,进入快速恢复算法如下:

  • 拥塞窗口 cwnd = ssthresh + 33 的意思是确认有 3 个数据包被收到了);
  • 重传丢失的数据包;
  • 如果再收到重复的 ACK,那么 cwnd 增加 1
  • 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;

参考资料