计算机 · 2021年12月30日 0

TCP与QUIC

这篇文章里链接了一个QUIC的那些事系列文章,点击链接,可以看到作者的一个TCP/IP系列文章,分别讲了TCP和QUIC的一些东西.

TCP

1. MSS

Maximum Segment Size,指TCP包里所含数据的最大长度(不包括TCP头).在syn/ack里协商,一般为1460(加上20字节TCP头,20字节IP头正好是数据链路层MTU).

2. MTU

常说的MTU是数据链路层的MTU,数据链路层里每个包里的数据部分长度的MTU一般为1500,数据链路层本身还有6个字节的源地址,6个字节的目的地址,2个字节的类型,以及4个字节的CRC校验.所以数据链路层一个包里的数据在达到1500字节的情况下,整个包的大小为1500+6+6+2+4=1518字节.
MTU最小值是46(64-6-6-2-4),这是由CSMA/CD机制检测碰撞所决定的,如果一个数据链路层包里的数据没有达到46个字节,需要进行补0.
探测MTU是通过给IP包设置DF(不要分片)标志来实现的,这样当IP包大于MTU时整个包就会被丢弃掉.

3. 四次挥手

 

一般面试中会经常问到这个TIME_WAIT状态的意义:

  1. 防止最后一个ack丢失,对端需要重传fin;
  2. 有足够的时间让这个连接不会跟后面的连接混在一起;

对负载高的服务器,可能出现TIME_WAIT状态的套接字很多,解决方法:

  1. tcp_tw_reuse(需要同时设置tcp_timestamps=1)
  2. tcp_tw_recycle 设置这两个参数可能导致建连出问题.

两端几乎同时close的情况: 

4. 三次握手

  1. 序列号的初始值不能是硬编码的,而是和内部的一个虚拟始终绑定的;
  2. 建立连接时,如果客户端一直不回最后一个ack,那么服务器端就会不断重传自己的那个syn+ack包,并且会重传5次,需要总共1s+2s+4s+8s+16s+32s=63s才会放弃这个这个TCP连接;
  3. syn flood攻击:如果客户端恶意的不停地发syn包,并且不回最后一个ack,那么就会很快耗尽服务器的syn连接队列;导致服务器不能为正常用户的连接服务;
  4. syn_cookie:为了应对syn flood攻击,linux在syn连接队列满的时候可以使用syn_cookie机制,对于客户端发过来的syn,利用源地址端口、目标地址端口和时间戳生成一个cookie发回客户端,并且这个时候服务端不会为这个新建的半连接存任何东西(也就避免了耗尽服务器的资源),客户端发最后一个ack时需要把这个cookie带上,服务器根据cookie恢复出源地址端口、目标地址端口和时间戳,并根据这些信息建立完整的TCP连接.
  5. syn_cookie的缺点:cookie里保存的东西有限,会丢掉客户端建连时带的一些TCP选项.

同时建立连接的情形: 

5. TCP报文格式解析

  1. 1-2字节:源端口
  2. 3-4字节:目的端口
  3. 5-8字节:序号,序号用来标识从TCP发端向TCP收端发送的数据字节流,到达2^32-1后又从0开始
  4. 9-12字节:确认号;确认号是发送确认的一端所期望收到的下一个序号
  5. 13-14字节:首先是4bit的首部长度(首部长度给出首部中32 bit字的数目。需要这个值是因为任选字段的长度是可变的,TCP最多有60字节的首部。如果没有任选字段,正常的长度是20字节);随后是6bit的保留字段;最后是6bit的Flags,含义如下:
    1. 紧急URG:当URG=1时,表示报文段中有紧急数据,应尽快传送
    2. 确认比特ACK:ACK = 1时代表这是一个确认的TCP包,取值0则不是确认包
    3. 推送比特PSH:当发送端PSH=1时,接收端尽快的交付给应用进程
    4. 复位比特(RST):当RST=1时,表明TCP连接中出现严重差错,必须释放连接,再重新建立连接
    5. 同步比特SYN:在建立连接是用来同步序号。SYN=1, ACK=0表示一个连接请求报文段,SYN=1,ACK=1表示同意建立连接
    6. 终止比特FIN:FIN=1时,表明此报文段的发送端的数据已经发送完毕,并要求释放传输连接
  6. 15-16字节:我方窗口上限,用于控制对方发送的数据量;
  7. 17-18字节:校验和,该字段检验的范围包括首部和数据这两部分。由发端计算和存储,并由收端进行验证。
  8. 19-20字节:紧急指针,紧急指针在URG=1时才有效,它指出本报文段中的紧急数据的字节数。
  9. 21-60字节:可选(长度可变),可以有,也可以无。

