CLOSED: 这个没什么好说的了,表示初始状态。
LISTEN: 这个也是非常容易理解的一个状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了。
SYN_RCVD: 这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态。
SYN_SENT: 这个状态与SYN_RCVD遥想呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即它会进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
ESTABLISHED:这个容易理解了,表示连接已经建立了。
FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。
FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。
TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。
LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。
TCP 的 TIME_WAIT 状态
在TCP的状态图中,从TIME_WAIT状态到CLOSED状态,有一个超时设置,这个超时设置是 2*MSL(RFC793定义了MSL为2分钟,Linux设置成了30s。这意味着TIME_WAIT的典型持续时间为1-4分钟。)
IP头部有一个TTL,最大值255。尽管TTL的单位不是秒(根本和时间无关),我们仍需假设,TTL为255的TCP报文在Internet上生存时间不能超过MSL。
- 为什么要这有TIME_WAIT?为什么不直接给转成CLOSED状态呢?
主要有两个原因:
① 为尽最大可能的实现TCP这种全双工(full-duplex)连接的可靠释放
TIME_WAIT确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到Ack,就会触发被动端重发Fin,一来一去正好2个MSL。
假设最后一个ACK丢失了,被动关闭一方会重发它的FIN。主动关闭一方必须维持一个有效状态信息(TIMEWAIT状态下维持),以便能够重发ACK。如果主动关闭的socket不维持这种状态而进入CLOSED状态,那么主动关闭的socket在处于CLOSED状态时,接收到FIN后将会响应一个RST。被动关闭一方接收到RST后会认为出错了。
② 为使旧的数据包在网络因过期而消失,以防止lost duplicate对后续新建正常链接的传输造成破坏。
有足够的时间让这个连接不会跟后面的连接混在一起,即,允许老的重复报文段在网络中消逝(你要知道,有些自做主张的路由器会缓存IP数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)。
TCP报文段可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个 原来的迷途分节就称为lost duplicate。在关闭一个TCP连接后,马上又重新建立起一个相同的IP地址和端口之间的TCP连接,后一个连接被称为前一个连接的化身 (incarnation),那么有可能出现这种情况,前一个连接的迷途重复分组在前一个连接终止后出现,从而被误解成从属于新的化身。为了避免这个情况,TCP不允许处于TIME_WAIT状态的连接启动一个新的化身,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个TCP连接的时候,来自连接先前化身的重复报文段已经在网络中消逝。
在高并发短连接的TCP服务器上,当服务器处理完请求后立刻按照主动正常关闭连接。这个场景下,会出现大量socket处于TIMEWAIT状态。如果客户端的并发量持续很高,此时部分客户端就会显示连接不上。
因此,关于高并发的短连接,我们需要注意:
a)高并发可以让服务器在短时间范围内同时占用大量端口,而端口有个0~65535的范围(当客户端为同一ip的情况下),并不是很多,刨除系统和其他服务要用的,剩下的就更少了。
b)在这个场景中,短连接表示“业务处理+传输数据的时间 远远小于 TIMEWAIT超时的时间”的连接。单用这个业务计算服务器的利用率会发现,服务器干正经事的时间和端口(资源)被挂着无法被使用的时间的比例是 1:几百,服务器资源严重浪费。
综合这两个方面,持续的到达一定量的高并发短连接,会使服务器因端口资源不足而拒绝为一部分客户服务。同时,这些端口都是服务器临时分配,无法用SO_REUSEADDR选项解决这个问题:(
① 利用SO_LINGER选项的强制关闭方式,发RST而不是FIN,来越过TIMEWAIT状态,直接进入CLOSED状态。
设置 l_onoff为非0,l_linger为0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态。
② 设置tcp_tw_reuse、tcp_tw_recycle参数
只要搜一下,你就会发现,十有八九的处理方式都是教你设置两个参数,一个叫tcp_tw_reuse,另一个叫tcp_tw_recycle的参数,这两个参数默认值都是被关闭的,后者recyle比前者resue更为激进,resue要温柔一些。另外,如果使用tcp_tw_reuse,必需设置tcp_timestamps=1,否则无效。这里,你一定要注意,打开这两个参数会有比较大的坑——可能会让TCP连接出一些诡异的问题(因为如上述一样,如果不等待超时重用连接的话,新的连接可能会建不上。正如官方文档上说的一样“It should not be changed without advice/request of technical experts”)。
③ 一般情况下出现TIME_WAIT状态的都是客户端,在业务逻辑中尽量让客户端主动关闭连接,这样也就将TIME_WAIT变相的转载了。但是在有些业务中必须让服务器主动关闭连接。
对于基于TCP的HTTP协议,关闭TCP连接的是Server端,这样,Server端会进入TIME_WAIT状态,可想而知,对于访问量大的Web Server,会存在大量的TIME_WAIT状态。
HTTP协议1.1版规定default行为是Keep-Alive,也就是会重用TCP连接传输多个request/response,一个主要原因就是发现了这个问题。
TCP 流量控制 和 拥塞控制
流量控制是通过接收方来控制流量的一种方式;而拥塞控制则是通过发送方来控制流量的一种方式。
拥塞控制的任务是确保子网能够承载所到达的流量。这是一个全局性问题,涉及到各方面的行为,包括所有的主机、所有的路由器、路由器内部的存储转发处理过程,以及所有可能会削弱子网承载容量的其它因素。
网络拥塞现象是指到达通信子网中某一部分的分组数量过多,使得该部分网络来不及处理,以致引起这部分乃至整个网络性能下降的现象,严重时甚至会导致网络通信业务陷入停顿,即出现死锁现象。拥塞控制是处理网络拥塞现象的一种机制。
TCP的慢启动(拥塞窗口):
TCP在局域网环境中的效率是很高的,但是到了广域网的环境中情况就不同了,在发送方和接收方之间可能存在多个Router以及一些速率比较慢的链路,而且一些中继路由器必须缓存分组,还可能分片,所以在广域网的环境中,TCP的效率可能出现问题。
为了解决这个问题,现在的TCP栈都支持“慢启动”算法,即拥塞窗口控制算法。其实,拥塞窗口是发送方使用的一种流量控制算法。
慢启动为TCP的发送方增加了一个拥塞窗口,当连接建立时,拥塞窗口被初始化为一个报文段大小,每收到一个ACK,拥塞窗口就会增加一个报文段,发送方取拥塞窗口与滑动窗口的最小值作为发送的上限。
流量控制只与特定的发送方和特定的接收方之间的点到点流量有关。它的任务是,确保一个快速的发送方不会持续地以超过接收方吸收能力的速率传输数据,以免数据丢失。
滑动窗口:
TCP使用滑动窗口协议来进行流量控制。特别需要注意的是,滑动窗口是一个抽象的概念,它是针对每一个TCP连接的,而且是有方向的,一个TCP连接应该有两个滑动窗口,每个数据传输方向上有一个,而不是针对连接的每一端的。
当左边沿和右边沿重合的时候表明窗口大小是0,此时发送方不应该在发送数据了,因为接收方的接收缓冲区已满,用户进程还没以接收。当用户进程接收完成后,接收方应该发送一个ACK,表明此时的接收窗口已经恢复,此ACK的序号同前一个win为0的ACK相同。
TCP窗口大小的调整:TCP窗口的大小通常由接收端来确认,也就是在TCP建立连接的第二个SYN+ACK报文的Win字段来确认。
sock API允许进程设置发送和接收缓存的大小。接收缓存的大小是该连接上所能够通告的最大窗口大小。
发送方一开始便向网络发送多个报文段,直至的窗口达到接收方通告大小为止。当发送方和接收方处于同一个局域网时,这种方式是可以的。但是如果在发送方和接收方之间存在多个路由器和速率较慢的链路时,就有可能出现一些问题。一些中间路由器必须缓存分组,并有可能耗尽存储器的空间。TCP需要支持一种被称为慢启动的算法。该算法通过观察到新分组进入网络的速率应该与另一端返回确认的速率相同而进行工作。
慢启动为发送方的TCP增加了另一个窗口:拥塞窗口,cwnd。
当与另一个网络的主机建立TCP连接时,拥塞窗口被初始化为1个报文段,即另一端通过的报文段大小。每收到一个ACK,拥塞窗口就增加一个报文段,cwnd以字节为单位,但是慢启动以报文段大小为单位进行增加。发送方取拥塞窗口与滑动窗口中的最小值作为发送上限。拥塞窗口是发送方使用的流量控制,而滑动窗口则是接收方使用的流量控制。
发送方开始时发送一个报文段,然后等待ACK。当接收该ACK时,拥塞窗口从1增加为2,即可以发送2个报文段。当收到这2个报文段的ACK时,拥塞窗口就增加为4。这是一种指数增加的关系。
TCP 的 接收缓冲区 和 发送缓冲区
先明确一个概念:每个TCP socket在内核中都有一个发送缓冲区和一个接收缓冲区,TCP的全双工的工作模式以及TCP的滑动窗口便是依赖于这两个独立的buffer以及此buffer的填充状态。
接收缓冲区:
接收缓冲区把数据缓存入内核,应用进程一直没有调用read进行读取的话,此数据会一直缓存在相应 socket的接收缓冲区内。再啰嗦一点,不管进程是否读取socket,对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中。 read所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,仅此而已。
接收缓冲区被TCP和UDP用来缓存网络上来的数据,一直保存到应用进程读走为止。对于TCP,如果应用进程一直没有读取,buffer满了之后,发生的动作是:通知对端TCP协议中的窗口关闭。这个便是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。 这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。
发送缓冲区:
进程调用send发送的数据的时候,最简单情况(也是一般情况),将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。换句话说,send返回之时,数据不一定会发送到对端去(和 write写文件有点类似),send仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中,但不代表将数据发送给对端。发送给对端成功的标志是接受到对端的ACK回应,这个时候发送端才可以将发送缓冲区的数据丢弃。不丢弃的原因是时刻准备重发丢失/出错的数据!
UDP:
每个UDP socket都有一个接收缓冲区,没有发送缓冲区,从概念上来说就是只要有数据就发,不管对方是否可以正确接收,所以不缓冲,不需要发送缓冲区。
关于UDP,当套接口接收缓冲区满时,新来的数据报无法进入接收缓冲区,此数据报就被丢弃。UDP是没有流量控制的;快的发送者可以很容易地就淹没慢的接收者,导致接收方的UDP丢弃数据报。
TCP 的 ACK 包
什么时候发送ACK确认数据包:
① 收到1个包,启动200ms定时器,等到200ms的定时器到点了(第二个包没来),于是对这个包的确认ack被发送。这叫做“延迟发送”;
② 收到1个包,启动200ms定时器,200ms定时器还没到,第二个数据包又来了(两个数据包一个ack);
③ 收到1个包,启动200ms定时器,还没超时,正好要给对方发点内容。于是对这个包的确认ack就跟着捎过去。这叫做“捎带发送”;
④ 每当TCP接收到一个超出期望序号的失序数据时,它总是发送一个确认序号为其期望序号的ACK;
⑤ 窗口更新或者也叫做打开窗口,通知发送端可以继续发送;
⑥ 正常情况下对对方保活探针的响应
TCP 的 RST 包
有以下情况会发送RST包
① connect一个不存在的端口;
② 向一个已经关掉的连接send数据;
③ 向一个已经崩溃的对端发送数据(连接之前已经被建立);
④ close(sockfd)时,直接丢弃发送缓冲区中为发送的数据,并给对方发一个RST。这个是由SO_LINGER选项来控制的;
⑤ a重启,收到b的保活探针,a发rst,通知b。
TCP socket在任何状态下,只要收到RST包,即可进入CLOSED初始状态。
值得注意的是RST报文段不会导致另一端产生任何响应,另一端根本不进行确认。收到RST的一方将终止该连接。程序行为如下:
- 阻塞模型下,内核无法主动通知应用层出错,只有应用层主动调用read()或者write()这样的IO系统调用时,内核才会利用出错来通知应用层对端RST。
- 非阻塞模型下,select或者epoll会返回sockfd可读,应用层对其进行读取时,read()会报错RST。
TCP 关键术语
首先要看TCP/IP协议,涉及到四层:链路层,网络层,传输层,应用层。
其中以太网(Ethernet)的数据帧在链路层
IP包在网络层
TCP或UDP包在传输层
TCP或UDP中的数据(Data)在应用层
它们的关系是 数据帧{IP包{TCP或UDP包{Data}}}
在应用程序中我们用到的Data的长度最大是多少,直接取决于底层的限制。
我们从下到上分析一下:
1.在链路层,由以太网的物理特性决定了数据帧的长度为(46+18)-(1500+18),其中的18是数据帧的头和尾,也就是说数据帧的内容最大为1500(不包括帧头和帧尾),即MTU(Maximum Transmission Unit)为1500;
2.在网络层,因为IP包的首部要占用20字节,所以这的MTU为1500-20=1480;
3.在传输层,对于UDP包的首部要占用8字节,所以这的MTU为1480-8=1472;
所以,在应用层,你的Data最大长度为1472。 (当我们的UDP包中的数据多于MTU(1472)时,发送方的IP层需要分片fragmentation进行传输,而在接收方IP层则需要进行数据报重组,由于UDP是不可靠的传输协议,如果分片丢失导致重组失败,将导致UDP数据包被丢弃)。
从上面的分析来看,在普通的局域网环境下,UDP的数据最大为1472字节最好(避免分片重组)。
但在网络编程中,Internet中的路由器可能有设置成不同的值(小于默认值),Internet上的标准MTU值为576,所以Internet的UDP编程时数据长度最好在576-20-8=548字节以内。
路径MTU:
当在同一个网络上的两台主机互相进行通信时,该网络的MTU是非常重要的。但是如果两台主机之间的通信要通过多个网络,那么每个网络的链路层就可能有不同的MTU。重要的不是两台主机所在网络的MTU的值,重要的是两台通信主机路径中的最小MTU。它被称作路径MTU。
两台主机之间的路径MTU不一定是个常数。它取决于当时所选择的路由。而选路不一定是对称的(从A到B的路由可能与从B到A的路由不同),因此路径MTU在两个方向上不一定是一致的。
在建立连接的时候,通信的双方要互相确认对方的最大报文长度(MSS),以便通信。一般这个MSS长度是MTU减去固定IP首部和TCP首部长度。对于一个以太网,一般可以达到1460字节。当然如果对于非本地的IP,这个MSS可能就只有536字节,而且,如果中间的传输网络的MSS更佳的小的话,这个值还会变得更小。
当一个连接建立时【三次握手】,连接的双方都要通告各自的MSS。当建立一个连
接时,每一方都有用于通告它期望接收的MSS选项(MSS选项只能出现在SYN报文段中)。如果一方不接收来自另一方的MSS值,则MSS就定为默认值536字节(这个默认值允许20字节的IP首部和20字节的TCP首部以适合576字节IP数据报)。
【MSS=外出接口上的MTU-IP首部-TCP首部】
故采用TCP协议进行数据传输,是不会造成IP分片的。若数据过大,只会在传输层进行数据分段,到了IP层就不用分片。
总结:UDP不会分段,就由IP来分。TCP会分段,当然就不用IP来分了!
IP分片产生的原因是链路层的MTU;TCP分段产生原因是MSS.
但在实际场景中,TCP网络传输的数据包大小不是1460字节,而是1448字节。这是因为,实际场景下,TCP包头中会带有12字节的选项----时间戳。这样,单个TCP包实际传输的最大量就缩减为1448字节。1448=1500-20(IP头)-32(20字节TCP头和12字节TCP选项时间戳)。
即,1460的MSS;1448的负载。
- MSL(Maximum Segment Lifetime)———— 报文最大生存时间
每个TCP报文在网络内的最长时间,就称为MSL(Maximum Segment Lifetime),它的作用和IP数据包的TTL类似。RFC793指出,MSL的值是2分钟,但是在实际的实现中,常用的值有以下三种:30秒,1分钟,2分钟。
每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime)。它是任何报文段被丢弃前在网络内的最长时间。我们知道这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。
- ** RTT(Round-Trip Time) ———— 往返时间**
是指一个报文段从发出去到收到此报文段的ACK所经历的时间。通常一个报文段的RTT与传播时延和发送时延两个因素相关。
TCP 选项
这个套接字选项通知内核,如果端口忙,但TCP状态处于TIME_WAIT,可以重用端口。如果端口忙,TCP状态处于其他状态,重用端口时依旧指明“地址已经在使用中”。如果你的服务程序停止后向立刻重启,而新套接字依旧使用同一个端口,此时SO_REUSEADDR选项非常有用。但是如果前一个连接处于TIME_WAIT状态,而允许另一个拥有相同五元组连接出现,可能处理TCP报文时,两个连接互相干扰。所以使用SO_REUSEADDR选项就需要考虑这种情况。
一个套接字由五个部分组成:协议,本地地址,本地端口,远程地址和远程端口。SO_REUSEADDR仅仅表示可以重用本地地址,本地端口。
此选项指定函数close对面向连接的协议如何操作(如TCP)。内核缺省close操作是立即返回,如果有数据残留在套接口缓冲区中则系统将试着将这些数据发送给对方。
有下列三种情况:
① 设置 l_onoff为0,则该选项关闭,l_linger的值被忽略,等于内核缺省情况,close调用会立即返回给调用者,如果可能将会传输任何未发送的数据;
② 设置 l_onoff为非0,l_linger为0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态;
③ 设置 l_onoff 为非0,l_linger为非0,当套接口关闭时内核将拖延一段时间(由l_linger决定)。如果套接口缓冲区中仍残留数据,进程将处于睡眠状态,直 到(a)所有数据发送完且被对方确认,之后进行正常的终止序列(描述字访问计数为0)或(b)延迟时间到。此种情况下,应用程序检查close的返回值是非常重要的,如果在数据发送完并被确认前时间到,close将返回EWOULDBLOCK错误且套接口发送缓冲区中的任何数据都丢失。close的成功返回仅告诉我们发送的数据(和FIN)已由对方TCP确认,它并不能告诉我们对方应用进程是否已读了数据。如果套接口设为非阻塞的,它将不等待close完成。
个人认为,①和③的区别在于,①仅会尽最大努力去保证发送缓冲区中的数据会成功发送给对端,即使不成功,close操作也不会失败,也就说close操作会正确返回;而③的话,如果发送缓冲区的数据没有在我们设定的时间内成功发送出去(即,收到ACK包才算是成功发送),则close操作将返回EWOULDBLOCK错误。
TCP重传机制
TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。
注意,接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。
一种是不回ack,死等3,当发送方发现收不到3的ack超时后,会重传3。一旦接收方收到3后,会ack 回 4——意味着3和4都收到了。
但是,这种方式会有比较严重的问题,那就是因为要死等3,所以会导致4和5即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到Ack,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致4和5的重传。
对此有两种选择:1
① 一种是仅重传timeout的包。也就是第3份数据。
② 另一种是重传timeout后所有的数据,也就是第3,4,5这三份数据。
这两种方式有好也有不好。第一种会节省带宽,但是慢,第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等timeout,timeout可能会很长(在下篇会说TCP是怎么动态地计算出timeout的)
- 快速重传机制
于是,TCP引入了一种叫Fast Retransmit的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就ack最后那个可能被丢了的包,如果发送方连续收到3次相同的ack,就重传。Fast Retransmit的好处是不用等timeout了再重传。
比如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了,于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2,因为2还是没有收到,于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是ack回6。示意图如下: