【lwip】12-一文解决TCP原理

前言

TCP的实现比UDP复杂很多。

所以把原理篇和源码篇分开写。

原文:https://www.cnblogs.com/lizhuming/p/16883586.html
李柱明博客园:https://www.cnblogs.com/lizhuming/

12.1 TCP协议简介

传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。

12.2 TCP相关的一些概念词

12.2.1 MSL

MSL :报文段最大生存时间,它是任何报文段被丢弃前在网络内的最长时间。

  • RFC 793 [Postel 1981c] 指出MSL为2分钟。现实中的常用值是30秒,1分钟,或2分钟。

12.2.2 MSS

MSS:Maximum Segment Size (MSS) Option

参考:RFC 1122, chap 4.2.2.6

12.3 TCP工作特性

12.3.1 面向连接

TCP是面向连接的传输层协议。应用程序在使用TCP协议之前,必须先建立TCP连接。在传送数据完毕后,必须释放已经建立的TCP连接。

12.3.2 全双工通信

在 TCP 连接建立后,那么两个主机就是对等的,任何一个主机都可以向另一个主机发送数据,数据是双向流通的,所以 TCP 协议是一个全双工的协议。

12.3.3 可靠性

TCP通过下列方式来提供可靠性:

  • 报文段:应用数据被分割成TCP认为最适合发送的数据块。由TCP传递给IP的信息单位称为报文段或段(segment)。

  • 确认与重传:当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。接收端收到数据后,需要响应一个确认。这个确认可以不是立即发送,通常将推迟几分之一秒(延迟确认)。发送端如果超时也未能收到一个确认,将重发这个报文段。

  • 差错控制:

    • 校验和:TCP会对TCP首部(包括伪首部)和TCP数据进行校验和验证。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错, TCP将丢弃这个报文段和不确认收到此报文段(希望发端超时并重发)。
    • 丢弃重复报文:IP数据报会发生重复,TCP的接收端必须丢弃重复的数据。
    • 重排序:TCP报文段作为 IP 数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。如果必要,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层。
  • 流量控制:TCP还能提供流量控制。TCP连接的每一方都有固定大小的缓冲空间。 TCP的接收端只允许另一端发送 接收端缓冲区 所能接纳的数据。(窗口)

12.3.4 缓冲机制

发送缓冲:

在发送方想要发送数据的时候,由于应用程序的数据大小、类型都是不可预估的,所以TCP提供了缓冲机制来处理这些数据。

在发送少量数据时,协议通常会延迟发送数据的时间,已缓冲到更多的用户数据后,组成一个合适大小的报文段再发送出去。

对于每个发送出去的报文,TCP也不会马上删除他们,而是将他们保存在缓冲区中,以便超时时重传,收到ACK时才删除。

接收缓冲:

报文到达时,可能是乱序的,也可能因为应用层来不及处理,需要缓存起来,这些数据就能先保存到接收缓冲区。

12.3.5 拥塞控制

如果一个主机还是以很大的流量给另一个主机发送数据,但是其中间的路由器通道很小,无法承受这样大的数据流量的时候,就会导致拥塞的发生,而拥塞控制考虑的就是网络的传输状况。

通常在路由器发送拥塞时,它会丢弃掉不能处理的数据报,这将导致发送方因接收不到确认而重传,重传的数据同样不会成功,且重传会使得路由器中拥塞更为严重。

拥塞发生时报文被丢弃,但是发送方不会得到任何报文丢失的信息,因此,发送方必须实现一种自适应机制,及时检测网络中的拥塞状况,自动调节数据的发送速度,这样才能提高数据发送的成功率。

在TCP中,引进了一个名为拥塞窗口的概念,与滑动窗口相似,拥塞窗口也是发送方控制数据发送速度的方式之一。

12.3.6 基于字节流

tcp是面向字节流的,数据间没有明显的间隔。

12.3.7 其它机制

糊涂窗口避免、零窗口探查、连接保活等。

12.4 TCP报文

12.4.1 TCP报文段封装