可以发现TCP头里没有标明payload的长度,这个长度需要通过IP包的长度减去IP头再减去TCP头长度计算得到.

6. 快速重传

由于TCP使用的是累积确认的方式,在丢失序号为n的包,且接收到序号大于n的包时,接收端会发ack n,那么发送端在连续重复收到3个ack n(总共收到4次ack n)后,就可以不用等待序号为n的包的超时重传定时器,而是直接重传序号为n的包了.快速重传算法仍然有一个问题,那就是仍然需要重传序号为n的包之后的包,为了解决这个问题,需要SACK.

7. SACK

需要在握手时通过TCP Sack-Permitted Option选项确认双方都支持SACK,握手时占用两个字节:

       +---------+---------+
       | Kind=4  | Length=2|
       +---------+---------+

在传输过程中,出现丢包时,通过SACK option指出收到的丢失包后面收到的包:

                         +--------+--------+
                         | Kind=5 | Length |
       +--------+--------+--------+--------+
       |      Left Edge of 1st Block       |
       +--------+--------+--------+--------+
       |      Right Edge of 1st Block      |
       +--------+--------+--------+--------+
       |                                   |
       /            . . .                  /
       |                                   |
       +--------+--------+--------+--------+
       |      Left Edge of nth Block       |
       +--------+--------+--------+--------+
       |      Right Edge of nth Block      |
       +--------+--------+--------+--------+
  1. 每个block确认收到的范围是[Left Edge, Right Edge).占用的TCP头字节数为2+8*k,k为block数,所以每个tcp包里最多包含4个block,而且如果打开了timestamp选项(占用10个字节),那么包含的block数又要减少,最多为3.
  2. sack只能算是优化重传的一种手段,真正确认包到达还是需要原来的累积确认ack包,即发送端重传时可以通过检查数据包的SACKed标志,避免不必要的重传,但是只有在收到累计确认包后才可以将该数据包从重传队列里删掉.
  3. 接收方Reneging:接收方可能出于内存不足等情况将已经SACK的数据包给丢掉,这就是上面第二点必须收到累计ACK才能将数据包从重传队列里删除的原因,在这种情况下这些已经SACK但是被丢掉的数据还是可以通过超时重传进行重传,从而达到数据可靠传输的目的.

8. D-SACK

SACK可以帮助发送端区分出已经收到不必要重传的数据包,对SACK进行扩展,形成D-SACK,可以帮助发送端区分出接收端重复收到的包.
D-SACK仍然使用了SACK的格式,只是需要额外的逻辑判断是否是D-SACK.
D-SACK使用了SACK的第一个段来做标志,如何判断D-SACK:

  1. 如果SACK的第一个段的范围被ACK所覆盖,那么就是D-SACK
  2. 如果SACK的第一个段的范围被SACK的第二个段覆盖,那么就是D-SACK

D-SACK的规则如下:

  1. 第一个block将包含重复收到的报文段的序号
  2. 跟在D-SACK之后的SACK将按照SACK的方式工作
  3. 如果有多个被重复接收的报文段,则D-SACK只包含其中第一个

9. 滑动窗口

window scale选项:kind=3,用于设置大的window size,最终的window大小为window size value * window size scaling factor.

Zero Window 接收窗口为0后,发送窗口不能继续发送数据,这时需要通过另外的方式更新这个接收窗口:

  1. 发送端发送Zero Window Probe,接收端ACK其窗口大小.发送ZWP的时间间隔为[CFG_WIN_PROBE_MIN,CFG_WIN_PROBE_MAX],如果3次过后还是0的话,有的TCP实现就会通过RST断开连接。
  2. 如果接收端滑动窗口不为0,则接收端主动发送TCP Window Update来更新发送方的滑动窗口.

