TCP 三次握手 & 四次挥手

TCP 报文段格式

先直接给出 TCP 报文段的格式,如下图所示:(这里只介绍本章会用到的字段,图中有颜色标注出来的字段!!)

1

端口号:由于 TCP 提供端到端的连接,计算机中可以通过端口号唯一确定一个进程,所以 TCP 报文段的前 4 个字节是源端口号和目的端口号

序列号:在建立连接时由计算机生成随机数作为初始序列号,通过 SYN 包发送给接收端,每发送一次数据就将序列化累加一次该包中「数据字节数」的大小,用来解决网络包乱序的问题

确认应答号:指下一次期望收到数据的序列号,发送端收到这个确认应答号后可以认为这个序列号之前的数据都已经被正常接收,用来解决网络丢包的问题

举例:假设客户端初始序列号都是 0,第一次发送数据到服务器,其中:Seq = 0,数据大小为 520;服务器收到数据后,发送 ACK 包给客户端,其中:ack = 520,表示下一次期待收到序列号位 520 的包,并且序列号 520 之前的数据 ([0, 519]) 都已经正常接收

控制位:用来表示该 TCP 数据报的类型,每个控制位的大小为 1 bit

问题:TCP 报文段中有校验和字段,主要用于判断数据有无出错,也就是用于差错控制,如果判断有问题直接丢弃该包,但在链路层会添加帧尾,其中包含 CRC 校验,也用于差错控制,那么这两者有何区别?

首先当帧离开网卡后就进入物理链路中,帧尾的 CRC 校验主要用于判断在物理链路的传输中有无产生差错;而到达另一个主机的链路层后会向上传递给网络层,网络层的一些操作可能会使报文出错,所以需要在传输层再次判断是否存在差错

更详细解释可见 在计算机网络中数据链路层和传输层都有流量控制和差错控制他们有什么区别?又为啥需要在不同层设置相同功能?

TCP 协议

TCP 是传输层协议,具有三个特点:

注意:TCP 报文会拆分;而 UDP 报文不会拆分,如果一条 UDP 报文长度超过 MSS,会在网络层进行拆分,在目的主机的网络层会拼装成完整 UDP 报文,然后才向上发送给传输层

问题:为什么 TCP 报文拆分,UDP 报文不拆分??

一般而言 TCP 报文较长,又要保证可靠传输,如果不拆分出现差错需要重传整条 TCP 报文,开销过大,将 TCP 报文拆分后只需要重传出错的部分即可;而 UDP 报文较短,整条报文重传开销也不算很大

UDP 和 TCP 的区别

UDP 是面向无连接、尽最大努力交付的数据传输服务,无法保证可靠,传输单位是用户数据报。UDP 报文头部固定 8 个字节,而 TCP 报文头部存在可变选项,在不算可变选项前提下 TCP 报文头部 20 字节

UDP 报文头部格式如下图所示:

2

端口号:虽然 UDP 不保证可靠传输,但依旧是端到端的通信,所以需要源端口号和目的端口号

包长度:该字段记录了 UDP 首部长度和数据长度之和

校验和:校验和是为了提供可靠的传输而设计的,虽然 UDP 不要求保证可靠,但也可以设计成可靠

UDP 和 TCP 的区别

连接

服务对象

可靠性

拥塞控制、流量控制

首部开销

传输方式

分片

UDP 和 TCP 的应用场景

由于 UDP 面向无连接,可以随时随地发送数据,再加上 UDP 本身的处理简单高效,所以常用于:

由于 TCP 面向连接,提供可靠的数据传输,所以常用于:

问题一:为什么 UDP 首部没有首部长度,而 TCP 首部有首部长度字段?

UDP 首部固定为 8 个字节,而 TCP 首部有选项字段,长度不固定

问题二:UDP 和 TCP 可以使用同一个端口吗?

由于端口号是传输层用来唯一确定主机中的应用进程,UDP 和 TCP 有不同的处理模块,就算 UDP 和 TCP 使用同一个端口,也可以使用协议类型来区分报文属于哪个应用进程

TCP 三次握手过程

TCP 是面向连接的协议,在通信前需要通过三次握手建立连接,三次握手的过程如下图所示:

4

三次握手的详细过程:

注意一:客户端发送第三次握手后,不管服务器有没有收到,可以直接发送数据

注意二:前两次握手过程不可以携带数据,第三次握手过程可以携带数据

注意三:如果没有服务器没有收到第三次握手,对于客户端后续发送的数据,一般会包含 ACK,所以可以当作第三次握手。这也是 TCP 规定除了第一次握手的 SYN 包外,其余所有包 ACK 必须为 1 的原因

更详细解释可见 TCP第三次握手能携带数据吗?做个实验就知道!

为什么是三次握手

为什么是三次握手,而不是两次握手或四次握手?主要有三个原因!!

原因一:避免历史连接

三次握手的首要原因就是为了避免历史重复连接初始化造成混乱!!

下面模拟一种场景:客户端先发送了 SYN (Seq = 90) 报文,然后客户端宕机,而且这个 SYN 报文还被网络阻塞,服务器并没有收到,接收客户端重启后又重新建立连接,发送 SYN (Seq = 100) 报文

上面场景的三次握手过程如下图所示:

5

从上图可以看到,由于服务器需要收到客户端的第三次握手才会分配资源建立连接,但客户端收到服务器对历史连接的 SYN-ACK 报文后会发送一个 RST 报文中止连接,避免了服务器分配资源建立连接

如果只有两次握手,那么过程如下图所示:

6

区别:两次握手会先分配资源建立连接,后收到 RST 报文后中止连接,这样无疑白白增加了不必要的初始化开销!!