12.4.2 TCP报文段格式

  • 端口号 :每个TCP段都包含源端和目的端的端口号,用于寻找发端和收端应用进程。这两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接。

    • socket :包含客户 IP 地址、客户端口号、服务器 IP 地址和服务器端口号的四元组。
  • 序号 :用于对字节流进行编号。

    • 例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。
  • 确认号 :期望收到的下一个报文段的序号。

    • 例如B正确收到A发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此B期望下一个报文段的序号为 701,B发送给A的确认报文段中确认号就为 701。
  • 数据偏移 :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。单位:字。

  • URG :紧急(The urgent pointer) 标志置位。只有当 URG 标志置1时紧急指针才有效。

  • ACK :确认标志。当 ACK=1 时确认号字段有效,否则无效。

    • TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。
  • PSH :推标志。该标志置位时,接收端不将该数据进行队列处理,而是尽可能快将数据转由应用处理。

    • 在处理 telnet 或 rlogin 等交互模式的连接时,该标志总是置位的。
  • RST :复位标志。复位标志有效,重建连接。

  • SYN :同步标志。同步序列编号(Synchronize Sequence Numbers)栏有效。

    • 在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。
  • FIN :结束标志。用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放运输连接。

  • 窗口 :窗口值作为接收方让发送方设置其发送窗口的依据。流量控制。

  • 校验和 :检验和覆盖了整个的TCP报文段:TCP首部(包括伪首部)和TCP数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。

  • 紧急指针 :紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。

    • TCP的紧急方式是发送端向另一端发送紧急数据的一种方式。
    • 只有当 URG 标志置1时紧急指针才有效。
  • 选项 :长度可变,最长可达40字节。当没有使用“选项”时,TCP的首部长度是20字节。选项字段长度需要为4字节的整数倍,不够的需要进行填充来模组4字节整数倍。

12.4.3 TCP伪首部

TCP校验和的计算包括了三部分:TCP伪首部+TCP首部+TCP数据区。

TCP伪首部包含IP首部一些字段。其目的是让TCP验证数据是否已经正确到达目的地。

TCP伪首部只参与校验,不参与实际发送。

12.4.4 TCP选项字段

kind
(Type)
Length Name Reference 描述&用途
0 1 EOL RFC 793 选项列表结束
1 1 NOP RFC 793 无操作(用于补位填充)
2 4 MSS RFC 793 最大segment长度
3 3 WSOPT RFC 1323 窗口扩大系数(Window Scaling Factor)
4 2 SACK-Premitted RFC 2018 表明支持SACK
5 可变 SACK RFC 2018 SACK Block(收到乱序数据)
8 10 TSOPT RFC 1323 Timestamps
19 18 TCP-MD5 RFC 2385 MD5认证
28 4 UTO RFC 5482 user Timeout(超过一定闲置时间后拆除连接)
29 可变 TCP-AO RFC 5925 认证(可选用各种算法)
253/254 可变 Experimental RFC 4727 保留,用于科研实验

12.4.5 选项格式

一般Option的格式为TLV结构,如下所示:

Kind / Type(1 Byte) Length(1 Byte) Value
  1. EOL和NOP Option(Kind 0、Kind 1)只占1 Byte,没有Length和Value字段;
  2. NOP用于将TCP Header的长度补齐至32bit的倍数(由于Header Length字段以32bit为单位,因此TCP Header的长度一定是32bit的倍数);
  3. SACK-Premitted Option占2 Byte,没有Value字段;
  4. 其余Option都以1 Byte的“Kind”开头,指明Option的类型;Length指明Option的总长度(包括Kind和Length)
  5. 对于收到“不能理解”的Option,TCP会无视掉,并不影响该TCP Segment的其它内容。

12.4.6 wireshark报文分析

12.5 TCP状态变迁图

需要熟记TCP的状态变迁图,这有助于源码阅读。

12.6 TCP连接与关闭

12.6.1 三次握手

TCP 是一个面向连接的协议,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接,俗称“三次握手”。

主要分为三个步骤:

  1. 第一步 :客户端向服务端发送一个 SYN报文段 (只有首部,且SYN被置 1),初始序号(ISN),假设为 A,ACK 置 0。( 客户端进入SYN_SEND状态 )

  2. 第二步 :服务器端收到 SYN报文段 ,便知道客户端需要请求握手,从 SYN报文段 中提取对应的信息,为该 TCP 连接分配 TCP 缓存和变量,并向该客户 TCP 发送允许连接的报文段(握手应答报文)。这个报文段只有首部,包含3个重要的信息:( 建立客户端-->服务端的连接 )( 服务器进入SYN_RECV状态 )

    1. SYN与ACK标志位1
    2. 将TCP报文段首部的确认序号字段设置为 A+1 (这个A(ISN)是从握手请求报文中得到)。
    3. 服务器随机选择自己的初始序号(ISN,注意此ISN是服务器端的ISN,假设为B),并将其放置到TCP报文段首部的序号字段中。
  3. 第三步 :客户端接收到服务器端的握手应答后,会将 SYN 置 0,ACK 置 1,确认序号置为 B+1 , 设置窗口值,可以添加数据域的报文段发给服务器端。同时给该TCP连接分配缓存和变量。( 建立服务端-->客户端的连接 )( 客户端和服务器端都进入ESTABLISHED状态 )

ISN:初始序列号(Inital Sequence Number)。

