【TCP/IP】TCP协议的流程图解

一、TCP协议起步

1. 什么是TCP协议

TCP是面向连接的协议,这是因为在一个应用进程可以开始向另一个应用进程发送数据之前,这两个进程必须先相互“握手”,即它们必须相互发送某些预备报文段,以建立确保数据传输的参数。它有以下几个特点:

  • 面向连接:TCP一定是“一对一”的,无法像 UDP 协议那样在同一时刻像多个主机发送消息,即无法做到一对多;
  • 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端(当然不是说绝对可靠);
  • 基于字节流:消息是“没有边界”的,所以无论我们消息有多大都可以进行传输。并且消息是“有序的”,当前一个消息没有收到的时候,即使它先收到了后面的字节已经收到,那么也不能扔给应用层去处理,同时对重复的报文会自动丢弃。

2. 为什么需要TCP协议,它在哪一层工作?

IP 层是“不可靠”的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中数据的完整性。如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。因为 TCP 是一个工作在传输层可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。

二、认识TCP头部格式

  • 【端口号】:端口号,用于多路复用或者分解来自(送到)上层的数据;
  • 【序列号】:在连接建立时由计算机计算出的初始值,通过 SYN 包传给对端主机,每发送一次新的数据包,就累加一次该序列号的大小。用来解决网络包乱序问题
  • 【确认应答号】:指下次期望收到的数据的序列号,发送端收到这个确认应答以后可以确认确认应答号-1的数据包已经被正常接收。主要用来解决不丢包的问
  • 【标志字段】:
    • 【ACK】:用以指示确认字段中的值是有效的,即该报文段包括一个对已被成功接收的报文段的确认;
    • 【RST】:用以指示连接的强制拆除,当接收到错误连接时会发送RST位置为1的报文;
    • 【SYN】:用以指示连接的建立,该位为1的报文表示希望建立连接;
    • 【FIN】:用以指示连接的终止,该位为1的报文表示希望断开连接;

三、TCP建立连接(三次握手)

1. TCP三次握手的流程

  • 假设一开始客户端和服务端都处于CLOSED的状态。然后先是服务端主动监听某个端口,处于LISTEN状态;

  • 【第一个报文】:客户端会随机初始化序列号(client_isn),将此序列号置于TCP首部的序列号字段中,同时将SYN标志位置为1,表示该报文为SYN报文。接下来就将第一个SYN报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。

  • 【第二个报文】:服务端收到客户端的SYN报文后,首先服务端也随机初始化自己的序列号(server_isn),将此序号填入TCP首部的序号字段中,其次把TCP首部的确认应答号字段填入 client_isn + 1,接着把 SYN 和 ACK 标志位置为1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。

  • 【第三个报文】:客户端收到服务端报文后,还要向服务端回应最后一个报文,首先应答报文 TCP 首部 ACK 标志位置为1,其次确认应答号字段填入 server_isn + 1,最后把报文发送给服务端,这次报文可以携带客户端到服务器的数据,之后客户端处于 ESTABLISHED 状态。

  • 最后服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。

2. 为什么是三次握手

为什么TCP连接确立需要三次握手,而不是两次?还是四次?这是一个经常能被问到的问题。接下来就几个方面分析为什么需要三次握手的原因:

2.1 避免历史连接(主要原因)

The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.

上面文字出自 RFC 793,三次握手的主要原因是为了防止旧的重复连接初始化造成混乱。

网络环境是比较复杂的,它不一定能保证我们先发送的数据包就一定能再我们期望的时间内送达,它可能半路 Poor Gay 了。也有可能超时后再抵达服务端,那么这时的TCP就会产生以下的一种情况:

如上图所示,如果一个SYN报文再超时后没有得到响应,客户端可能再次发送一个新的SYN请求,而这时旧的SYN请求可能比新的SYN请求先达到服务器。如果此时没有第三次连接来确认此次连接是否是历史连接的话,那么双方可能会建立两个链接?造成数据混乱。而如果是三次连接的话,客户端就有机会再去确认或者中止掉错误的连接,防止历史连接初始化了连接

2.2 同步双方初始序列号

TCP协议通信的可靠性,是靠着“序列号”所维持的,上面我们看到,每次通信后,都会回一个ACK报文,ACK中的确认应答号都是靠着之前报文的序列号 + 1实现的,这样做有几个好处:

  • 接收方可以去除重复的数据报;
  • 接收方可以根据数据包的序列号按序接收;
  • 可以标识发送出去的数据包中, 哪些是已经被对方收到的;

可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带初始序列号的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送初始序列号给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。

四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了“三次握手”。

而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。

2.3 避免资源浪费

上面我们在介绍避免历史连接的时候,就已经提及到了如果没有三次握手,无法确认客户端收到了服务端发送的建立连接的 ACK 确认序号信号。

假如我们现在是两次握手,如果有网络阻塞等原因造成旧的连接SYN请求还没抵达服务端,就已经达到了超时时间,那么客户端就会再次发送请求SYN报文,之后如果两次请求都能达到服务端的话,由于服务端现在只有两次握手,无法确定当前的SYN就是客户端想要的连接,只能一收到 SYN 就返回一个 ACK 报文再建立起一个连接,这么做就会造成资源的浪费。看下图所示:

小结,不能使用两次握手四次握手的原因:

两次握手:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;

四次握手:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

四、TCP传输过程的控制

1. 如何提高数据传输的可靠性

从上面我们聊TCP连接的建立过程,我们可以知道,当客户端的数据达到接收主机的时候,服务端主机会返回一个已收到消息的通知。这个消息就是前面提到的应答消息(ACK)。这个消息机制具体的实现就是,每次当接收端收到对端发送过来的消息时,都会将对端消息中的序列号+1,作为自己消息发送的应答号。

TCP通过肯定的确认应答(ACK)实现可靠的数据传输。当发送端将数据发送之后等待对端的确认应答。如果有确认应答,说明数据已经成功到达对端。反之,则数据丢失的可能性很大。当在一定时间内没有收到确认应答,发送端就认为数据已经丢失,那么就会进行重新发送。由此,即使产生了丢包,仍然能够保证数据能够到达对端,实现可靠传输。这个过程如下所示:

当然还有另外一种情况,就是主机B已经收到了数据,丢失确认应答的消息在传输过程中丢失,那么此时主机A在一段时间内没有收到确认应答消息,也会认为主机B没有收到消息,从而再发送一次,如下图:

这种情况在传输并不鲜见,如果一直收到主机A发送的重复数据,对于主机B来说,它必须去放弃一些重复的包,这就需要我们上面所提到的序列号了。根据序列号判断这个数据包先前是否收到过,如果收到过就放弃,如果没有收到就保留。序列号的生成有其独特算法,这里就不做赘述了。

2. 超时重发如何确定

超时重传机制是用来确保TCP传输的可靠性的重要手段之一,我们在上面已经提及过多次:每次发送数据包时,发送的数据报都有seq号,接收端收到数据后,会回复 ACK 进行确认,表示某 seq 号数据已经收到。发送端在发送了某个seq包后,等待一段时间,如果没有收到对应的 ACK 回复,就认为该报文丢失,会重传这个数据包。中间等待的这段时间我们称为超时时间。那么如何来确定这个超时时间呢?

比较理想的方式就是定义一个固定值的最小时间,它能保证“确认应答一定能在这个时间内返回”。但是这样却有很大的弊端,因为在长距离的通信时(访问外网)时,延迟肯定就大,那么如果把值设置得太大,那么短距离的通信就很不友好了(隔了那么就才能知道这个包丢了)。如果值设置得小了,那么每次长距离访问都被判定为丢包???所以这样设置固定值是不可取的,需要根据网络的延迟,动态设置超时时间

在这里先引入两个概念:

  • RTT(Round Trip Time):往返时延,也就是数据包从发出去到收到对应 ACK 的时间。RTT 是针对连接的,每一个连接都有各自独立的 RTT。
  • RTO(Retransmission Time Out):重传超时,也就是前面说的超时时间。

TCP经典算法RTT是:R <- αR + (1-α)M,重传时间为: RTO=Rβ

// 挖坑,以后再写,这里要写的话太长了。

此外,数据也不会被无限次、反复地发送。达到一定重发次数后,如果仍然没有任何确认应答返回,就会判断为网络或者对端主机发生了异常,强制关闭连接。并且通知应用通信异常强行终止。

3. 利用窗口控制提高速度

通过上面我们知道,TCP通过确认机制来保证数据传输的可靠性,在比较早的时候使用的是send-wait-send的方式,也称为stop-wait的方式,就是我们上面提及的方式。发送数据的一方在发送数据的时候会启动定时器,但是如果数据或者ACK丢失,那么定时器到期之后,收不到ACK就认为发送出现状况,要进行重传。这样就会降低了通信的效率,如下图所示,这种方式被称为 positive acknowledgment with retransmission (PAR)

