目录

可靠数据传输

可靠数据传输原理

可靠数据传输(Reliable Data Transfer)是一个很大的概念,在计算机网络的五层模型中,它可以出现在传输层、数据链路层和应用层。简单点说,可靠数据传输服务保证了通信过程中的数据接收方接收到的数据与其从数据发送方发出的数据一致。而提供这种可靠数据传输服务的协议称为可靠数据传输协议,例如传输层的TCP协议。

可靠数据传输为上层实体提供的服务抽象是:数据可以通过一条可靠的信道进行传输。在这条信道中传输的数据bit不会受到损坏和或丢失,而且所有的数据都是按其发送顺序交付。其对应的服务框架如下图所示,TCP协议向调用它的因特网应用所提供的服务模型恰恰如此,所以当谈到可靠数据传输时,不可避免地会涉及大量TCP协议的分析。

那么一个网络协议要实现可靠数据传输需要具备哪些特性呢?或者说,协议中需要设定哪些规则来避免数据传输的不可靠?为了数据传输的可靠,我们就要解决数据不可靠的情况,而数据传输的不可靠的情形最基本的有如下几种:

  • 数据在信道中传输时,比特出现了错误,例如本来该bit为0,到了接收端却成为了1
  • 数据在信道中传输时,比特发生了丢失,例如本来按序发送了16个比特,结果到了接收端只收到了8个比特
  • 数据在信道中传输时,比特流的顺序发生了变化,例如发送端按照序号1,2,3,4,5发送的比特流,结果到了接收端收到是1,3,2,5,4
  • 数据在信道中传输时,接收端接收到了重复的数据,例如发送端按照序号1,2,3发送,到了接收端收到是1,2,2,3

先思考一下,针对上面的若干基本情况,我们要怎么处理。首先数据在接收方出现了不一致的情况,我们如何得知呢?这就需要通过差错检测机制,通过差错检测,我们可以检测到何时何处出现了比特差错。然后,出现了差错该怎么办呢?作为接收方,肯定要告诉发送方我收到的数据有问题,所以就要有接收方反馈功能,也就是接收方发送明确的反馈信息给发送方。那么发送方接收了该反馈信息又该怎么做呢?最基本且有效的情况无非就是重新发送一份错误数据对应的原数据,也就是重传

可靠数据传输特征

差错检测

​ 差错检测主要的实现方式就是在传输的数据流中添加额外的比特信息以增加冗余度,主要分为两大类:奇偶效验和分组检验。其中分组校验中典型的方法有校验和法(IP数据报)和循环冗余校验法。

接收方反馈

一般是给出肯定确认ACK或否定确认NAK,也可以通过冗余ACK的方式代替否定确认。

重传

在分组传送时,主要有累积重传和选择重传。

通过上述几个功能,我们可以设计出能够工作的协议,但是,仔细想象一下你就会发现,这个步骤中有一个致命缺陷。在接收方给发送方发送反馈信息时,假设这个反馈信息也出了错误,那么发送方就无法知道接收方是否正确接收上一次发送的数据了。

再思考一下,如果我们是协议设计者,会怎么做?可以考虑以下几种方式:

  1. 发送方接收到错误的反馈信息表示无法理解时,便又向接收方发送针对该反馈信息的解释请求,即引入了一种新型的从发送方到接收方的分组。就像打电话时一方不理解另一方时会提问“你说啥?”,另一方则会重复回答。要是这个“你说啥”也产生了差错,岂不又是越陷越深了。

  2. 增加足够的检验和比特,使得发送方不但可以检测差错还可以恢复差错,对于只产生错误而没有丢失的分组,就可以直接解决问题。

  3. 当发送方收到含糊不清的反馈信息后,只需要重传当前数据分组即可。这种方法在信道中引入了冗余分组。而冗余分组所带来一个问题就是,接收方不知道它上次发送的反馈信息是否被发送方正确接收,也就是说接收方无法事先知道接收到的分组时新的还是一次重传,这很重要,因为如果将重传分组当作新的分组,那么数据包就会出错了。

序号