三次握手的原因:

  1. 避免重复连接(主要原因):参考RFC 793,主要原因是为了防止旧的重复连接引起连接混乱问题。假设两次握手,如果由于网络原因出现SYN重传,第一个SYN请求到达后建立连接,数据交互完毕后关闭连接,如果此时服务器收到重传过来的SYN请求,就直接建立连接,这样会导致虚假连接。
  2. 同步双方初始序列号:TCP 协议的通信双方, 都必须维护一个序列号。SYN请求连接的报文,需要服务端回一个ACK应答报文,表示客户端的SYN报文已被服务端成功接收,那当服务端发送SEQ给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
  3. 避免资源浪费:只需要三次握手即可。

12.6.2 四次挥手

等待2MSL的原因

  1. 保证 TCP 协议的全双工连接能够可靠关闭。

    • 如果远端没有收到 ACK ,触发超时重发 FIN 报文段,主动端依然能处理重发 ACK。如果主动端直接 CLOSE 状态,就不能保证远端收到 ACK。
  2. 保证这次连接的重复数据段从网络中消失。

    • 保证下次连接收到的数据报文段都是来自新连接的目标端。
  3. 2MSL的原因:ACK到达对端最长时间是MSL,如果在ACK到达对端前,多对发送重传FIN,FIN过来最长时间是也MSL,所以共2MSL。如果等待2MSL都没有收到FIN,就可以认为对端已经收到我们的ACK。

SO_REUSEADDR选项的配置,能直接复用处于TIME_WAIT状态的端口。

四次挥手的原因:而在释放连接时需要四次是因为TCP连接的半关闭造成的。由于TCP是全双工的(即数据可在两个方向上同时传递),所以,每个方向都必须要单独进行关闭,单方向的关闭就叫半关闭。

12.6.3 同时打开

同时打开需要交换4个报文段,比正常的三次握手多一个。没有任何一端称为客户或服务器,因为每一端既是客户又是服务器。

12.6.4 同时关闭

在标准的情况下通过一方发送FIN来关闭连接,但是双方都执行主动关闭也是有可能的。

12.6.5 半关闭

半关闭:TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。

如果应用程序不调用close()而调用shutdown(),且第2个参数值为1,则插口的API支持半关闭,举个例子:

12.7 窗口

TCP数据中每一个字节都有自己的编号SEQ,而窗口就是能接收或发送的SEQ段。

12.7.1 窗口大小通告

就是TCP报文段首部的窗口字段。

用来告知发送端自己所能接收的数据量,从而达到一部分流控的目的。

TCP的选项字段中还包含了一个TCP窗口扩大因子,option-kind为3,option-length为3个字节,option-data取值范围0-14。

窗口扩大因子用来扩大TCP窗口,可把原来16bit的窗口,扩大为31bit。具体在TCP选项字段分析。

12.7.2 拥塞窗口

塞窗口是发送方使用的流量控制,而通告窗口则是接收方使用的流量控制。

拥塞窗口和发送窗口去MIN值,就是实际能发送的窗口大小。

12.7.3 发送窗口

参考LWIP:TCP 控制块中关于发送窗口的成员变量有 lastack、snd_nxt、snd_lbb、snd_wnd:

  • lastack 记录了已经确认的最大序号;
  • snd_nxt 表示下次要发送的序号;
  • snd_lbb 是表示下一个将被应用线程缓冲的序号;
  • snd_wnd 表示发送窗口的大小。

通过上面的几个参数就知道发送窗口的信息,如下图:

  • 发送窗口是TCP层当前可以发送的SEQ号段。
  • 窗口snd_wnd如果填满了数据,则不能在发送了,等发送窗口往右移动时,才能发送新数据。
  • 而窗口内的数据可以有已经被ACK的,只是最小未被ACK的SEQ号就是窗口最左。

12.7.4 接收窗口

参考LWIP:TCP 控制块中关于接收窗口的成员变量有 rcv_nxt、rcv_wnd、rcv_ann_wnd、rcv_ann_right_edge:

  • rcv_nxt:下次期望接收到的数据SEQ;
  • rcv_wnd:接收窗口的大小;
  • rcv_ann_wnd:窗口大小通告值,即是告诉发送方窗口的大小;
  • rcv_ann_right_edge:记录了窗口的右边界。

通过上面的几个参数就知道发送窗口的信息,如下图:

  • 接收窗口是TCP层当前能够接收的数据段,用于流量控制。一般用接收窗口右边界表示当前能接收到的最大SEQ号-1的数据。
  • 在收到对端数据后,接收窗口会减少;在应用层读走数据后,接收窗口会增加。但是并不是每次增减都会通告到对端,因为这样会出现糊涂窗口综合症。

12.7.5 糊涂窗口综合症

12.7.5.1 概念