Silly Window Syndrome 如果接收端来不及接收RCV.WND中的数据,那么窗口就会越来越小直至为0,如果接收端刚刚腾出很小的空间就告知发送端,那么发送端就会继续发送数据,这种现象就叫做Silly Window Syndrome(糊涂窗口综合征)。
要解决这个问题也不难,就是避免对小的Window size做出响应,直到有足够大的window size再响应,这个思路可以同时实现在发送端和接收端两端。

  • 如果这个问题是由接收端引起的,可以:如果接收端收到的数据导致Window size小于某个值,可以直接Ack(0)回复发送端,这样就把Window给关闭了,发送端就不会继续发送数据。当接收端处理了一些数据后Windows size 大于等于了MSS,或者接收端缓冲区有一半为空,通知发送端可以继续发送数据。
  • 如果这个问题是由发送端引起的,那么就会使用著名的 Nagle’s 算法,这个算法的思路也是延时处理,需要满足如下两个条件:
    1. 要等到 Window size>=MSS 或是 Data Size >=MSS;
    2. 收到之前发送数据的Ack,才会发数据,否则就是在攒数据;

10. 慢启动

什么时候用慢启动算法

  1. 连接刚刚建立时
  2. 检测到丢包时

慢启动算法原理

  1. 连接建好的开始先初始化CWS = 1,表明可以传一个MSS大小的数据
  2. 每当收到一个ACK,CWS++; 呈线性上升
  3. 每当过了一个RTT,CWS = CWS*2; 呈指数让升
  4. 当CWS超过一个极限值SSThresh(Slow Start Threshold)时,就不在按照上述的增长方式增加,而是进入“拥塞避免”

11. 在途字节数

在途字节数 = 已发送数据-已收到Ack的数据 = Seq + Len – Ack

12. 拥塞避免

慢启动算法的拥塞窗口大小CWS(Congestion Window Size )有个上限值SSThresh(Slow Start Threshold),当CWS >= SSThresh时,就会进入“拥塞避免”阶段。一般来说SSThresh的值是65535字节,当CWS达到这个值时后,CWS的更新算法如下:

  1. 收到一个ACK时,CWS = CWS + 1/CWS
  2. 当每过一个RTT时,CWS =CWS + 1

进入拥塞避免阶段后,网络依然会拥塞。如果进入拥塞状态,网络发生丢包,那么有两种处理机制:

  1. 等到RTO超时,重传数据包
    SSThresh及CWS的更新过程如下:
    SSThresh = CWS / 2
    CWS = 1
    随后进入慢启动过程
  2. 使用快速重传(Fast Retransmit):收到3个Duplicate ACK时就开启重传
    TCP Tahoe的实现和RTO超时一样;
    TCP Reno的实现是:
    CWS = CWS /2
    SSThresh = CWS
    进入快速恢复算法

13. 快速恢复

TCP RENO中的快速恢复算法:

  1. 启动快速重传
  2. 设置SSThresh = CWS/2 以及 CWS = SSThresh + 3 * MSS.
  3. 使用拥塞避免算法更新CWS及SSThresh :
    1)收到一个Ack时,CWS= CWS+ 1/CWS
    2)当每过一个RTT时,CWS= CWS+ 1

TCP New Reno算法:

  1. 当发送端收到3个重复Acks,进入快速重传模式,重传重复Acks指示的那个包。如果只有这一个包丢了,重传这个包后回来的Ack会把整个已经被发送端传输出去的数据Ack回来。如果没有的话,说明有多个包丢了,我们叫这个ACK 为 Partial ACK。
  2. 一旦发送端发现 Partial ACK 出现,发送端就可以推理出来有多个包被丢了,于是继续重传滑动窗口里未被 Ack 的第一个包。直到再也收不到了 Partial Ack,才真正结束快速重传这个过程 

14. Early Retransmit

出现dupack不够的情况:

  1. cwnd较小
  2. 发送窗口里大量的数据包都被丢失了
  3. 在数据发送的尾端发生丢包时

但是,上面各种极端的case有共同的特点:

  1. 无法产生足够的dupack
  2. 没有新的数据包可以发送进入网络

Early Retransmit机制就是在判断条件m和n都成立后,选择降低触发Fast Retransmit 的阈值,来避免只能通过RTO超时重传的问题。

15. TLP(Tail Loss Probe)

