TCP

TCP protocol

Introduction

OSI 七层网络模型

MTU(Maximum Transmission Unit):网络设备或接口数据包最大值 MSS (Maximum Segment Size):TCP 段最大值

  • 物理层(Physical Layer) 报文名称:Bit(位)

  • 数据链路层(Data Link Layer) 报文名称:Frame(帧) 协议:Ethernet、Wi-Fi (IEEE 802.11) Ethernet MTU = 46~1518 Bytes

  • 网络层(Network Layer) 报文名称:Packet(包) 协议:IP、ICMP、BGP IP MTU = 1518 - 14(Frame Header) - 4(CRC) = 1500 Bytes

  • 传输层(Transport Layer) 报文名称:Segment(段)OR Datagram(报) 协议:TCP、UDP MSS = 1500(Ethernet MTU) - 20(IP Header) - 20(TCP Header) = 1460 Bytes

  • 会话层(Session Layer) 报文名称:DataStream(数据流)

  • 表示层(Presentation Layer) 报文名称:Message(消息) 协议:SSL/TLS

  • 应用层(Application Layer) 报文名称:Message(报文) 协议:HTTP、SMTP、SSH、Telnet

TCP 报文头格式

![[/DevOps/Network/attachements/Pasted image 20230905091738.png]] 一个TCP连接使用五元组定义同一个连接(src_ip, src_port, dst_ip, dst_port, protocol)

  • Sequence Number是包的序号,用来解决网络包乱序(reordering)问题。

  • Acknowledgement Number就是ACK——用于确认收到,用来解决不丢包的问题。

  • Window又叫Advertised-Window,也就是著名的滑动窗口(Sliding Window),用于解决流控的。

  • TCP Flag, 也就是包的类型,主要是用于操控TCP的状态机的。

TCP 状态机

![[/DevOps/Network/attachements/Pasted image 20230905101408.png]]

![[/DevOps/Network/attachements/Pasted image 20230905101422.png]]

  • 对于建链接的3次握手,主要是要初始化Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的Sequence Number(缩写为ISN:Inital Sequence Number)——所以叫SYN,全称Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。

  • 对于4次挥手,其实你仔细看是2次,因为TCP是全双工的,所以,发送方和接收方都需要Fin和Ack。只不过,有一方是被动的,所以看上去就成了所谓的4次挥手。如果两边同时断连接,那就会就进入到CLOSING状态,然后到达TIME_WAIT状态。下图是双方同时断连接的示意图(你同样可以对照着TCP状态机看)

![[/DevOps/Network/attachements/Pasted image 20230905101514.png]] 注意事项:

  • SYN_RECV 状态:server 端收不到建连的 ACK 包,重发 Syn+Ack 包。Linux 中默认重试5次,从1s开始每次翻倍,总共 1s + 2s + 4s + 8s+ 16s + 32s = 2^6 -1 = 63s,63s 超时后 TCP 才断开连接。优化参数:1)tcp_synack_retries 减少重试次数。2)tcp_max_syn_backlog 与 net.core.somaxconn 增大 SYN 半连接队列。3)tcp_abort_on_overflow 全连接队列满拒绝连接扔掉 Ack、tcp_syncookies 通过五元组 hash 一个 cookie 返回,客户端发回时携带过来建立连接(不建议开启)

  • ISN 初始化:ISN 会和一个假的时钟绑在一起,这个时钟会在每4微秒对 ISN 做加一操作,直到超过2^32,又从0开始。一个 ISN 的周期大约是4.55个小时。假设 TCP Segment 在网络上的存活时间不会超过 Maximum Segment Lifetime(MSL),所以只要 MSL 的值小于4.55小时就不会重用 ISN

  • MSL 与 TIME_WAIT:TIME_WAIT 状态到 CLOSED 状态超时设置为 2*MSL(RFC793定义了MSL为2分钟,Linux设置为30s 内核参数 net.ipv4.tcp_fin_timeout)。原因:1)TIME_WAIT 确保有足够的时间让对端收到了 ACK,如果被动关闭的那方没有收到 Ack,就会触发被动端重发 Fin,一来一去正好2个 MSL。2)有足够的时间让这个连接不会跟后面的连接混在一起(如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)

  • TIME_WAIT 数量过多:作为 client 端高并发短连接下,TIME_WAIT 状态太多。优化参数:1)tcp_tw_reuse 重用连接,需同时开启 tcp_timestamps=1(不太建议开启)。2)tcp_tw_recycle 假设对端开启了 tcp_timestamps 并比较时间戳重用连接,高版本已废弃。3)tcp_max_tw_buckets TIME_WAIT 状态数量,默认值180000,超过时系统 destory 打印警告