TCP协议栈基于滑动窗口动态调整机制进行流量控制会导致一种被称为“糊涂窗口综合症SWS (Silly WindowSyndrome)"的状况。

糊涂窗口综合症SWS:当TCP接收方通告了一个小窗口,并且TCP发送方立即发送数据填充该小窗口时,就会产生糊涂窗口,有效载荷比例降低。

当TCP的双方都是以小窗口通告和小报文段发送来实现通信,会使TCP数据流包含很多非常小的报文段,而不是满长度的报文段;而小单元报文段中IP首部和TCP首部这些字段占了大部分空间,会导致真正有效的TCP数据却很少,因此小报文的传输浪费了网络的大量带宽,从而网络性能严重下降。

12.7.5.2 原因

糊涂窗口综合症可以由TCP连接双方中的任何一方引起:

  • 接收方:接收方通告一个小的窗口(而不是一直等到有大的窗口时才通告)。
  • 发送方:发送方发送少量的数据(而不是等待更多的数据以便发送一个大的报文段)。

12.7.5.3 解决

解决措施:

  • 接收方不通告小窗口。通常的算法是在窗口增大一个MSS或者增大到缓冲区一半之前,接收方不通告一个比当前窗口大的窗口(宁可为0)。

  • 发送方避免出现糊涂窗口综合症的措施是只有以下条件之一满足时才发送数据:

    1. 需要发送的数据是一个满长度(MSS)的报文段,可发送。
    2. 需要发送的数据长度大于等于接收通告窗口值的一半时,可发送。
    3. 没有飞行数据时(即是没有已发送但是未收到ACK的数据时),可发送。
    4. 禁用了Nagle算法时,可发送。

12.8 拥塞控制&一些可靠算法

拥塞:当数据从一个大的管道(如一个快速局域网)向一个较小的管道(如一个较慢的广域网)发送时便会发生拥塞。发送拥塞后,发送端并不知道哪里发送拥塞而被丢弃数据,只能等待超时重传,这个将会非常耗时,而且会重新进入慢启动和降低上门限值ssthresh。所以需要尽量避免超时重传。

拥塞控制:避免出现拥塞。

拥塞控制算法包括慢启动、拥塞避免、拥塞发生(超时重传、快重传)、快恢复。

这几种算法下面会介绍到。

12.8.1 RTT和RTO计算

RTT(Round Trip Time):一个连接的往返时间,即数据发送时刻到接收到确认的时刻的差值;
RTO(Retransmission Time Out):重传超时时间,即从数据发送时刻算起,超过这个时间便执行重传。
RTT和RTO 的关系是:由于网络波动的不确定性,每个RTT都是动态变化的,所以RTO也应随着RTT动态变化。

12.8.1.1 RTT测量

两种方法:

  1. 方法一:TCP Timestamp选项:TCP时间戳选项可以用来精确的测量RTT:RTT = 当前时间 - 数据包中Timestamp选项的回显时间。这个回显时间是该数据包发出去的时间,知道了数据包的接收时间(当前时间)和发送时间(回显时间),就可以轻松的得到RTT的一个测量值。
  2. 方法二:选择一个指定SEQ的数据包,在发出时记录系统当前时间,在收到该SEQ的ACK后,用当前时间 减 发出时的时间就是本次RTT。

12.8.1.2 RTT估计器

每次测量RTT都会有差异,所以我们需要平滑下。

假设每次实测的RTT值为SampleRTT。

估计RTT值为EstimatedRTT。

TCP会通过多次SampleRTT来维护EstimatedRTT。

算法:EstimatedRTT = (1-a)* EstimatedRTT + a * SampleRTT

  • 其中a通常取值为0.125

12.8.1.3 RTT方差

在最初的RTO算法中,RTO等于一个值为2的时延离散因子与RTT估计值的乘积,即:

  • RTO = 2 * EstimatedRTT

但这种做法有个很大的缺陷,就是在RTT变化范围很大的时候,使用这个方法无法跟上这种变化,从而引起不必要的重传。

由于新测量SampleRTT的权值只占EstimatedRTT的12.5%(通常情况下),当实际RTT变化很大的时候,即便测量到的SampleRTT变化也很大,但是所占比重小,最后EstimatedRTT的变化也不大,从而RTO的变化不大,造成RTO过小,容易引起不必要的重传。因此对RTT的方差跟踪则显得很有必要。

在TCP规范中定义了RTT偏差DevRTT,用于估算SampleRTT一般会偏离EstimatedRTT的程度:

  • DevRTT = (1-B) * DevRTT + B * |SampleRTT - EstimatedRTT|

    • 其中B的推荐值为0.25,当RTT波动很大的时候,DevRTT的就会很大。

12.8.1.4 RTO值计算

超时重传时间间隔RTO的计算公式为:RTO = EstimatedRTT + 4 * DevRTT