​为了解决上述冗余分组中的问题,我们就又提出一种协议功能,就是给数据分组编号,这也是包括TCP在内所有的数据分组采用的方法。即在数据分组中添加一个新的字段,让发送方对其数据分组编号,将发送数据分组的序号放在该字段。于是,接收方只需要检查序号就可以确定该分组是否是一次重传了。对于停等协议(Stop and Wait,SW)来说,该字段只需要一个比特即可。因为停等协议中,发送方必须等待接收方传回ACK或NAK才能继续发送下一个数据分组。

除了具备差错检测与恢复外,我们还需要考虑当分组丢失分情况,而上面的协议功能不足以解决分组丢失的问题。当发送方发送一个数据后,在信道中丢失,接收方自然没有收到数据从而不会发送反馈,于是发送方迟迟没有收到确认。这时候,我们一般情况下都是会等待一定时间后重新发送该分组。所以我们又引入一个新的协议功能:倒计数定时器,通过设定一个给定的时间量,当时间量过期后,便重传分组。

倒计数定时器

为了容忍数据丢失,我们在发送方引入倒计数定时器。即1. 每次发送一个分组时(包括第一次分组和重传分组),启动一个定时器。2. 响应定时器中断(采取适当动作,比如重传)。3. 终止定时器。

综上,在实现了差错检测,接收方反馈,重传,序号,倒计数定时器这些协议功能后,就可以得到一个可靠数据传输协议了。

具备上述协议功能的协议分为两种,一种是停止-等待协议,一种是流水线协议,后者是前者的加强版,提升了信道的利用率。实际上ARQ协议也是分为了类似的两类。说到ARQ,那么就介绍一下它:

 ARQ协议,即自动重传请求(Automatic Repeat-reQuest),是OSI模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送。ARQ包括停止等待ARQ协议连续ARQ协议,拥有错误检测(Error Detection)、正面确认(Positive Acknowledgment)、超时重传(Retransmission after Timeout)和 负面确认及重传(Negative Acknowledgment and Retransmission)等机制。其中,连续ARQ就是一种流水线协议。

我们说到流水线协议相对于停止等待协议增加了信道的利用率,但是也带来了一定程度的局限性。例如:

  • 流水线协议必须增加序号的范围,因为每个传输中的分组必须有一个唯一的序号用来区分分组,而且也许在输送中存在多个未确认的报文。而停止等待协议只需一个比特位来给分组编号(即比特交替协议)

  • 协议的发送方和接收方两端也许必须缓存多个分组。这样做的目的是为了差错恢复,这取决于协议如何处理丢失、损坏以及时延过大的分组所需序号范围i和对缓冲的要求取决于数据传输协议如何

流水线协议中差错恢复的两种方法:回退N步(Go-Back-N,GBN)和选择重传(Selective Repeat,SR)。

回退N步协议(GBN)

回退N步协议允许发送方发送多个分组而不确认,但是其在流水线中未确认的分组的数量不能超过N。这个协议又被称作滑动窗口协议,是因为流水线中已被发送但还未被确认的分组的许可范围构成了一个在序号范围内长度为N的窗口,N就是窗口的大小,随着协议运行,该窗口的序号空间不断向前滑动。思考一下,为什么要设置这么一个窗口呢?其实是为了流量控制和拥塞控制的需要,我们将在后文介绍。

在GBN中,发送方必须响应的三种类型事件是:

  • 上层的调用。

  • 收到一个ACK,该协议对序号为n的分组的确认采取累积确认的方式,表明接收方已经正确接收到序号为n的以前且包括n在内的所有分组。

  • 超时事件,go-back N行为正来自出现丢失或时延过长分组时发送方采取的差错恢复行为。同样也用到了定时器,出现超时的时候,发送方将重传所有已发送但还未被确认的分组,所以在发送方缓存这些分组是十分必要的。如果收到一个ACk,但仍有已发送但未确认的分组,则定时器会被重新启动,如果没有则会终止。