原因二:同步双方初始序列号

TCP 需要保证可靠传输,具体包括:数据可以无差错、不丢失、不重复、按序到达,可以根据序列号来判断报文的顺序以及是否重复

客户端和服务器在建立连接前都会随机初始化序列号,所以需要让双方知道对方的初始化序列号,才可以保证后续数据的不重复,按序到达

两次握手只能保证服务器知道客户端的初始序列号,但无法保证客户端知道服务器的初始序列号

问题:为什么客户端和服务器都需要随机初始化序列号,直接初始化为 0 不行吗?

每次建立连接时都需要随机初始化序列号,主要是为了:

原因三:避免资源浪费

假设客户端第一次发送的 SYN 报文由于网络阻塞长时间抵达服务器,经过一段时间后客户端超时重发 SYN 报文 (Seq 和第一个相同),如果只有两次握手,那么就有两次分配资源建立连接的过程

如果只有两次握手,那么过程如下图所示:

7

注意:宕机重启后重发的 SYN 报文序列号会发生变化;超时重发的 SYN 报文序列号不会发生变化

问题:为什么超时重发旧的连接不会被客户端识别到,然后发送 RST 报文中止连接?

由于只有两次握手,客户端会将旧的 SYN 报文认为是服务器重发报文,因为两个报文的序列号是一样的,所以不会发送 RST 报文中止连接

握手丢失

第一次握手丢失

当客户端发送第一次握手后会处于 SYN_SEND 状态。如果第一次握手丢失,那么客户端会超时重发第一个 SYN 包,序列号相同

超时重发规则:内核会通过参数控制重发次数,默认为 5。第一次超时重发在 1s 后,第二次超时重发在 2s 后,第三次超时重发在 4s 后,第四次超时重发在 8s 后,第五次超时重发在 16s 后。第五次超时重发后会等待 32s,如果 32s 内依旧没有收到服务器的响应就会断开连接

第二次握手丢失

对于客户端来说可能认为是自己发送的第一次握手服务器没有收到,就会按照第一次握手丢失的情况超时重发报文

对于服务器来说可能认为是自己发送的第二次握手客户端没有收到,就会按照相同的规则重发第二次握手

第三次握手丢失

对于客户端来说不会重发 ACK 报文,而是由服务器重发第二次握手报文

TCP 四次挥手过程

TCP 是面向连接的协议,在通信结束前需要通过四次挥手断开连接,四次挥手的过程如下图所示:

8

四次挥手的详细过程:

问题:为什么是四次挥手?

首先彻底断开连接必须在双方都处理完数据的基础上,任意一方断开连接都不算真正的断开连接,只有当双方都主动断开连接后才算真正断开连接

一般断开连接都是客户端主动发起,它会向服务器发送一个 FIN 包,表示自己已经处理完所有数据,可以断开连接

当服务器收到 FIN 包后可能还需要处理剩余数据,当处理完剩余数据后向客户端发送 FIN 包表示自己也可以断开连接

挥手丢失

第一次挥手丢失

当客户端发送第一次挥手后会处于 FIN_WAIT_1 状态。如果第一次挥手丢失,那么客户端会超时重发第一个 FIN 包

超时重发规则:内核会通过参数控制重发次数,假设为 5。第一次超时重发在 1s 后,第二次超时重发在 2s 后,第三次超时重发在 4s 后,第四次超时重发在 8s 后,第五次超时重发在 16s 后。第五次超时重发后会等待 32s,如果 32s 内依旧没有收到服务器的 ACK 包就会断开连接

第二次挥手丢失

第二次挥手是服务器向客户端发送 ACK 包,而 ACK 包是不会重发,所以如果第二次挥手丢失,客户端会认为是自己发送的第一次挥手服务器没有收到,就会按照第一次挥手丢失的情况超时重发报文

第三次挥手丢失

第三次挥手丢失类似于第一次挥手丢失,服务器会超时重发 FIN 包

第四次挥手丢失

第四次挥手丢失类似于第二次挥手丢失,服务器会超时重发第三次挥手的 FIN 包

几个问题

问题一:为什么 TIME_WAIT 等待时间是 2MSL?

MSL,全称 Maximum Segment Lifetime,表示报文最大生存时间,在网络中任何报文超过该时间都将会被丢弃,Linux 将 MSL 设置为 30s

客户端处于 TIME_WAIT 状态后需要等待 2MSL 后才关闭,主要是为了服务器也可以顺利关闭连接。当第四次握手丢失,服务器会超时重传一个 FIN 报文,最坏情况下只需要 2MSL 客户端就可以重新收到服务器重发的 FIN 报文,然后重新发送 ACK 包给服务器,此时 TIME_WAIT 等待时间重新计算。也就是说客户端可以允许一次报文丢失

至于不把等待时间设置为更长,是因为一次丢包的概率是 1/100,连续两次丢包的概率是 1/10000,概率太小,忽略它更具有性价比

问题二:为什么需要 TIME_WAIT 状态?

主要有两个原因:

问题三:TIME_WAIT 过多有什么危害?

主要有两个原因:

问题四:服务器出现大量 TIME_WAIT 的原因?

只有主动关闭的一方才会出现 TIME_WAIT,服务器出现大量的 TIME_WAIT 说明服务器主动关闭了连接,主要有三个原因:

问题五:服务器出现大量 CLOSE_WAIT 的原因?

只有被动关闭的一方才会出现 CLOSE_WAIT,服务器出现大量的 CLOSE_WAIT 说明服务器卡在了发送第三次挥手报文,也就是服务器程序没有调用close函数关闭连接,只有客户端单方面关闭连接