在[RFC 6298]中,推荐初始超时重传时间为1秒,当出现超时后,超时重传时间将以指数退避的方法加倍,以免即将被确认的后继报文段过早出现超时。

12.8.2 慢启动

在设备启动往网络上发送数据时,并不知道网络状况,所以进行试探,发少量数据在逐步增加数据量。

慢启动为发送方的 TCP增加了另一个窗口:拥塞窗口 (congestion window),记为cwnd。

拥塞窗口初始化为1个报文段(1个MSS)。每收到一个报文段的ACK,拥塞窗口就增加大一个报文段。收到两个,就增大两个。

12.8.3 拥塞避免

当拥塞窗口增大到慢开始上门限值ssthresh时,就开始拥塞避免算法。每次只增加一个报文段。

12.8.4 拥塞发生

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

  1. 超时重传。
  2. 快速重传。

12.8.4.1 超时重传

当发送了超时重传,就可以认为是发送了拥塞,会执行拥塞发生算法

  • 拥塞窗口cwnd重置为1。
  • 慢开始上门限值ssthresh减半。

12.8.4.2 快速重传

超时重传的拥塞发生算法过于激进的做法,这适合与真的发生拥塞时,但是有时候网络正常丢包,这不不是超时引起的重传,而是网络丢包引起的重传。

当收到对端连续三次ACK同一个SEQ时,我们就能判断为发送了网络丢包,这时就不用等待超时,不用执行超时重传的拥塞算法了,而是执行快速重传的拥塞发生算法:

  • 拥塞窗口cwnd设为原来的一半:cwnd /= 2
  • 慢开始上门限值ssthresh = cwnd;(cwnd为减半后的拥塞窗口)
  • 进入快恢复算法。

12.8.5 快恢复

快速重传和快速恢复算法一般同时使用。

因为快恢复算法认为,能收到三个ACK,说明网络还不是很差,没必要像RTO一样搞得那么僵。

快恢复算法:

  • 收到第3个重复ACK时,先执行快速重传算法,然后拥塞窗口cwnd = ssthresh + 3(3:每收到1个ACK,可以认为对端收到1次TCP包,网络上就少了1个TCP包,一个包最大为1个报文段,所以快恢复的拥塞窗口就追加3个报文段)。
  • 收到超过3个重复的ACK时,每次都会增大拥塞窗口:cwnd += 1
  • 当收到新的ACK后:cwnd = ssthresh。然后进入拥塞避免算法。

12.8.6 Nagle算法

如果一个TCP报文每次只发送1个字节,这就产生了一些41字节长的分组:20字节的IP首部、20字节的TCP首部和1个字节的数据,利用率太低。(IPV4)

解决方法就是RFC 896 [Nagle1984]中所建议的Nagle算法。

nagle算法是提高传输效率,降低拥塞出现的可能。

nagle算法: 尽可能组合更多数据合到同一个报文段中。满足以下条件之一,nagle算法都不会生效:

  • 用户设置了TF_NODELAY标志。(该标志表示关闭nagle算法)
  • 没有飞行中的数据,可以立即发送。
  • 报文段中还有FIN标记,可立即发送。
  • 未发送的报文段长度大于或大于一个MSS,也满足立即发送条件。
  • 发生超时,立即发送。

12.8.7 延迟确认

如果接收到一个TCP包,立即响应ACK,如果在很短时间内,又收到一个TCP包,又立即响应一个ACK,这种情况下,还不如合起来响应一个ACK。

所以就有了延迟确认这个算法。

如果开启了延迟确认,在接收到TCP包后,等待一小段时间(Linux 上默认是 40ms、LWIP默认250ms),如果在这段时间内收到新的TCP包,则只需要在时间到达后确认一次即可。

当然,遇到特殊情况可以不用等待时间到达,可以立即响应ACK:

  1. 收到乱序包。
  2. 收到窗口外的TCP包。
  3. 需要调整窗口。
  4. 收到RST。
  5. 发送的报文段中含有FIN。

12.9 四个定时器

12.9.1 重传定时器

重传定时器是用来计算TCP报文段的超时重传时间的,每发送一个报文段就会启动重传定时器,如果在定时器时间到后还没收到对该报文段的确认,就重传该报文段,并将重传定时器复位,重新计算;如果在规定时间内收到了对该报文段的确认,则撤销该报文段的重传定时器。

当然,在一些轻量的TCPIP协议栈中,并不会为每个报文都使用独立的超时计时,如LWIP,每个TCP控制块只有一个超时计时值,每收到一个新的ACK都会被清零重新计时,在RTO后都还没收到ACK,便会把UNACK队列中的所有数据都会回迁至UNSENT队列。

12.9.2 坚持定时器