ER解决dupack不够用的情况,TLP解决没有dupack可用的情况。
TLP算法会在TCP还是Open状态的时候,设置一个Probe TimeOut (PTO)。 当链路中有未被确认的数据包,同时在PTO时间内未收到任何ACK,则会触发PTO 超时处理机制。 TLP会选择传输序号最大的一个数据包作为tail loss probe包,这个序号最大的包可能是 一个可以发送的新的数据包,也可能是一个重传包。 TLP通过这样一个tail loss probe包,如果能够收到相应的ACK,则会触发FR机制,而不是RTO机制。

TCP FAST OPEN

TCP FAST OPEN选项可以允许在syn包中携带应用层数据,以及允许服务器在三次握手完成前将syn包中携带的数据返回给应用层.
工作原理:第一次连接时服务器会返回cookie给客户端,客户端需要保存这个cookie,后面客户端在重新连接时带上这个cookie,并且在syn包中可以携带数据.
开启方式:sysctl修改net.ipv4.tcp_fast_open,设置的值默认为1的话只是允许客户端在syn包中携带数据,因此对于服务器需要将这个值改为3,以打开服务器端的FAST OPEN选项,以允许服务器在三次握手完成前将接收到的syn包中的数据抛给应用层.
编程注意点:客户端想要在syn包里发送数据,那就不能使用connect,而是使用sendto,sendmsg直接发送数据,而且要带上TCP_FASTOPEN选项.服务器端需要通过setsockopt设置下FASTOPEN选项,设置TFO队列长度.

16. 帧类型

普通帧类型

  • PADDING
  • RST_STREAM
  • CONNECTION_CLOSE
  • GOAWAY
  • WINDOW_UPDATE
  • STOP_WAITING
  • PING 特殊帧类型
  • STREAM
  • ACK
  • CONGESTIN_FEEDBACK

ACK中的序列号为迄今为止观察到的最大PacketNumber,随后是丢失数据包列表或者NACK。

14. NAT

QUIC 1.包类型和格式

RFC9000所说,一个QUIC包里是可以包含不同stream的frame的,因此丢一个包是可能导致这个包里的几个stream阻塞的.在实现QUIC的时候,应该让尽量少的stream的frame放在同一个包里:

A sender sends one or more frames in a QUIC packet; see Section 12.4.

A sender can minimize per-packet bandwidth and computational costs by including as many frames as possible in each QUIC packet. A sender MAY wait for a short period of time to collect multiple frames before sending a packet that is not maximally packed, to avoid sending out large numbers of small packets. An implementation MAY use knowledge about application sending behavior or heuristics to determine whether and for how long to wait. This waiting period is an implementation decision, and an implementation should be careful to delay conservatively, since any delay is likely to increase application- visible latency.

Stream multiplexing is achieved by interleaving STREAM frames from multiple streams into one or more QUIC packets. A single QUIC packet can include multiple STREAM frames from one or more streams.

One of the benefits of QUIC is avoidance of head-of-line blocking across multiple streams. When a packet loss occurs, only streams with data in that packet are blocked waiting for a retransmission to be received, while other streams can continue making progress. Note that when data from multiple streams is included in a single QUIC packet, loss of that packet blocks all those streams from making progress. Implementations are advised to include as few streams as necessary in outgoing packets without losing transmission efficiency to underfilled packets.

特殊Packet:

  • 版本协商Packet
  • Public Reset Packet 

普通Packet:

  • FEC Packet
  • Frame Packet

QUIC 2.帧类型和格式

QUIC 3.0RTT

QUIC 4.拥塞控制

  1. 单调递增的Packet Number,可以帮助区分重传包和非重传包,每个Stream有自己的Offset.
  2. 不允许 Reneging:协议层面不允许丢弃已经SACK的包,避免TCP那种接收方丢弃SACK包的行为,防止干扰重传.
  3. 更多的Ack Block:TCP头由于长度的限制(最多60字节),SACK最多带4个Block,Quic Ack Frame 可以同时提供 256 个 Ack Block,在丢包率比较高的网络下,更多的 Sack Block 可以提升网络的恢复速度,减少重传量.
  4. QUIC计算了Ack Delay(从收到数据包到Ack该数据包的delay),让RTT的计算更准确.