TIME_WAIT 状态只存在主动断开连接一端,HTTP 服务器建议开启 keepalive(浏览器会重用一个 TCP 连接来处理多个 HTTP 请求,http/1.1 版本以上默认开启),由客户端主动断开连接

数据传输中的 Sequence Number

wireshark filter expression: ip.addr == 172.22.3.29 && tcp.port == 9000 ![[/DevOps/Network/attachements/Pasted image 20230906170656.png]]

![[/DevOps/Network/attachements/Pasted image 20230906171805.png]] SeqNum的增加是和传输的字节数相关的。

注意:Wireshark 为了显示更友好,使用了 Relative SeqNum——相对序号,你只要在右键菜单中的protocol preference 中取消掉就可以看到“Absolute SeqNum”了。

TCP 重传机制

注:接收端给发送端的 ACK 确认只会确认最后一个连续的包 1、超时重传机制:1-5五份数据,第3份数据收不到时

  • 仅重传丢失 timeout 的包,也就是第3份(节省带宽,慢)

  • 重传 timeout 之后所有包,3 4 5三份数据(好一点,浪费带宽)

2、快速重传机制 Fast Retransmit 算法,不以时间驱动,以数据驱动重传。只ack最后可能丢的那个包。第一份先到送了,于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2,因为2还是没有收到,于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是ack回6 ![[/DevOps/Network/attachements/Pasted image 20230906172736.png]] 问题:重传是重传 ACK 丢失的包还是之前的所有包?

3、Selective Acknowledgment(SACK):需要在 TCP 头里加一个 SACK 的东西,ACK 还是 Fast Retransmit 的 ACK,SACK 则是汇报收到的数据碎版 ![[/DevOps/Network/attachements/Pasted image 20230906172757.png]] 在发送端就可以根据回传的 SACK 来知道哪些数据到了,哪些没有到。于是就优化了 Fast Retransmit 的算法。当然,这个协议需要两边都支持。 Linux 内核参数 net.ipv4.tcp_sack=1 开启该功能 注意:接收方 Reneging 问题,接收方有权扔掉发送方的 SACK 数据。接收方可能需要内存给更重要的东西,所以发送方不能完全依赖 SACK,还需要 ACK 并维护 Time-Out,如果后续的 ACK 没有增长依然需要重传 SACK 的数据。

4、Duplicate SACK(D-SACK):重复收到数据的问题,其主要使用了SACK来告诉发送方有哪些数据被重复接收了

  • ACK 丢包:SACK 的第一个段的范围被 ACK 所覆盖,那么就是 D-SACK。如图所示请求中丢了两个 ACK 包(3500,4000),第三个包返回 ACK=4000 SACK=3000-3500,则这个 SACK 为 D-SACK 包,说明数据没丢而是 ACK 包丢了。 ![[/DevOps/Network/attachements/Pasted image 20230907091907.png]]

  • 网络延误:SACK 的第一个段的范围被 SACK 的第二个段覆盖,那么就是 D-SACK。如图所示,网络包(1000-1499)被网络给延误了,导致发送方没有收到 ACK,而后面到达的三个包触发了“Fast Retransmit算法”,所以重传,但重传时被延误的包又到了,所以回了一个SACK=1000-1500,因为 ACK 已到了3000,所以,这个 SACK 是 D-SACK——标识收到了重复的包。

这个案例下,发送端知道之前因为“Fast Retransmit算法”触发的重传不是因为发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延时了。

Linux 内核参数 net.ipv4.tcp_dsack=1 开启该功能 ![[/DevOps/Network/attachements/Pasted image 20230907091442.png]] 使用 D-SACK 好处: 1)可以让发送方知道,是发出去的包丢了,还是回来的 ACK 包丢了。 2)是不是自己的 timeout 太小了,导致重传。 3)网络上出现了先发的包后到的情况(又称 reordering) 4)网络上是不是把数据包给复制了

TCP RTT算法

RTT(Round Trip Time):数据包从发送到ACK回来的时间,发送端发包时间是t0,ACK接收到时间是t1,RTT采样=t1-t0

RTO(Retransmission TimeOut):TCP 的 TimeOut 设置,让重传高效

算法:经典算法(加权移动平均)、Karn / Partridge 算法、Karn / Partridge 算法

TCP 滑动窗口 - Sliding Window

TCP 头中字段 Window(Advertised-Window):接收端告诉发送端自己还有多少缓冲区可以接收数据