坚持(persist)定时器使窗口大小信息保持不断流动,即使另一端关闭了其接收窗口。

先来了解下窗口探查。

窗口探查(window probe):

当接收方TCP缓冲区没有剩余空间后,在ACK中会通知发送方window=0,此时发送方就暂停发送数据。

当接收方TCP缓冲区又有空间后,会再次发送一个ACK,告知其剩余缓冲区大小,可以接受新的数据包了,这个ACK叫做窗口更新。

TCP接收方则等待新的数据包过来。但是如果这个窗口更新的ACK丢失了,那么会出现两端互相等待:接收方等待新的数据包,因为他已经通知对端新的window大小了,而发送方则还在等待窗口更新,因为它还认为对端窗口为0。

坚持定时器(Persist Timer)就是为了解决这个问题而设计的。

发送方使用一个坚持定时器来周期性的向接收方查询,以便发现窗口已经增大。这些从发送方发出的查询报文段被称为窗口探查(window probe)

窗口探查包含一个字节,TCP总是允许发送已关闭窗口之后一个字节的数据。

发送方在收到window=0的通知后就开启这个定时器,如果这个定时器的时间到还没收到接收方的窗口更新,那么它就探查这个空的窗口以决定窗口是否丢失。

窗口探查的时间间隔可以逐步增大,一定次数后,可以认为对端已经关闭连接。

12.9.3 保活定时器

保活(keepalive)定时器可检测到一个空闲连接的另一端何时崩溃或重启,而不是一直永久地等下去。

初始超时通常为2小时,如果2小时没有收到客户端的数据,服务端就发送一个探测报文,以后每隔75秒发送一次,如果连续发送10次探测报文段后仍没有收到客户端的响应,服务器就认为客户端出现了故障,就可以终止这个连接。(具体查看各个TCPIP协议栈的具体实现)

TCP保活探测报文:是将之前TCP报文的序列号减1,并设置1个字节,内容为“00”的应用层数据。

注意:保活机制并不是 TCP 规范中的一部分,对此主机需求RFC 1122给出了三个理由:

  1. 在出现短暂的网络错误的时候,保活机制会使一个好的连接断开 (接收保活报文的一端可能只是因为一时的故障没有响应,故障可能很快会恢复,但另一端并不知情,他只知道自己发送的保活报文没有收到回应,那么就错误地认为对方不在工作了,于是断开连接);
  2. 保活机制会占用不必要的带宽 (因为不影响数据流,需要额外的报文的开销);
  3. 在按流量计费的情况下会在互联网上花掉更多的钱 (同样因为使用额外的报文)。

12.9.4 2MSL定时器

2MSL定时器的设置主要是为了确保发送的最后一个ACK报文段能够到达对方,并防止之前与本连接有关的由于延迟等原因而导致已失效的报文被误判为有效。

12.10 常用选项字段分析

12.10.1 MSS

MSS:Maximum Segment Size (MSS) Option

参考:RFC 1122, chap 4.2.2.6

一般情况下,通信双方在建立连接时,SYN Segment中会携带MSS Option,MSS指明本端可以接受的最大长度的TCP Segment(Payload,不含TCP Header),也就是说,对端发送数据的长度不应该大于MSS(单位Byte)。

  1. 首先要明确一点,MSS并非和对端协商的值,而是对对端发送数据长度的“限制”,表明在整个TCP连接期间,都不会接收长度大于MSS的TCP Segment。
  2. 如果收到的SYN中没有MSS,将使用默认值536。MSS Option的Value字段长度固定为16bit,所以MSS最大值为65535(单位Byte)。因此,网络中所有设备都被要求,必须能够处理大小小于576Byte的数据包(IP Header + TCP Header + Default MSS 最小值为 576 Byte)
  3. IPv4网络中,MSS的典型取值为1460 ,1460Byte + 20Byte IP Header + 20Byte TCP Header = 1500Byte = 以太网典型MTU;
  4. IPv6网络中,典型MSS取值为1440;另外,如果MSS=65535,表示MSS = PMTU - 60

12.10.2 SACK

SACK:Selective Acknowledgment (SACK) Options

在标准的TCP实现中,使用的是累加式的ACK,例如“ACK Num = n”代表对序列号n以前的Bytes进行确认。但是,显然,这样将无法对不连续的Segment进行确认。此外,当出现不连续Segment时,还会导致TCP的接收队列出现一个“坑”,不将这个坑填上,坑后的数据就无法交付给应用程序。

为解决上述问题,TCP定义了SACK Option,可以使TCP接收者将这个“坑”的位置通告给发送者,让其对这一段数据进行重传。

注意:若要使用SACK特性,必须在建立连接时,在SYN Segment中附加上SACK-Permitted Option,以此告知对方自己支持SACK。

SACK-Permitted Option格式如下所示:

Kind = 4 Length = 2

SACK Option格式如下所示:

  • SACK Option通过“Left Edge ~ Right Edge”,指定了一个或多个范围的Seq Num,称为SACK Block,指明了处于“坑”后面(或坑之间)的、已成功接收的Bytes。
  • 由于TCP Header最长为60 Byte,因此SACK Option中最多只能包含4个SACK Block。
Kind
(5)
Length
(可变)
Left Edge of 1st B1ock
(32bit)
ight Edge of 1st B1ock
(32bit)
... ... Left Edge of nst B1ock
(n≤4)
ight Edge of nst Block
(n≤4)

例子:

  1. 终端A收到了TCP数据流中的Seq Num为0 至 1452、2905 至 4096的字节,但缺少了1453 至 2904;
  2. 终端A向B发送ACK Segment,其中ACK Num=1453、SACK Option=2905 至 4097,表明它已经收到了数据流中的Seq Num为2905 至 4096的字节,但没有还没收到1453 至 2904;
  3. 终端B收到这个SACK后,重传包含Byte 1453 至 2904的TCP Segment;
  4. 终端A向B发送ACK Segment,其中ACK Num=4097,表明它已经收到Seq Num 4097之前的所有字节;
  5. 之后,数据通信恢复正常。

lwip:

  • 从lwip的tcp_enqueue_flags()函数看,如果对端不支持SACK,本地也不会支持SACK。

12.10.3 WSOPT

WSOPT:Window Scale (WSCALE or WSopt) Option

TCP Header的Window Size字段长度为16bit,因而正常情况下,Window Advertisement最大只能是65535 Bytes。

Window Scale Option用于将TCP Header的Window Size字段从16bit扩展至最多30bit,格式如下所示:

kind Length shift.cnt
(3) (3) (范围0~14)
  1. shift.cnt的取值范围为0~14,表示将Window Advertisement的值扩展至“WindowSize × 2^shift.cnt,这就是最终的窗口值。

    1. 取值范围[0, 14]的原因:最大TCP序号限定为2^16 * 2^14 = 2^30 < 2^31。该限制用于防止字节序列号溢出。
  2. WSopt只能出现在SYN Segment或SYN+ACK Segment中,因此shift.cnt在三次握手之后就会固定下来。

  3. 另外,WSopt是双向独立的,因此连接的两个方向可以有不同的Shift.cnt。但是,WSopt必须双向同时启用,也就是说,如果SYN中不带有WSopt,SYN+ACK中也不能出现WSopt;同样,如果SYN+ACK中不带有WSopt,那么发起SYN的一端就会当作自己也不曾发送过WSopt。

  4. shift.cnt根据接收Buffer的大小,由TCP自动选取。接收Buffer由系统或程序设定。

12.10.4 TSOPT

TSOPT:Timestamps Option and PAWS

  • Timestamps:时间戳。

  • PAWS:Protect Against Wrapped Sequence Numbers:防止序列号回绕。

    • 回绕:就是序列号溢出,重新从起点计算。

主要两个功能:计算RTT和防止序列号回绕。

启用Timestamp Option后,每个TCP Segment中都会带有Timestamp Option,其中包含了两个32bit的Timestamp(TSval和TSecr)。

具体格式如下所示:

Kind
(8)
Length
(10)
imestamp value
(TSval)
Timestamp Echo Reply
(TSecr)
  1. TSval指明了发送端在发送TCP Segment时的Timestamp;接收端在对该TCP Segment做ACK时,将TSval值回显在TSecr字段中。

    1. 注意:由于TCP连接是双向的,接收端在ACK中回显TSecr时,也会把自己当前的Timestamp放入TSval字段。
  2. Timestamp是一个随时间单调递增的值,由于TCP接收端只需要在ACK中将TSval简单地回显,因此通信双方并不需要进行时间同步等操作。

  3. 通过Timestamp Option,发送端再也不需要在内存中保存发送Segment的时间了,只需要将其放入TSval,然后接收端将其回显在ACK Segment即可。当发送端收到ACK Segment后,取出TSscr,和当前时间做算术差,即可完成一次RTT的测量。

  4. 若非通过Timestamp Option来计算RTT,大部分TCP实现只会以“每个Window采样一次”的频率来测算RTT。因此通过Timestamp Option,可以实现更密集的RTT采样,使RTT的测算更精确。

Timestamp Option还能防止序列号回绕(PAWS)。

序列号回绕冲突只会出现在高速连接上。

序列号回绕冲突是指序列号seq[0,2^32],即是最大4G。如果在高速的连接中,某段数据A因为路由问题出现重传(此时网络是可能出现2个以上时间段A),收到一个时间段A后继续接收。seq溢出,轮回第二次序列号seq[0,2^32],如果此时上一轮回重传的数据段A也到达了,那怎么判断当前序列号seq是本次轮回的还是上次轮回的?(当然,一次seq的轮回需要在MSL内,否则这个报文段在它的TTL到期时会被某个路由器丢弃)