由此我们引入了滑动窗口的概念,笼统讲就是我每次发送的包都有一个序列号,接收端必须对每一个包进行确认,这样主机 A一次可以多发送几个包,而不必等待ACK后再发送,同时接收端也要告知发送端它能够收多少个包,这样发送端发起来也有个限制。当然还需要保证顺序性,不要乱序,对于乱序的状况,我们可以允许等待一定情况下的乱序,比如先缓存提前到的数据,然后去等待需要的数据,如果一定时间没来就DROP掉,来保证顺序性。

3.1 数据类别

在TCP协议中,滑动窗口的引入解决了上述问题,接下来我们进一步了解滑动窗口的相关概念,首先发送端数据分为以下几个类别:

  • Sent and Acknowledged:这些数据表示已经发送成功并已经被确认的数据,比如图中的前31个bytes,这些数据其实的位置是在窗口之外了,因为窗口内顺序最低的被确认之后,要移除窗口,实际上是窗口进行合拢,同时打开接收新的带发送的数据

  • Send But Not Yet Acknowledged:这部分数据称为发送但没有被确认,数据被发送出去,没有收到接收端的ACK,认为并没有完成发送,这个属于窗口内的数据。

  • Not Sent,Recipient Ready to Receive:这部分是尽快发送的数据,这部分数据已经被加载到缓存中,也就是窗口中了,等待发送,其实这个窗口是完全有接收方告知的,接收方告知还是能够接受这些包,所以发送方需要尽快的发送这些包

  • Not Sent,Recipient Not Ready to Receive: 这些数据属于未发送,同时接收端也不允许发送的,因为这些数据已经超出了发送端所接收的范围。

对于接收端也是有一个接收窗口的,类似发送端,接收端的数据有3个分类,因为接收端并不需要等待ACK所以它没有类似的接收并确认了的分类,情况如下:

  • Received and ACK Not Send to Process:这部分数据属于接收了数据但是还没有被上层的应用程序接收,也是被缓存在窗口内

  • Received Not ACK:已经接收并,但是还没有回复ACK,这些包可能输属于Delay ACK的范畴了

  • Not Received:有空位,还没有被接收的数据。

3.2 发送窗口与可用窗口

对于发送方来讲,窗口内的包括两部分,就是发送窗口(已经发送了,但是没有收到ACK),可用窗口,接收端允许发送但是没有发送的那部分称为可用窗口。

  • Send Window: 20 个bytes 这部分值是有接收方在三次握手的时候进行通告的,同时在接收过程中也不断的通告可以发送的窗口大小,来进行适应;

  • Window Already Sent: 已经发送的数据,但是并没有收到ACK。

3.3 滑动窗口原理

TCP并不是每一个报文段都会回复ACK的,可能会对两个或者多个报文段回复一个ACK【累计ACK】。比如说发送端有1、2、3 共计3个报文段,先发送了2,3两个报文段,但是接收方还期望收到1报文段,这个时候2,3报文段就只能放在缓存中等待报文1的空洞被填上,如果报文1,一直不来,报文2,3也将被丢弃,如果报文1来了,那么会发送一个ACK对这3个报文进行一次确认。接下来举一个例子来说明滑动窗口的原理:

举一个例子来说明一下滑动窗口的原理:

  1. 假设32~45 这些数据,是上层 Application 发送给 TCP 的,TCP将其分成四个 Segment 来发往 Internet;

  2. seg1 32~34, seg2 35~36, seg3 37~41,seg4 42~45 这四个片段,依次发送出去,此时假设接收端之接收到了seg1 seg2 seg4;

  3. 此时接收端的行为是回复一个ACK包说明已经接收到了32~36的数据,并将 seg4 进行缓存(保证顺序,产生一个保存 seg3 的 hole);

  4. 发送端收到ACK之后,就会将32~36的数据包从发送并没有确认切到发送已经确认,提出窗口,这个时候窗口向右移动

  5. 假设接收端通告的Window Size仍然不变,此时窗口右移,产生一些新的空位,这些是接收端允许发送的范畴;

  6. 对于丢失的seg3,如果超过一定时间,TCP就会重新传送(重传机制),重传成功会seg3,seg4一块被确认,不成功,seg4也将被丢弃;

就是不断重复着上述的过程,随着窗口不断滑动,将真个数据流发送到接收端,实际上接收端的 Window Size 通告也是会变化的,接收端根据这个值来确定何时及发送多少数据,从对数据流进行流控。原理图如下图所示:

3.4 滑动窗口动态调整

主要是根据接收端的接收情况,动态去调整Window Size,然后来控制发送端的数据流量

  1. 客户端不断快速发送数据,服务器接收相对较慢,看下实验的结果;
  2. 包175,发送ACK携带WIN = 384,告知客户端,现在只能接收384个字节;
  3. 包176,客户端果真只发送了384个字节,Wireshark 也比较智能,也宣告TCP Window Full;
  4. 包177,服务器回复一个ACK,并通告窗口为0,说明接收方已经收到所有数据,并保存到缓冲区,但是这个时候应用程序并没有接收这些数据,导致缓冲区没有更多的空间,故通告窗口为0, 这也就是所谓的零窗口,零窗口期间,发送方停止发送数据;
  5. 客户端察觉到窗口为0,则不再发送数据给接收方;
  6. 包178,接收方发送一个窗口通告,告知发送方已经有接收数据的能力了,可以发送数据包了;
  7. 包179,收到窗口通告之后,就发送缓冲区内的数据了。

总结一点,就是接收端可以根据自己的状况通告窗口大小,从而控制发送端的接收,进行流量控制。

五、TCP终止连接(四次挥手)

了解完TCP连接的建立以及传输过程中的一些知识点后,再来了解TCP连接的终止就比较容易了。TCP连接中的双方都可以主要断开连接,断开连接后的主机中的资源将被释放,接下来我们先来了解终止过程的大概,如下图:

1. TCP四次挥手的过程

  • 现在客户端与服务端都处在连接建立的状态,假设此时客户端想要关闭连接;

  • 【第一个报文】:客户端发送一个 FIN 报文,用来关闭客户端到服务端的数据传送,也就是客户端告诉被服务端:我已经不会再给你发数据了(当然,在 FIN 包之前发送出去的数据,如果没有收到对应的 ACK 报文,客户端依旧会重发这些数据),但此时客户端还可以接受数据;

  • 【第二个报文】:服务端收到 FIN 报文后,发送一个 ACK 给对方,确认序号为收到序号 + 1,此时服务端进入 CLOSED_WAIT 状态。客户端接收到 ACK 报文后,进入 FIN_WAIT_2 状态;

  • 【第三个报文】:服务端发送一个 FIN 报文,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了,接下来服务端进入 LAST_ACK 状态;

  • 【第四个报文】:客户端收到 FIN 报文之后,发送一个 ACK 给服务端,确认应答号为收到序号 + 1,此时客户端进入 TIME_WAIT 状态;

  • 服务端收到 FIN 报文后,就直接进入了 CLOSED 状态,连接资源将被释放,四次挥手到此结束;

  • 客户端在经过 2MSL 时间后,自动进入CLOSED 状态,至此客户端也完成了连接的关闭。

2. 为什么需要四次挥手

再来回顾下四次挥手双方发 FIN 包的过程,就能理解为什么需要四次了:

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
  • 服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手导致多了一次。

3. 为什么需要 TIME_WAIT

3.1 防止旧连接的数据包

这个过程比较容易理解,我们看下图先:

如果没有TIME_WAIT这个等待时间的话,像上图中由于网络延迟的消息,就有可能在下次连接建立的时候重新发送到接收端。因此TCP设计出在关闭连接时还需要等待 2 MSL这个时间,来确定是否还有后续的数据报发送过来,之后再关闭连接。

3.2 保证连接的正确关闭

TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.

也就是说,TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。

假设 TIME-WAIT 没有等待时间或时间过短,断开连接会造成什么问题呢?

从上面我们可以了解到,如果没有TIME_WAIT这段等待时间,那么客户端发送的 ACK应答包可能在网络中丢失,此时由于 TIME_WAIT 过短或者没有,那么客户端会直接进入 CLOSE 状态,而服务端却一直在 LAST_ACK 状态下等待。那么下一次连接的建立可能就无法成功。

如果TIME_WAIT的时间足够长,那么此时上面这种情况就得以解决:

  • 服务端正常收到四次挥手的最后一个 ACK 报文,则服务端正常关闭连接。
  • 服务端没有收到四次挥手的最后一个 ACK 报文时,则会重发 FIN 关闭连接报文并等待新的 ACK 报文。

4. TIME_WAIT 为什么是 2 MSL

MSL 是 Maximum Segment Lifetime 的缩写,译为报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。

MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。

TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间

比如如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 Fin 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。

2 MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME_WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2 MSL 时间将重新计时

在 Linux 系统里 2 MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒

本文绝大多数资料来源网上,只是为了自己加深理解才写篇博客整理总结,若是哪里有不足之处请评论区指出。

这篇文章只是简单的TCP协议入门,后续会继续写相关的文章。

参考资料:

posted @ 2020-12-09 21:47  周二鸭  阅读(9289)  评论(1编辑  收藏  举报