GBN协议中,接收方会丢弃失序的分组,因为GBN采用累积确认,一次交付给上层一个分组,接收方必须按序将数据交付给上层,丢失分组是一种简单的方法之一,另外的一种就是放在接收方缓存起来,等带下一个按序的分组,最后在组装正确的数据交付。 

选择重传协议(SR)

GBN虽然采用了流水线解决了信道利用率的问题,但是也会存在另外的性能问题,例如当窗口长度很大或者带宽时延积都很大时,在流水线中有更多的分组时更甚。因为在这些情况下,必须重传大量分组,代价太大了,也许只是窗口中诸多分组的一个或几个分组出现错误,便要重传全部已经发送但未确认的分组,未免太浪费了。

而选择重传机制就是让发送方仅仅重传那些它怀疑在接收方出错(丢失或受损)的分组,从而避免了不必要的重传。但是这种方式就不能采用累积确认了,而是要求接收方逐个地确认正确接收的分组。即接收方确认正确的分组不管其是否失序,失序的分组将被缓存直到所有比失序分组序号小的分组都被接收为止,才会将一批分组按序交付。

但是这种方式会带来一个问题,就是接收方和发送方的窗口并不是总是一致的,因为对于哪些分组已经被正确接收,哪些没有,发送方和接收方并不是总能看到一样的结果。这意味着,当序号范围有限时,发送方和接收窗口之间缺乏同步会产生严重的后果。这就要求SR协议中,窗口长度必须小于或等于序号空间大小的一半。后续将证明。

到现在为止,我们介绍了若干可靠传输机制,下表是一个总结。

机制 用途和说明
检验和 用于检测在一个传输分组中的比特错误
定时器 用于超时/重传一个分组,可能因为该分组(或其ACK)在信道中丢失了。由于当一个分组延时但未丢失,或当一个分组已被接收方接收但从接收方到发送方的ACK丢失时,可能产生超时事件,所以接收方可能会收到一个分组的多个冗余副本
序号 用于为从发送方流向接收方的数据分组按序号编号。所接受分组的序号的空隙可使接收方检测出丢失的分组。具有相同序号的分组可使接收方检测出一个分组的冗余副本
确认 接收方用于告诉发送方一个分组或一组分组已经被正确地接收到了。确认报文通常携带着被确认的分组或多个分组的序号。确认可以是逐个的或积累的,这取决于协议
否定确认 接收方用于告诉发送方某个分组未被正确的接收.否则人确定报文通常携带着未被正确接收的分组的序号
窗口、流水线 发送方也许被限制仅发送那些序号落在一个指定范围内的分组.通过允许一次发送多个分组但未被确认,发送方的利用率可以在停等操作模式上得到增加。我们很快将会看到,窗口长度可根据接收方接收和缓存报文的能力、网络中的拥塞程度或两者的情况来进行设置

从TCP协议中看可靠数据传输

介绍了可靠数据传输的基本原理之后,我们再来探讨一种典型的可靠数据传输协议——TCP,TCP是因特网运输层的面向连接的可靠的运输协议。

TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 IP 地址、端口号等。

TCP 可以看成是一种字节流,它会处理 IP 层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在 TCP 头部。

TCP 提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用 四次挥手来关闭一个连接

TCP报文段结构

16 位端口号(port number):告知主机报文段是来自哪里(源端口)以及传给哪个上层协议或应用程序(目的端口)的。进行 TCP 通信时,客户端通常使用系统自动选择的临时端口号。