Timestamp Option能解决这个冲突。

参考以下例子来理解:

  1. 假设TCP Window Size为1GB(使用Window Scale),发送者每发送一个Window的数据Timestamp值加100,数据的发送情况如下所示:
时间点 发送数据量 Seq Num Timestamp 接收
1 0G:1G OG:1G 0~100 OK
2 1G:2G 1G:2G 100~200 其中某些segment丢包后重传
(重传后,网络上可能会出现多个这个数据段的包;
也就是说可能会因为网络延迟原因,接收端会收到多个这个时间段的包。)
3 2G:3G 2G:3G 200~300 OK
4 3G:4G 3G: 4G 300~400 OK
5 4G:5G 0G:1G 400~500 OK
6 5G:6G 1G:2G 100~200、500~600 接收500~600的包。
丢弃时间戳为100~200的包,因为从400开始序列号seq就已经开始回绕了。

在时间点2的时候,发生了丢包,然后重传。

在时间点5,序列号开始回绕。

在时间点6,已经被认为“丢包”的Segment延迟到达了。

那怎么判断这个序列号seq为[1G:2G]的报文是上一轮回的还是现在需要接收的?

通过Timestamp Option字段的时间戳去区别。由于最近生效的时间戳都超500了。所以比这个时间戳前的字段都视为过期字段,PAWS机制将其丢弃。

  • 注意理解这句话。窗口1GB,说明没有收到确认的数据后面最多能发送不超过1GB的数据。看上表,时间点5都已经接收完毕了,说明时间点5之前的数据都已经全部接收完了。后面重传只会出现在[5G:6G]中间。如果这里收到的数据的时间戳比时间点5的时间戳500还要少,说明是过期重传的数据,我们不需要。
  • 最近有效的时间戳:参考下图理解。所以只要收到的数据的时间戳少于有效时间戳,就视为过期数据。

12.10.5 UTO

UTO:User Timeout (UTO) Option

UserTimeout值表明了TCP发送者等待ACK的时间,如果在指定时间内没收到ACK,就会认为对端挂掉。

对于传统TCP(RFC 793)而言,UserTimeout是本地配置的。

RFC 1122建议,当TCP重传3次后,应该通知应用程序,100s后,应该删除连接。

通过UTO,可以让TCP将UserTimeout值“告知”给对端,UTO格式如下所示:

Kind
(28)
Length
(4)
G bit
(Granularity bit)
UserTimeout
  1. G bit = 1,表示UserTimeout的单位为分钟;G bit = 0,表示UserTimeout的单位为秒。

  2. 通过UTO,TCP接收者可以根据“对端的UserTimeout”来调整自己的行为。UserTimeout建议取值为:min(U_Limit,max(Adv_UTO,Remot_UTO,L_Limit))。

    1. U_Limit是本地UserTimeout的最高限制;
    2. Adv_UTO是通告出去的UserTimeout;
    3. Remot_UTO是对端的UserTimeout;
    4. L_Limit是本地UserTimeout的最低限制。
  3. 要注意的是,UTO只是用于“告知”,TCP接收者却不一定要根据对端的UTO值来调整自己的行为。

  4. 此外,NAT设备也可以根据UTO来调整连接保活计时器。

  5. 若使用 = min(U_LIMIT, max(ADV_UTO, REMOTE_UTO, L_LIMIT))。

12.10.6 TCP-AO

TCP-AO:Authentication Option (TCP-AO)and TCP MD5 Signature Option(TCP-MD5)

TCP-MD5和TCP-AO主要用于防止TCP欺骗攻击(TCP Spoofing Attacks)。

TCP-MD5是旧标准(RFC 2385),例如BGP、LDP等协议就是以TCP-MD5作为认证手段的。2010年后,IETF建议使用TCP-AO去取代TCP-MD5,然而TCP-AO当前的普及率还很低。

TCP-MD5和TCP-AO的格式如下:

TCP-MD5 Option的MD5 Hash根据以下信息计算:

  1. TCP伪头部。
  2. TCP头部(包括Option,checksum设为0)。
  3. TCP Segment Data。
  4. 密钥。

相对于TCP-MD5,TCP-AO的主要改进之处在于:

  1. 支持多种MAC算法
  2. 支持带内的密钥变更操作

注意:TCP-AO与TCP-MD5一样,都不包含密钥分发机制。因此在密钥分发方面都存在一定风险。

posted @ 2022-11-12 13:43  李柱明  阅读(3127)  评论(0编辑  收藏  举报