![[/DevOps/Network/attachements/Pasted image 20230907171639.png]]

  • 接收端LastByteRead指向了TCP缓冲区中读到的位置,NextByteExpected指向的地方是收到的连续包的最后一个位置,LastByteRcved指向的是收到的包的最后一个位置,我们可以看到中间有些数据还没有到达,所以有数据空白区。

  • 发送端的LastByteAcked指向了被接收端Ack过的位置(表示成功发送确认),LastByteSent表示发出去了,但还没有收到成功确认的Ack,LastByteWritten指向的是上层应用正在写的地方 于是:

  • 接收端在给发送端回ACK中会汇报自己的AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;

  • 而发送方会根据这个窗口来控制发送数据的大小,以保证接收方可以处理

发送方滑动窗口示例: 滑动前 ![[/DevOps/Network/attachements/Pasted image 20230908140914.png]] 滑动后 ![[/DevOps/Network/attachements/Pasted image 20230908141002.png]]

![[/DevOps/Network/attachements/Pasted image 20230908141907.png]]

Zero window

发送端在窗口变成0后,会发 ZWP 的包给接收方,让接收方来 ack 他的 Window 尺寸,一般这个值会设置成3次,每次大约30-60秒(不同的实现可能会不一样)。如果3次过后还是0的话,有的 TCP 实现就会发 RST 把链接断了。

注意:只要有等待的地方都可能出现 DDoS 攻击,Zero Window 也不例外,一些攻击者会在和HTTP 建好链发完 GET 请求后,就把 Window 设置为0,然后服务端就只能等待进行 ZWP,于是攻击者会并发大量的这样的请求,把服务器端的资源耗尽。

Wireshark中,可以使用 tcp.analysis.zero_window 来过滤包,然后使用右键菜单里的follow TCP stream,你可以看到 ZeroWindowProbe 及 ZeroWindowProbeAck 的包

Silly Window Syndrome

接收方太忙了,来不及取走Receive Windows 里的数据,那么,就会导致发送方越来越小。到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的 window,而我们的发送方会义无反顾地发送这几个字节。MSS=1460,这样发送携带 IP 头与 TCP 头浪费带宽。 解决办法:避免对小的window size做出响应,直到有足够大的window size再响应。可以 receiver 和 sender 同时实现。

  • 在 receiver 端,如果收到的数据导致 window size 小于某个值,可以直接 ack(0)回 sender,这样就把 window 给关闭了,也阻止了 sender 再发数据过来,等到 receiver 端处理了一些数据后windows size 大于等于了MSS,或者,receiver buffer有一半为空,就可以把window打开让 sender 发送数据过来。

  • Sender 端引起的,那么就会使用著名的 Nagle’s algorithm。这个算法的思路也是延时处理,他有两个主要的条件:1)要等到 Window Size>=MSS 或是 Data Size >=MSS,2)收到之前发送数据的 ack 回包,他才会发数据,否则就是在攒数据。

TCP 拥塞处理 - Congestion Handling

1)慢启动

  1. 连接建好的开始先初始化 cwnd = 1,表明可以传一个 MSS 大小的数据。

  2. 每当收到一个 ACK,cwnd++;呈线性上升

  3. 每当过了一个 RTT,cwnd = cwnd*2;呈指数上升

  4. ssthresh(slow start threshold)阈值。当 cwnd >= ssthresh 时,就会进入“拥塞避免算法”

2)拥塞避免 一般来说ssthresh的值是65535,单位是字节,当cwnd达到这个值时后,算法如下:

  1. 收到一个ACK时,cwnd = cwnd + 1/cwnd

  2. 当每过一个RTT时,cwnd = cwnd + 1

3)拥塞发生(快速重传)

  1. 等到RTO超时,重传数据包。TCP认为这种情况太糟糕,反应也很强烈。

  • sshthresh = cwnd /2

  • cwnd 重置为 1

  • 进入慢启动算法

  1. Fast Retransmit算法,也就是在收到3个duplicate ACK时就开启重传,而不用等到RTO超时。

  • TCP Tahoe 的实现和 RTO 超时一样。

  • TCP Reno的实现是:

    • cwnd = cwnd / 2

    • sshthresh = cwnd

    • 进入快速恢复算法——Fast Recovery

4)快速恢复

  1. cwnd = sshthresh + 3 * MSS(3的意思是确认有3个数据包被收到了)

  2. 重传 Duplicated ACKs 指定的数据包

  3. 如果再收到 duplicated Acks,那么 cwnd = cwnd +1

  4. 如果收到了新的Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法了。

算法示意图 ![[/DevOps/Network/attachements/Pasted image 20230908161112.png]]

TCP 全连接与半连接队列

半连接队列溢出 & SYN Flood

测试使用 tcp-server 端

全连接队列溢出

Reference:

Last updated