32 位序号(sequence number):一次 TCP 通信(从 TCP 连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。假设主机 A 和主机 B 进行 TCP 通信,A 发送给 B 的第一个TCP 报文段中,序号值被系统初始化为某个随机值 ISN(Initial Sequence Number,初始序号值)。那么在该传输方向上(从 A 到 B),后续的 TCP 报文段中序号值将被系统设置成 ISN 加上该报文段所携带数据的第一个字节在整个字节流中的偏移。例如,某个 TCP 报文段传送的数据是字节流中的第 1025 ~ 2048 字节,那么该报文段的序号值就是 ISN + 1025。另外一个传输方向(从B 到 A)的 TCP 报文段的序号值也具有相同的含义。

32 位确认号(acknowledgement number):用作对另一方发送来的 TCP 报文段的响应。其值是收到的 TCP 报文段的序号值 + 标志位长度(SYN,FIN) + 数据长度 。假设主机 A 和主机 B 进行TCP 通信,那么 A 发送出的 TCP 报文段不仅携带自己的序号,而且包含对 B 发送来的 TCP 报文段的确认号。反之,B 发送出的 TCP 报文段也同样携带自己的序号和对 A 发送来的报文段的确认序号。

4 位头部长度(head length):标识该 TCP 头部有多少个 32 bit(4 字节)。因为 4 位最大能表示 15,所以 TCP 头部最长是60 字节

6 位标志位包含如下几项

  • URG 标志,表示紧急指针(urgent pointer)是否有效。

  • ACK 标志,表示确认号是否有效。我们称携带 ACK 标志的 TCP 报文段为确认报文段。

  • PSH 标志,提示接收端应用程序应该立即从 TCP 接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在 TCP 接收缓冲区中)。

  • RST 标志,表示要求对方重新建立连接。我们称携带 RST 标志的 TCP 报文段为复位报文段。

  • SYN 标志,表示请求建立一个连接。我们称携带 SYN 标志的 TCP 报文段为同步报文段。

  • FIN 标志,表示通知对方本端要关闭连接了。我们称携带 FIN 标志的 TCP 报文段为结束报文段。

16 位窗口大小(window size):是 TCP 流量控制的一个手段。这里说的窗口,指的是接收通告窗口(Receiver Window,RWND)。它告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。

16 位校验和(TCP checksum):由发送端填充,接收端对 TCP 报文段执行 CRC 算法以校验 TCP 报文段在传输过程中是否损坏。注意,这个校验不仅包括 TCP 头部,也包括数据部分。 这也是 TCP 可靠传输的一个重要保障。

16 位紧急指针(urgent pointer):是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP 的紧急指针是发送端向接收端发送紧急数据的方法

TCP三次握手

1、三次握手是什么?

三次握手其实就是指在建立TCP连接时需要客户端和服务器端总共发送三个数据包,主要作用是确认服务器端和客户端的发送和接受能力是否正常,指定自己的初始序列号为后面的可靠性传输作准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息

第一次握手: 客户端向服务器端发送一个SYN报文,并指明自己的初始序列号ISN,此时客户端处于SYN_SEND状态。

首部的同步位SYN=1,初始序列号seq=1,SYN=1的报文段不能携带数据,但要消耗掉一个序号

第二次握手:服务器端在收到客户端的SYN报文后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。

在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y

第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接

确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。

2、为什么需要三次握手,两次不行吗?

弄清这个问题,我们需要先弄明白三次握手的目的是什么,能不能只用两次握手来达到同样的目的。

  • 第一次握手:客户端发送网络包,服务端收到了。 这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
  • 第二次握手:服务端发包,客户端收到了。 这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
  • 第三次握手:客户端发包,服务端收到了。 这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

因此,需要三次握手才能确认双方的接收与发送能力是否正常。

试想如果是用两次握手,则会出现下面这种情况:

如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源

3、什么是半连接队列?

服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列

当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。

这里在补充一点关于SYN-ACK 重传次数的问题: 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。 注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s,2s,4s,8s……

4、ISN(Initial Sequence Number)是固定的吗?

当一端为建立连接而发送它的SYN时,它为连接选择一个初始序号。ISN随时间而变化,因此每个连接都将具有不同的ISN。ISN可以看作是一个32比特的计数器,每4ms加1 。这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它做错误的解释。

三次握手的其中一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number),以便让对方知道接下来接收数据的时候如何按序列号组装数据。如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。

5、三次握手过程中可以携带数据吗?

其实第三次握手的时候,是可以携带数据的。但是,第一次、第二次握手不可以携带数据

为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。

也就是说,第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。

6、SYN攻击是什么?

服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。

检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 netstats 命令来检测 SYN 攻击。

