前置芝士
在讲解 TCP 三次握手和四次挥手之前,我们先来简单的讲解 TPC 的基础知识,这样更有助于我们后面的理解。
首先我们得知道什么是 TCP ?
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议,中文叫传输控制协议。下面简单的解释一下:
- 面向连接:一定是一对一才能连接,不能像 UDP 协议 可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的。
- 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端。
- 字节流:消息是没有边界的,所以无论我们消息有多大都可以进行传输。并且消息是有序的,当前一个消息没有收到的时候,即使它先收到了后面的字节已经收到,那么也不能扔给应用层去处理,同时对重复的报文会自动丢弃。
TCP 一般用于文件传输、发送和接收邮件、远程登录等场景
那为什么需要 TCP 呢?
由于 IP 层是不可靠的,它只是尽力的去传输,不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。(什么,还不懂 IP ,等我 🤪🤪🤪)
如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP
协议来负责。因为 TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。
下面我们看看分层模型:
在讲解 TCP 连接之前,我们还需要知道 TCP 报文的头格式,接下来我们就看看 TCP 报文头:
下面我们简单的介绍一下我们下面会用到的字段,(其他的字段如果不懂的话,等我 🤪🤪🤪)
序列号(Sequense Number,SN):在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就累加一次该数据字节数的大小,用来解决网络包乱序问题。
确认应答号:指下一次期望收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收,用来解决不丢包的问题。
控制位:
ACK:该位为
1
时,确认应答的字段变为有效,TCP 规定除了最初建立连接时的SYN
包之外该位必须设置为1
。RST:该位为
1
时,表示 TCP 连接中出现异常必须强制断开连接。SYN:该位为
1
时,表示希望建立连,并在其序列号的字段进行序列号初始值的设定。FIN:该位为
1
时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN
位置为 1 的 TCP 段。
我们暂且先了解上面的这些字段的意思,对后面的阅读很有作用,这里我们简单的介绍一下 TCP 和 UDP 区别:
用户数据报协议 UDP(User Datagram Protocol):
- UDP 在传送数据之前不需要先建立连接,远程主机在收到 UDP 报文后,不需要给出任何确认,而且 UDP 支持一对一、一对多、多对多的交互通信。
- UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如:QQ 语音、 QQ 视频 、直播等等。
- 首部字段不一样,UDP 的首部字段很简单,只有 8 字节并且是固定不变的,开销较小。
TCP 三次握手
我们首先讲一下什么是 TCP 连接:
简单来说就是,用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。这个概念可能不是很懂,下面我们来讲解。
三次握手过程详解
TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手而进行的。三次握手意思就是需要三个步骤才能建立连接的机制,关于为什么需要三次握手我们后面慢慢讲解,先来看看三次握手的步骤吧。
总体过程如下图所示:
一开始,客户端和服务端都处于 CLOSED
状态,先是服务端主动监听某个端口,处于 LISTEN
状态。
1)第一次握手:
客户端向服务端发送一个 SYN 报文(SYN = 1),并指明客户端的初始化序列号 ISN(x),即图中的 seq = x,表示本报文段所发送的数据的第一个字节的序号。此时客户端处于 SYN_Send
状态。
SYN-SENT
:在发送连接请求后等待匹配的连接请求时的状态
此时,这个报文头如下:
注意:此时是没有携带数据的,也不可以携带数据,但是序列号还是会加1。
2)第二次握手:
服务器收到客户端的 SYN 报文之后,会发送 SYN 报文作为应答(SYN = 1),并且指定自己的初始化序列号ISN(y),即图中的 seq = y。同时会把客户端的 ISN + 1 作为确认号 ack 的值,表示已经收到了客户端发来的的 SYN 报文,希望收到的下一个数据的第一个字节的序号是 x + 1,此时服务器处于 SYN_REVD
的状态。
SYN_REVD
:在收到和发送一个连接请求后等待对连接请求的确认。
此时,这个报文头如下:
第二次握手也是不能携带数据的,序列号加1。
3)第三次握手:
客户端收到服务器端响应的 SYN 报文之后,会发送一个 ACK 报文,也是一样把服务器的 ISN + 1 作为 ack 的值,表示已经收到了服务端发来的的 SYN 报文,希望收到的下一个数据的第一个字节的序号是 y + 1,并指明此时客户端的序列号 seq = x + 1(初始为 seq = x,所以第二个报文段要 +1,虽然没有数据,但是还是要 + 1),此时客户端处于 Establised
状态。
服务器收到 ACK 报文之后,也处于 Establised 状态
,至此,双方建立起了 TCP 连接。
ESTABLISHED
:代表一个打开的连接,数据可以传送给用户
此时,这个报文头如下:
从图中我们可以看到,第三次握手是可以携带数据的,前两次握手是不能携带数据的。
相信你看了上面的过程肯定会有如下的疑问:(如果没有当我没说 🤡🤡🤡)
握手过程中可以携带数据吗?
上面说了只有第三次握手是可以携带数据的,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,然后疯狂重复发 SYN 报文的话(因为攻击者根本就不用管服务器的接收、发送能力是否正常,它就是要攻击你),这会让服务器花费很多时间、内存空间来接收这些报文。
简单的记忆就是,请求连接/接收 即
SYN = 1
的时候不能携带数据的。
而对于第三次的话,此时客户端已经处于 ESTABLISHED
状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以当然能正常发送/携带数据了。
为什么客户端和服务端的初始序列号 ISN 是不相同的?
三次握手的其中一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number),以便让对方知道接下来接收数据的时候如何按序列号组装数据。当一端为建立连接而发送它的 SYN 时,它会为连接选择一个初始序号。ISN 随时间而变化,因此每个连接都将具有不同的 ISN。如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。
连接放到哪里了?
服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD
状态,此时双方还没有完全建立其连接,服务器会把这种状态下的请求连接放在一个队列里,我们把这种队列称之为半连接队列。
当然还有一个全连接队列,完成三次握手后建立起的连接就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
如果发送的包丢失了这么办?
假如服务器发送完 SYN-ACK 包,未收到客户端响应的确认包,也即第三次握手丢失。那么服务器就会进行首次重传,若等待一段时间仍未收到客户确认包,就进行第二次重传。如果重传次数超过系统规定的最大重传次数,则系统将该连接信息从半连接队列中删除。(每次重传的时间可能不一样,一般会是指数增长)
为什么需要三次握手?
我相信这个问题已经是老生常谈的问题了,大家比较常回答的是:“因为三次握手才能保证双方具有接收和发送的能力。” 虽然这个回答没有问题,但是是片面的,没有说出根本原因。主要是下面的这三个原因:
- 三次握手才可以阻止历史重复连接的初始化(主要原因)
- 三次握手才可以同步双方的初始序列号
- 三次握手才可以避免资源浪费
1)避免历史连接
简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。下面举个例子:
如果客户端认为第一次握手的报文丢了(可能是由于网络阻塞等情况超时了),就会重新发送一个 SYN 报文,但是如果第一次发送的比第二次发送的报文先到达,那么就会出现如下的问题,看图:
上图中正是由于三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接,如果是两次握手,就不能判断当前连接是否是历史连接。
2)同步双方初始序列号
当客户端发送携带初始序列号的 SYN
报文的时候,需要服务端回一个 ACK
应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送初始序列号给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
3)避免资源浪费
如果只有两次握手,当客户端的 SYN
请求连接在网络中阻塞,客户端没有接收到 ACK
报文,就会重新发送 SYN
,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK
确认信号,所以每收到一个 SYN
就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端的 SYN
阻塞了,重复发送多次 SYN
报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
即两次握手会造成消息滞留情况下,服务器重复接受无用的连接请求 SYN
报文,而造成重复分配资源。
至于为啥不是四次握手:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
到这里我们的三次握手就讲解完了,下面我们继续看四次挥手。
TCP 四次挥手
当客户端和服务端没有需要传输的数据的时候,就需要断开连接,TCP 断开连接是通过四次挥手方式。双方都可以主动断开连接,断开连接后主机中的资源将被释放。
至于为什么需要四次挥手,且听我慢慢道来:
建立一个 TCP 连接需要三次握手,而终止一个 TCP 连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这是由于 TCP 的半关闭(half-close)特性造成的,TCP 提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
主要过程如下:
刚开始双方都处于ESTABLISHED
状态,假设是客户端先发起关闭请求。四次挥手的过程如下:这里我就不将各阶段的 TCP 头画出来了。
1)第一次挥手:客户端发送一个 FIN 报文(请求连接终止:FIN = 1),报文中会指定一个序列号 seq = u。并停止再发送数据,主动关闭 TCP 连接。此时客户端处于 FIN_WAIT1
状态,等待服务端的确认。
FIN-WAIT-1
- 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;
2)第二次挥手:务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT
状态。
CLOSE-WAIT
- 等待从本地用户发来的连接中断请求;
此时的 TCP 处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2
(终止等待 2)状态,等待服务端发出的连接释放报文段。
FIN-WAIT-2
- 从远程TCP等待连接中断请求;
3)第三次挥手:如果服务端也想断开连接了(没有要向客户端发出的数据),和客户端的第一次挥手一样,发送 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK
的状态,等待客户端的确认。
LAST-ACK
- 等待原来发向远程TCP的连接中断请求的确认;
4)第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答(ack = w+1),且把服务端的序列值 +1 作为自己 ACK 报文的序号值(seq=u+1),此时客户端处于 TIME_WAIT
(时间等待)状态。
TIME-WAIT
- 等待足够的时间以确保远程TCP接收到连接中断请求的确认;
我相信你看了上面的四次挥手肯定和我一样有下面的疑问,如果没有当我没说 🤡🤡🤡。
为什么需要四次握手呢?
回顾下四次挥手双方发 FIN
包的过程,就能理解为什么需要四次了。
- 关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。 - 服务器收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK
和 FIN
一般都会分开发送,从而比三次握手多了一次。
为什么 TIME_WAIT 等待的时间是 2MSL?
我们首先解释一下 MSL 是啥?
MSL
是报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
TIME_WAIT 等待 2 倍的 MSL是因为如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 Fin 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。
2MSL
的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
好啦,我们的三次握手和四次挥手就讲完了,大家慢慢消化吧
引用: