TCP数据传输

1 概述

主要内容

tcp数据传输主要包括以下几个方面

  • 可靠数据传输(保证数据传输的正确性)
    • 面向连接
    • 序号机制(保证数据顺序)
    • 校验和(防止数据出错)
    • 确认机制(应对丢包)
    • 超时重传(应对丢包、丢失确认)
    • 累计确认(发送方动作,发送方认为之前全被正确接收)
    • 选择重传(接受方动作,接收方维持滑动串口,只要求重传丢失的分组)
  • 流量控制(提高传输效率的流水线方法,考虑接收端的接受能力,发送方的发送速率不应该超过接收端的接受能力)
    • 滑动窗口(对应缓冲区大小,双向缓冲窗口流水线技术)
  • 拥塞控制(通过流量控制实现,考虑断点之间的网络情况,目的使负载不超过网络的传输能力。)
    • 慢启动
    • 拥塞避免
    • 快重传
    • 快恢复
  • 在接收方实现了流量控制
  • 在发送方实现了拥塞控制

TCP主要任务

当TCP连接建立之后,应用程序即可使用该连接进行数据收发。应用程序将数据提交给TCP,TCP将数据放入自己的缓存,并且在其认为合适的时候将数据发送出去。在TCP中,数据会被当做字节流并按照MSS的大小进行分段,然后加上TCP头部并提交给网络层。之后数据就会被网络层提交给目地主机,目地主机的IP层会将分组提交给TCP,TCP根据报文段的头部信息找到相应的socket,并将报文段提交给该socket,socket是和应用关联的,也就提交给了应用。

相关概念

  • 字节流:编了序号的字节串

2 TCP可靠传输

这一部分主要介绍了TCP实现可靠数据传输的基本原理。包括校验和、序号机制、确认机制、超时重传、流水线技术(滑动窗口)、累计确认、选择重传等。

可靠保证

TCP提供了可靠的传输服务,这是通过下列方式提供的:

  • 校验和TCP将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段(希望发端超时并重发)。
  • 序号机制由于IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。如果必要,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层。IP数据报会发生重复,TCP的接收端必须丢弃重复的数据。
  • 确认机制和超时重传:应用数据被分割成TCP认为最适合发送的数据块。由TCP传递给IP的信息单位称为报文段或段(segment)当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。

GBN协议与选择重传结合——流水线技术

  • 流水线技术IP提供的服务是尽力交付的服务,也是不可靠的服务。但是TCP在IP之上提供了可靠度传输服务。TCP采用了流水线下的可靠数据传输协议,但是在差错恢复时,并没有简单的采取GBN协议或者选择重传协议,而是将二者结合了起来。

    TCP采用了累积确认-选择重传的方式

  • 累计确认:这类似于GBN,即如果TCP发送了对某个序号N的确认,则表明在N之前的所有字节流都已经被正确接收。但是另一方面,TCP又不会像GBN协议那样简单丢弃失序到达的报文段,而是会将它们缓存起来,但是这些被缓存的报文段不会逐个被确认。

  • 选择重传TCP还允许接收方选择性的确认失序到达的分组,当发生超时时,TCP只会重传发生超时的那一个报文段。而不是所有的没有确认的报文段。

基本工作过程

  1. 发送分组

    TCP会为发送的每一个分组分配一个唯一的序号,该序号和ISN以及该报文段在字节流中的位置有关。序号被填入TCP头部的序号字段。如果重传定时器还没有运行,则会启动重传定时器。

  2. 接收到ACK

    由于是累积确认的,因此如果收到的ACK是合法的,即是对已发送但未被确认的报文段的确认,则更新send_base,并且如果还有未被确认的已发送的报文段,则重启重传定时器。

  3. 超时

    重传引起超时的报文段,并重启定时器。TCP的重传不一定是重传引起超时的报文段本身,TCP可能重新进行分组然后重传,唯一被保证的是所有数据都会被传输。

  4. 产生ACK

    每个TCP报文段的TCP头部都固定包含了ACK域,如果在传输中,为了确认一个报文段而单独发送一个ACK,则该ACK就是一个数据部分长度为0的特殊TCP报文段,如果这样的分段太多,网络的利用率就会下降。为此,TCP采取了延迟确认的机制。其工作过程:

    如果收到的报文段的序号等于rcv_base,并且所有在rvc_base之前的报文段的确认都已经被发送,则只更新rcv_base,但是延迟该报文段的ACK的发送,最多延迟500ms。延迟的ACK可能会在接收端有数据要发送给发送端时被发送或者在接收端有多个ACK需要被发送给发送端时被发送。

    如果收到的报文段的序号等于rcv_base,并且有延迟的ACK待发送,则更新rcv_base,并发送累积的ACK以确认这两个按序报文段

    如果收到的报文段的序号大于rcv_base,则发送冗余的ACK,即重传对已经确认过的最后一个按序到达的报文段的ACK

3 超时重传机制

发送端超时重传机制

选择重传中每个报文段都有自己的超时值,TCP采用了RFC2988建议的机制用一个单一定时器来完成该功能。RFC2988定义的原则:

  • 发送TCP分段时,如果还没有重传定时器开启,那么开启它。
  • 发送TCP分段时,如果已经有重传定时器开启,不再开启它。
  • 收到一个非冗余ACK时,如果有数据在传输中,重新开启重传定时器。
  • 收到一个非冗余ACK时,如果没有数据在传输中,则关闭重传定时器。

往返时延的估算与超时

TCP协议定义了RTT来代表一个TCP分段的往返时间。然而由于IP网络是尽力而为的,并且路由是动态的,且路由器可能缓存或者丢弃IP数据报,因此一个TCP连接的RTT是动态变化的,因而也需要动态测量。样本RTT(SampleRTT)是报文段被发出到报文段的确认被收到的时间间隔。TCP不会为每一个发动的报文段测量一个SampleRTT,而是仅为已发送但是未被确认的分组测量SampleRTT。这样做是为了产生一个近似于RTT的SampleRTT。TCP不会为重传的报文段测量SampleRTT。

得到多个SampleRTT后,TCP会尝试使用这些信息来尽可能得到一个较为准确的RTT,为此TCP采用了经常被采用的收到即使用一个滤波器来对多个SampleRTT进行计算。TCP使用如下的滤波器来计算一个EstimateRTT:

$$
EstimateRTT= (1- α) * EstimateRTT +α * SampleRTT
$$

RFC2988给出的α参考值为1/8。EstimateRTT 是一个平滑后的RTT。

除此之外,TCP还将RTT的变化率也应该考虑在内,如果变化率过大,则通过以变化率为自变量的函数为主计算RTT(如果陡然增大,则取值为比较大的正数,如果陡然减小,则取值为比较小的负数,然后和平均值加权求和),反之如果变化率很小,则取测量平均值。TCP计算了一个DevRTT。它用于估量SampleRTT偏离EstimateRTT的程度。其公式为:
$$
DevRTT= (1-β)* DevRTT + β* |SampleRTT - EstimateRTT |
$$
β的参考值为1/4。
之后重传定时器的值会被设置为EstimateRTT + 4 * DevRTT

倍数增加的重传间隔

在发生超时重传时,TCP不是以固定的时间间隔来重传的,而是每次重传时都将下一次重传的间隔设置为上次重传间隔的2倍,因此重传间隔是倍数增加的。直到收到确认或者彻底失败。由于正常发送报文段时,重传定时器的超时值为EstimateRTT + 4 * DevRTT,因此第一重传时会将下一次的超时时间设置为2倍的该值,依次类推。

快速重传

倍数增加的重传间隔会增大端到端的时延,使得发送端可能不得不等待很长时间才能重传报文段。冗余ACK使得TCP可以得到分组丢失的线索。TCP基于冗余ACK提供了一种快速重传机制。其原理是:如果收到了对相同数据的三个冗余的ACK,发送端就认为跟在这个被确认了三次的报文段之后的报文段丢失了,因此重传它,而不是等待它的超时定时器到期。这就是快速重传。

4 流量控制

流量控制的原因