1
netstat -n -p TCP | grep SYN_RECV

常见的防御 SYN 攻击的方法有如下几种:

  • 缩短超时(SYN Timeout)时间
  • 增加最大半连接数
  • 过滤网关防护
  • SYN cookies技术 

TCP滑动窗口

滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包 (称窗口尺寸)。 、

TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方一般不能再发送数据报。

滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构

TCP四次挥手

1、四次挥手过程?

建立一个连接需要三次握手,而终止一个连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这由TCP的半关闭(half-close)造成的。所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。

TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务器均可主动发起挥手动作。

刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:

  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。 即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。 即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
  • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。 即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
  • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。 即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。

收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。

在socket编程中,任何一方执行close()操作即可产生挥手操作。

2、挥手为什么需要四次?

因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。

3、2MSL等待状态

TIME_WAIT状态也成为2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。

对一个具体实现所给定的MSL值,处理的原则是:当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)

这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。

4、四次挥手释放连接时,等待2MSL的意义?

MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

为了保证客户端发送的最后一个ACK报文段能够到达服务器。因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器。最后客户端和服务器都能正常的关闭。假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。

两个理由:

  1. 保证客户端发送的最后一个ACK报文段能够到达服务端。 这个ACK报文段有可能丢失,使得处于LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认,服务端超时重传FIN+ACK报文段,而客户端能在2MSL时间内收到这个重传的FIN+ACK报文段,接着客户端重传一次确认,重新启动2MSL计时器,最后客户端和服务端都进入到CLOSED状态,若客户端在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到服务端重传的FIN+ACK报文段,所以不会再发送一次确认报文段,则服务端无法正常进入到CLOSED状态。
  2. 防止“已失效的连接请求报文段”出现在本连接中。 客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。

5、为什么TIME_WAIT状态需要经过2MSL才能返回到CLOSE状态?

理论上,四个报文都发送完毕,就可以直接进入CLOSE状态了,但是可能网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文

TCP状态转换

流量控制

TCP 利用滑动窗口实现流量控制

流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。

TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。

TCP的两端都有发送/接收缓存和发送/接收窗口。TCP的缓存是一个循环队列。在进程调用send()发送的数据的时候,会将数据拷贝进入内核的socket发送缓冲区之中,然后send便会在上层返回。换句话说,send返回之时,数据不一定会发送到对端去。接收缓冲区把数据缓存入内核,应用进程如果一直没有调用recv()进行读取的话,此数据会一直缓存在相应socket的接收缓冲区内。不管进程是否调用recv()读取socket,对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中。

发送缓存中的发送窗口可以用3个指针表示。已发送但未被确认数据大小<=发送窗口的大小。已被确认的数据会从发送缓存中删除。然后发送窗口整体向左移。此外要说明一下,接受方的接受窗口是指接受方中的接受缓存中剩余可用空间大小,该值会通知给发送端调整发送窗口,从而控制流量。

发送方应用程序必须控制写入缓存的速率,不能太快,否则发送缓存会没有存放数据的空间。但是,linux下可以动态调整缓冲区的大小。发送缓冲区的动态调整比较简单,当发送缓冲区的剩余空间充足的时候,可以减小发送缓冲区的大小,反之则增加缓冲区的大小。

1
2
3
4
5
cat /proc/sys/net/ipv4/tcp_wmem

4096    87380   4194304

// 4096表示缓冲区的最小大小,4194304表示缓冲区的最大大小,87380 表示默认值

发送窗口的大小受TCP数据报中窗口大小的影响,TCP数据报中的窗口大小是接收端通知发送端其还可以接收多少数据,因此发送窗口根据接收的的窗口大小的值动态变化。需要明确的是,发送窗口只是发送缓存的一部分。

接受缓存区也可以自动调整,在高并发的场景中可以降低默认的缓冲区大小,提高并发性。

1
2
3
cat /proc/sys/net/ipv4/tcp_rmem

4096    87380   4194304

查看是否开启接受缓冲区自动调整的功能

1
2
3
cat /proc/sys/net/ipv4/tcp_moderate_rcvbuf