在TCP中,连接双都为该连接设置了接收缓存,当报文段被连接的一端接收时,它会进入该接收缓存,被接收的数据并不一定立即被提交给应用程序。因为应用可能由于各种而没能及时读取缓存中的数据。如果发送方发送的数据太快,而应用没有及时读取被缓存的数据,缓存就会变满,此时为了防止缓存溢出,就要丢弃报文段,显然丢弃已经正确接收的报文段是对网络资源的浪费。为了解决该问题,TCP需要提供一种机制来防止接收缓存溢出。

流量控制的机制

TCP提供了流量控制功能,来防止发送方发送过快而导致接收方缓存溢出的情形出现。TCP采用大小可变的滑动窗口进行流量控制:

  • 接受端根据自己的接受缓存大小,随时动态调整发送端的发送窗口的上限值。接收端窗口rwnd,被放在TCP报文段首部的窗口字段当中。

  • 发送端根据其对网络拥塞程度的估计确定窗口值,叫做拥塞窗口cwnd。
    $$
    发送窗口的上限值=Min[cwnd,rwnd]
    $$

  • 发送端的窗口大小通告的可视化的描述如下图:

  • 但TCP连接的一端可能通告一个大小为0的窗口,这时候接收到对端通告大小为0的窗口的一端并不会停止发送,而是会启动一个定时器来发送窗口探测报文段,该报文段只包含一个字节,该报文段会被接收方确认,该定时器会一直重启自身来发送窗口探测包直到对端通告了一个大小不为0的窗口为止。定时器的超时值会逐渐增大到一个最大值,然后固定以该值重发窗口探测包。

窗口边沿移动规律

  • 窗口合拢:
    当窗口的左边沿向右移动时,称为窗口合拢。此时,数据被发送并被确认。
  • 零窗口:
    当左边沿与右边沿重合时,称为零窗口,发送端不发送任何有效数据。
  • 窗口张开:
    当窗口右边沿向右移动时将允许发送更多数据,称为窗口张开。此时接收端已经确认了数据,并释放了TCP的接受缓存。
  • 窗口收缩:
    当右边沿向左移动时,称为窗口收缩,一半不会发生,但应该能够处理这种情况。

窗口大小协商(报文交互)

为了获得最优的连接速率,使用TCP窗口来控制流速率(flow control),滑动窗口就是一种主要的机制。这个窗口允许源端在给定连接传送数据分段而不用等待目标端返回ACK,一句话描述:窗口的大小决定在不需要对端响应(acknowledgement)情况下传送数据的数量。

TCP header中有一个Window Size字段,它其实是指接收端的窗口,即接收窗口,用来告知发送端自己所能接收的数据量,从而达到一部分流控的目的。其实TCP在整个发送过程中,也在度量当前的网络状态,目的是为了维持一个健康稳定的发送过程,比如拥塞控制。因此,数据是在某些机制的控制下进行传输的,就是窗口机制。发送端的发送窗口是基于接收端的接收窗口来计算的。

最早TCP协议涉及用来大范围网络传输时候,其实是没有超过56Kb/s的​连接速度的。因此,TCP包头中只保留了16bit用来标识窗口大小,允许的最大缓存大小不超过64KB。为了打破这一限制,RFC1323规定了TCP窗口尺寸选择,是在TCP连接开始的时候三步握手的时候协商的(SYN, SYN-ACK,ACK),会协商一个 Window size scaling factor,之后交互数据中的是Window size value,所以最终的窗口大小是二者的乘积。

在相互发送的报文中,窗口大小字段,是指各自接受缓冲区窗口的大小,与发送窗口的具体大小没有关系(不需要在报文中。

窗口大小优化设置

TCP在传输数据时和windows size 关系密切,本身窗口用来控制流量,在传输数据时,发送方数据超过接收方就会丢包,流量控制,流量控制要求数据传输双方在每次交互时声明各自的接收窗口「rwnd」大小,用来表示自己最大能保存多少数据,这主要是针对接收方而言的,通俗点儿说就是让发送方知道接收方能吃几碗饭,如果窗口衰减到零,也就是发送方不能再发了,那么就说明吃饱了,必须消化消化,如果硬撑胀漏了,那就是丢包了。

TCP​窗口既然那么重要,那要怎么设置,一个简单的原则是2倍的BDP.这里的BDP的意思是bandwidth-delay product,也就是带宽和时延的乘积,带宽对于网络取最差连接的带宽。为什么是2倍?因为可以这么想,如果滑动窗口是bandwidth*delay,当发送一次数据最后一个字节刚到时,对端要回ACK才能继续发送,就需要等待一次单向时延的时间,所以当是2倍时,刚好就能在等ACK的时间继续发送数据,等收到ACK时数据刚好发送完成,这样就提高了效率。

慢启动过程窗口变化

​虽然流量控制可以避免发送方过载接收方,但是却无法避免过载网络,这是因为接收窗口「rwnd」只反映了服务器个体的情况,却无法反映网络整体的情况。

为了避免网络过载,慢启动引入了拥塞窗口「cwnd」的概念,用来表示发送方在得到接收方确认前,最大允许传输的未经确认的数据。「cwnd」同「rwnd」相比不同的是:它只是发送方的一个内部参数,无需通知给接收方,其初始值往往比较小,然后随着数据包被接收方确认,窗口成倍扩大,有点类似于拳击比赛,开始时不了解敌情,往往是次拳试探,慢慢心里有底了,开始逐渐加大重拳进攻的力度。

拥塞窗口的大小以MSS(最大报文段长度)为单位,是MSS的整数倍。

在慢启动的过程中,随着「cwnd」的增加,可能会出现网络过载,其外在表现就是丢包,一旦出现此类问题,「cwnd」的大小会迅速衰减,以便网络能够缓过来。

拥塞避免过程窗口变化

从慢启动的介绍中,我们能看到,发送方通过对「cwnd」大小的控制,能够避免网络过载,在此过程中,丢包与其说是一个网络问题,倒不如说是一种反馈机制,通过它我们可以感知到发生了网络拥塞,进而调整数据传输策略,实际上,这里还有一个慢启动阈值「ssthresh」的概念,如果「cwnd」小于「ssthresh」,那么表示在慢启动阶段;如果「cwnd」大于「ssthresh」,那么表示在拥塞避免阶段,此时「cwnd」不再像慢启动阶段那样呈指数级整整,而是趋向于线性增长,以期避免网络拥塞,此阶段有多种算法实现,通常保持缺省即可,这里就不一一说明了。

丢包后窗口变化

在慢启动的过程中,随着「cwnd」的增加,可能会出现网络过载,其外在表现就是丢包,一旦出现此类问题,「cwnd」的大小会迅速衰减,以便网络能够缓过来。

5 SWS(糊涂窗口综合症)

糊涂窗口综合症是指在发送端应用进程产生数据很慢、或接收端应用进程处理接收缓冲区数据很慢,或二者都存在时,通过TCP连接传输的报文段会很小,这会导致有效载荷很小。极端情况下,有效载荷可能只有1个字节;而传输开销有40字节(20字节的IP头+20字节的TCP头) 这种现象就叫糊涂窗口综合症。

发送端引起的SWS

如果TCP发送端的应用是产生数据很慢的应用程序(比如telnet),它可能一次只产生一个字节。这种应用程序一次只往TCP提交一个字节的数据,如果没有特殊的处理,这就会导致TCP每次都产生一个只有一个有效载荷的报文段。最终导致网络的有效利用率非常低。解决办法是防止TCP发送过小的报文段,如果应用提交的数据较短,就等待足够的数据来组成一个较大的报文段再发送,为了防止长时间等待导致时延过大,可以加入一个等待时间限制,如果时间到期还没等到足够的数据就直接发送不再等待。

  • Nagle算法就是这样的一种算法。
  • CORK选项是为了提高利用率。

接收端引起的SWS

如果TCP接收端的应用处理数据的速度很慢,一次只从TCP缓存取走很小数量的数据,比如一个字节,而发送方发送的速度较快,这就会导致接收方的缓存被填满,然后接收方每次在应用取走一个字节的数据后都通告一个大小为1的窗口,这就限制发送方每次只能发送包含一个字节的有效载荷的报文段。
对于这种糊涂窗口综合症,即应用程序消耗数据比到达的慢,有两种建议的解决方法:

  • Clark解决方法 Clark解决方法是只要有数据到达就发送确认,但通告的窗口大小为零,这个过程持续到缓存空间已能放入具有最大长度的报文段或者缓存空间的一半已经空了。
  • 延迟确认 第二个解决方法是延迟一段时间后再发送确认。这时接收方不立即确认收到的报文段。接收方在确认收到的报文段之前一直等待,直到入缓存有足够的空间为止。该方法阻止了发送端滑动其窗口,当发送端发送完其数据后,它就停下来了。这样就防止了这种症状。延迟的确认还减少了通信量。接收端不需要确认每一个报文段。但它有可能使发送端重传其未被确认的报文段。可以给延迟的确认加一个时间限制来降低该方法缺点的影响。

Nagle算法

Nagle算法的核心思想是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块。Nagle算法的规则:

  • 如果包长度达到MSS,则允许发送;
  • 如果该包含有FIN,则允许发送;
  • 设置了TCP_NODELAY选项,则允许发送;
  • 未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
  • 上述条件都未满足,但发生了超时(一般为200ms),则立即发送。

Nagle算法在任意时刻只允许存在一个未被确认的报文段,但他它并不关心报文段的大小,因此它事实上就是一个扩展的停止等待协议,只不过它是基于报文段的而不是基于字节的。Nagle算法完全由TCP协议的ACK机制决定,因此也有一些缺点,比如如果对端ACK回复很快的话,Nagle事实上不会拼接太多的数据包,虽然避免了网络拥塞,网络总体的利用率依然很低。

在某些时刻也可能会需要关闭该算法,尤其是交互式的TCP应用,因为这种应用期望及时收到响应。这可以通过打开TCP_NODELAY选项来实现。

TCP_CORK 选项

设置该选项后,内核会尽力把小数据包拼接成一个大的数据包(一个MTU)再发送出去,当然若一定时间后(一般为200ms,该值尚待确认),内核仍然没有组合成一个MTU时也必须发送现有的数据。

Nagle算法与CORK算法区别

Nagle算法和CORK算法非常类似,但是它们也有区别:
它们的目地不同。Nagle算法主要避免网络因为有太多的小报文段而拥塞,而CORK算法则是为了提高网络的利用率,使得总体上协议头占用的比例尽可能的小。

用户通过TCP_NODELAY来启用或禁用Nagle算法而通过TCP_CORK来启用或禁用CORK算法。

Nagle算法关心的是网络拥塞问题,只要有ACK回来则发包;而CORK算法关心的是报文段大小,在前后数据发送间隔很短的前提下,即使你是分散发送多个小数据包,你也可以通过使能CORK算法将这些内容拼接在一个包内,如果此时用Nagle算法的话,则可能做不到这一点。

6 紧急方式

TCP提供了“紧急方式(urgentmode)”,它使连接的一端可以告诉另连接的一端有些 “紧急数据”已经被放置在数据流中。紧急数据的处理方式由接收方决定。

要发送紧急数据需要设置TCP首部中的两个字段来。URG比特被置1,并且要将16bit的紧急指针设置为一个正的偏移量,该偏移量必须与TCP首部中的序号字段相加,以便得出紧急数据的最后一个字节的序号。

TCP必须通知接收进程,何时已接收到一个紧急数据指针以及何时某个紧急数据指针还不在此连接上,或者紧急指针是否在数据流中向前移动。接着接收进程可以读取数据流,并必须能够被告知何时碰到了紧急数据指针。只要从接收方当前读取位置到紧急数据指针之间有数据存在,就认为应用程序处于“紧急方式”。在紧急指针通过之后,应用程序便转回到正常方式。

没有办法指明紧急数据从数据流的何处开始。TCP通过连接传送的唯一信息就是紧急方式已经开始(TCP首部中的URG比特)和指向紧急数据最后一个字节的指针。其他的事情留给应用程序去处理。