4096 87380 4194304

发送缓冲区自动调节的依据是待发送的数据,接收缓冲区由于只能被动地等待接收数据,它该如何自动调整呢?可以依据空闲系统内存的数量来调节接收窗口。如果系统的空闲内存很多,就可以把缓冲区增大一些,这样传给对方的接收窗口也会变大,因而对方的发送速度就会通过增加飞行报文(发送还未确认的报文)来提升。反之,当内存紧张时就会缩小缓冲区,这虽然会减慢速度,但可以保证更多的并发连接正常工作。需要注意,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样做就可以避免了丢包情况。

当接受缓存满并且系统不再有内存分配调整大小,此时接受方会发送零窗口大小的报文,具体做法是将返回的确认(ACK)报文的窗口值大小设置为0(窗口关闭),让发送方停止发送数据。之后接收方有足够的缓存,再发送非零窗口大小的报文。

此外,TCP为每一个连接设置一个持续计时器(persistence timer)。只要TCP的一方收到对方的零窗口通知,就启动该计时器,周期性的发送一个零窗口探测报文段。对方就在确认这个报文的时候给出现在的窗口大小(注意:TCP规定,即使设置为零窗口,也必须接收以下几种报文段:零窗口探测报文段、确认报文段和携带紧急数据的报文段)。这样做是为了避免发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,使得变成相互等待,会造成了死锁的现象

拥塞控制

在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的包丢失,此时网络拥塞将会进入恶性循环并且不断地放大。

拥塞控制协议维护着一个拥塞窗口,会根据网络的拥塞状态而动态调整,其调整规则为1.只要网络中没有出现拥塞,cwnd 就会增大;2. 网络中如果出现了拥塞,cwnd 就减少。加入拥塞窗口后,此时发送窗口swnd = min(cwnd(拥塞窗口), rwnd(接受窗口))

那么如何判断是否出现网络拥塞呢?一种简单的方法就是只要发生了丢包,触发超时重传就会判定网络出现拥塞。

TCP 在刚建立连接完成后,首先是有个慢启动的过程,以指数性的提高发送数据包的数量。当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。当慢启动一段时间后,达到慢启动门限 ssthresh,就会改用拥塞避免模式,进行线性增大。当网络出现超时重传(拥塞)时,门限值调整为一半,cwnd=1,然后改为执行慢启动模式。当网络出现快速重传(拥塞)时,cwnd = cwnd/2ssthresh = cwnd,然后改为执行拥塞避免模式。

慢开始+拥塞避免

(1)拥塞窗口cwnd初始化为1个报文段,慢开始门限初始值为16;

(2)执行慢开始算法,指数规律增长到第4轮,即cwnd=16=ssthresh,改为执行拥塞避免算法,拥塞窗口按线性规律增长;

(3)假定cwnd=24时,网络出现超时(拥塞),则更新后的ssthresh=cwnd/2=12cwnd重新设置为1,并执行慢开始算法。

(4)当cwnd=12=ssthresh时,改为执行拥塞避免算法。

快重传+快恢复

快重传(Fast retransmit)

快速重传(Fast retransmit)要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方),而不要等到自己发送数据时捎带确认。例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。

快重传算法规定,发送方只要一连收到3个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计数器时间到期。

快恢复(Fast Recovery)

(1)当发送方连续收到三个重复确认,就执行“乘法减小”算法,把慢开始门限ssthresh减半。这是为了预防网络发生拥塞。此时直接进入拥塞避免。请注意:接下来不执行慢开始算法。 (2)由于发送方现在认为网络很可能没有发生拥塞,因此与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。

拥塞控制与流量控制区别

拥塞控制:拥塞控制是作用于网络的,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况;常用的方法就是:( 1 )慢开始、拥塞避免( 2 )快重传、快恢复。

流量控制:流量控制是作用于接收者的,它是控制发送者的发送速度从而使接收者来得及接收,防止分组丢失的。流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。

参考

【计算机网络】学习笔记汇总(目录)(谢希仁) - 知乎 (zhihu.com)