文章目录
- 一、引言
- 二、TCP
- 1、TCP 的数据格式
- 2、TCP 的三次握手
- 3、TCP 的四次挥手
- 4、TCP 的全双工通信
- 三、TCP 的状态转换
- 1、TCP 连接的建立(三次握手)状态
- 2、TCP 连接的终止(四次挥手)状态
- 3、TCP 异常情况
一、引言
TCP与UDP的区别相当大。它充分地实现了数据传输时各种控制功能,可以进行丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。而这些在UDP中都没有。此外,TCP作为一种面向有连接的协议,只有在确认通信对端存在时才会发送数据,从而可以控制通信流量的浪费(由于UDP没有连接控制,所以即使对端从一开始就不存在或中途退出网络,数据包还是能够发送出去。(当ICMP错误返回时,有时也实现了不再发送的机制。)) 。根据TCP的这些机制,在IP这种无连接的网络上也能够实现高可靠性的通信。
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
所谓“字节流”是指一种不间断的、顺序传输的数据结构,可以把它比作水管中的水流。在TCP通信中,虽然发送的数据可以保证按顺序到达接收方,但这些数据会以连续的方式流动,没有明确的分隔。例如,当发送端的应用程序使用TCP发送10次100字节的消息,接收端的应用程序可能会一次性收到一个1000字节的连续数据,而不会以独立的100字节消息形式显示。
因此,在TCP通信中,为了确保接收端能够正确理解每一条消息的边界,发送端的应用程序通常需要在消息中设置一个表示长度或分隔的字段。这个字段用于告诉接收端如何拆分和解析数据流,确保每一条消息能被正确解读。这种机制对于确保TCP数据流中的完整性和消息边界识别尤为重要。
TCP 的特点:
- 面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
- 可靠的:基于校验和应答重发机制保证传输的可靠性。无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
- 字节流:传输的数据是无结构的字节流,基于字节流的服务没有字节序问题的困扰。用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃。
- 全双工传输:各主机 TCP 协议以全双工的方式进行数据流交换。
- 缓存传输:缓冲传输可以延迟传送应用层的数据,允许将应用程序所需要传送的数据积攒到一定数量才进行集中的发送。
- 流量控制:TCP协议的滑动窗口机制,支持主机间的端到端的流量控制。
如何理解传输层对数据的封装和解包?内核如何处理?
操作系统内可以同时存在多个收到的报文(这个报文还没来得及处理,可能刚拷贝到传输层的接收缓冲区,甚至还可能没有交给传输层而在数据链路层)。
OS内一定会收到大量还没来得及处理的报文。那么OS要不要对这些的暂未处理的,但是已经收到的报文进行管理?要。如何管理?对其进行抽象,抽象为数据结构。
在操作系统中,处理网络数据的关键部分是网络层和传输层的协议栈。对于UDP和TCP等协议,接收到的网络报文在处理之前需要临时存储和管理。这是通过一个称为sk_buff
(socket buffer)的结构来实现的。
sk_buff
是 Linux 内核中处理网络数据包的核心组件。它提供了一种统一的方式来存储和管理网络数据包,使得不同层次的网络协议栈可以高效地协同工作。它不仅管理实际的数据,还管理所有相关的元数据,使得内核能够正确地处理、转发、路由和传输网络数据包。
-
传输层对数据的封装和解包
- 封装:在传输层(TCP或UDP),应用层的数据被分段或打包,附加上传输层的头部信息。TCP会加入序列号、确认号等信息,确保数据可靠传输;UDP则直接附加简单的头部,进行快速、无连接的传输。最终,封装后的“数据段(TCP)”或“数据报(UDP)”会被传递到网络层。
- 解包:当数据到达目标设备时,数据包会逆向通过各层协议。传输层负责去掉传输层的头部,处理相应的传输层任务(如重新排序、流量控制等),并将解包后的数据交给应用层。
-
内核如何处理数据包:在操作系统内核中,网络数据的处理是通过网络协议栈完成的,关键步骤包括:
-
数据包的接收:当数据报文从网络接口到达操作系统时,它首先会被临时存储在内存中,还没来得及处理的数据会在较低层(数据链路层)等待。
-
管理大量未处理的报文:由于操作系统可能同时收到大量的数据报文,它需要对这些未处理的数据进行有效管理。Linux内核使用一种叫做**
sk_buff
**(socket buffer)的数据结构来管理这些报文。 -
sk_buff
的作用:sk_buff
是Linux内核中的核心数据结构,用于存储和管理从网络接口接收到的每一个数据包。它不仅存储实际的数据,还包含元数据(如数据包的长度、协议类型等),以便在各个网络协议栈层之间协同工作。通过sk_buff
,内核可以有效地处理、转发、路由和传输数据包。 -
多层处理:
sk_buff
帮助操作系统内核将数据包传递给适当的传输层协议(如TCP、UDP),然后在传输层进一步处理,最终交给应用程序。
-
-
也就是说,Linux内核使用sk_buff
来管理未处理的数据包,确保协议栈的不同层次能够高效协同工作,处理大量的网络流量。sk_buff
(Socket Buffer)结构体作为 Linux 内核网络栈中用于管理和处理网络数据包的核心数据结构。我们结合 sk_buff
理解解包和分用的过程:
- 解包:当一个网络数据包被接收到时,它被封装在一个
sk_buff
结构中。网络栈使用sk_buff
中的指针(如network_header
和transport_header
)来解析不同协议层的头部,并提取相应的信息。 - 分用:
sk_buff
中存储的头部信息(如 IP 地址、协议类型、端口号)被用来确定数据包的目的地。根据这些信息,系统将sk_buff
传递给对应的处理函数或应用程序。
它通常在以下几种情况下形成:
- 数据包接收时:当网络接口卡(NIC)接收到一个数据包时,NIC 的驱动程序会创建一个
sk_buff
结构来保存该数据包的数据和元数据。这个过程通常包括从 NIC 的接收缓冲区复制数据到sk_buff
结构中。 - 数据包发送时:当应用程序通过系统调用(如
sendto
或send
)发送数据时,传输层协议(如 TCP 或 UDP)会创建一个或多个sk_buff
结构来保存数据。随后,这些sk_buff
被传递到网络层,最终通过 NIC 发送出去。
二、TCP
1、TCP 的数据格式
TCP 在 IP 协议的基础上进行传输数据,TCP数据的报文格式如下:
源端口号和目的端口号不再赘述。点击此处理解端口号
序列号(Sequence Number):32位,表示分配给TCP包的编号。序列号用于标志应用程序从 TCP 的发送端到接收端发送的字节流。当 TCP 开始连接的时候,发送一个序列号给接收端,连接成功后,这个序列号作为初始序列号(ISN,Initial Sequence Number)。建立连接成功后将按照字节的大小进行递增。用于数据包的排序和数据的重组。
- 当TCP开始建立连接时,客户端和服务端需要交换初始序列号。这个过程通过所谓的“三次握手”来完成:
- 第一次握手:客户端向服务端发送一个SYN报文段,报文段中包含客户端选择的初始序列号。此时,客户端的序列号用来标记数据传输的起始位置。
- 第二次握手:服务端收到SYN报文段后,发送一个SYN_ACK报文段,确认它已收到客户端的初始序列号,同时服务端自己也生成一个初始序列号,并将其发送给客户端。
- 第三次握手:客户端收到服务端的SYN_ACK报文后,向服务端发送ACK报文,确认已收到服务端的序列号。至此,连接正式建立。
后文会详细介绍三次握手。
确认号(Acknowledgment Number):32位,仅在ACK位设置为1时有效,表示期望接收的数据段的下一个字节的序号。通过确认号,接收方告知发送方已经成功收到哪些数据。
也就是说,发送方对发送的首字节进行了编号,当接收方成功接收后,发送回接收成功的序列号+1 表示确认,发送方再次发送的时候从确认号开始。
序列号和确认号的具体使用如下:
- 序列号:
- 由发送方(例如主机A)在TCP报头中设置。
- 表示该TCP报文段中携带的数据的第一个字节的序列号。
- 序号用于确保数据的有序传输,接收方(例如主机B)可以根据序号正确地重组接收到的数据。
- 确认序号:
- 由接收方(例如主机B)在TCP报头中设置。
- 表示接收方期望从发送方收到的下一个TCP报文段的数据的第一个字节的序列号。
- 确认序号用于确认已成功接收的数据,并且告诉发送方可以发送下一个数据序列。
例如,当主机A向主机B发送数据时,它会指定一个序号。主机B在接收到数据后,会发送一个确认应答(ACK),在ACK报文中包含确认序号,以告知主机A它期望接收的下一个数据字节的序列号。
数据偏移 (Data Offset):4位,也称为头部长度,表示 TCP 报文段首部的长度,单位是4字节(32位),因此最大值是15,这意味着TCP首部最大长度可以是60字节。该字段用于定位数据部分的起始位置。
若首部长度为20字节,那么其4位字段应该是 0101。
保留字段(Reserved):6位,预留供将来使用,目前必须设置为0。
控制标志(Flags):6位,用于控制TCP连接的状态,可以多个位一起设置。提供服务的服务端在通信过程中会收到各种各样的报文或者是来自不同客户端的报文。因为TCP报文需要区分类型。不同报文的要做的工作不同(建立连接的报文、正常通信的报文、断开连接的报文)。这些标志位包括:
标志位名称 | 英文简称 | 功能描述 |
---|---|---|
紧急指针有效标志 (Urgent Pointer) | URG | 当URG位为1时,表示紧急指针字段有效,指出紧急数据的结束位置。用于传输需要优先处理的数据。 |
确认号有效标志 (Acknowledgment Number) | ACK | 当ACK位为1时,表示确认号字段有效。接收方通过该标志确认已收到的数据。 |
推送标志 (Push) | PSH | 当PSH位为1时,表示接收方应立即将数据推送到应用层,而不是缓冲。用于需要即时处理的数据传输。 |
重置标志 (Reset) | RST | 当RST位为1时,表示重置连接。通常用于异常终止连接或拒绝非法连接请求。 |
同步标志 (SYNchronize) | SYN | 当SYN位为1时,表示同步序列号,用于建立连接(三次握手的第一步)。 |
终止标志 (Finish) | FIN | 当FIN位为1时,表示发送方已经完成数据发送,请求释放连接。 |
窗口大小(Window Size):16位,表示接收方可以接收的字节数,用于流量控制。
校验和(Checksum):16位,TCP使用该字段来检查报头和数据是否在传输过程中出现错误。发送方计算校验和并附加在报头中,接收方进行验证。
紧急指针(Urgent Pointer):16位,仅在URG标志有效时使用,指出紧急数据的结束位置(或者是是紧急数据的字节的顺序编号),通常在需要高优先级传输的数据时使用。
选项(Options):可变长度,用于支持各种扩展功能。经常使用的为最大报文段大小(MSS,Maximum Segment Size),TCP 连接通常在第一个通信的报文中指明这个选项,它指明当前主机所能接受的最大报文的长度。
填充(Padding):为了使TCP报头长度是32位的倍数,报头后可能会附加一些填充值。
下面 Linux 中 TCP 头部结构的定义,用于操作系统中的TCP协议处理。它展示了TCP报头各字段在大端序和小端序环境下的定义。
根据TCP首部字段理解报头和有效载荷是如何分离的
在TCP报文段中,数据偏移量(也称为4位首部长度)是一个4位字段,它表示TCP首部的长度。数据偏移量的单位是4字节(32位),所以它的值范围是5到15(因为TCP首部的最小长度是20字节,即5个4字节单元,最大长度是60字节,即15个4字节单元)。
数据偏移量的作用
- 指示TCP首部的结束位置: 数据偏移量字段告诉我们TCP首部的长度。通过这个长度,我们可以确定TCP首部的结束位置,从而知道从哪里开始是数据部分(有效载荷)。
- 分离首部和数据: 数据偏移量实际上是TCP报文段中数据部分的起始位置。这个位置是相对于报文段的起始位置计算的。通过数据偏移量字段,接收端可以正确地从报文段中提取出TCP首部和数据部分。
假设数据偏移量字段的值是x
,那么TCP首部的实际长度就是 x * 4
字节。接收方在解析TCP报文段时,通过读取数据偏移量字段,就能确定TCP首部的长度,并从这个长度之后的位置开始读取数据部分。
例如:
-
如果数据偏移量是5,那么TCP首部长度是
5 * 4 = 20
字节,这也是没有任何选项的标准TCP首部长度。 -
如果数据偏移量是8,那么TCP首部长度是
8 * 4 = 32
字节,意味着首部包含了选项字段。
分离过程
- 读取TCP首部: 从报文段的开始位置读取
x * 4
字节的数据,这就是TCP首部的内容。 - 提取有效载荷: 在TCP首部之后的数据即为有效载荷。从报文段的
x * 4
字节位置开始读取剩余的数据,就是TCP的数据部分。
为什么 UDP 头部没有首部长度字段,而 TCP 有呢?
TCP 有可变长的选项字段,而 UDP 首部长度是不会变化的,无需多一个字节去记录 UDP 的首部长度。
2、TCP 的三次握手
TCP的三次握手用于建立一个连接。通常会发送如下场景:
1️⃣服务端必须准备好接收外来的连接。这通常通过调用socket
、bind
和listen
这三个函数来完成,我们称之为被动打开(passive open)。
2️⃣客户端通过调用connect
发起主动打开(active open)。这导致客户 TCP 发送一个 SYN
(同步)报文,它告诉服务端客户将在(待建立的)连接中发送的数据的初始序列号。通常 SYN
报文不携带数据,其所在 IP 数据报只含有一个IP首部、一个TCP首部及可能有的TCP选项。
3️⃣服务端必须确认(ACK
)客户的SYN,同时自己也得发送一个SYN
分解,它含有服务端将在该同一连接中发送的数据的初始序列号。服务端在单个报文中发送SYN
和对客户SYN
的ACK
(确认)。
4️⃣客户必须确认服务端的SYN
。
如下图所示:
上图给出的客户端序列号为 J ,服务端的初始序列号为K。ACK
中的确认号是发送这个ACK
的一端所期待的下一个序列号。因为SYN
占据一个字节的序列号空间,所以每一个SYN
的ACK
中的确定号我就是该SYN
的序列号+1。类似的,每一个FIN
的ACK
中的确认号为该FIN
的序列号+1。
建立TCP连接涉及的一系列步骤,就像使用电话系统一样。首先,
socket
函数的作用就像是确保你有一部电话可以使用。接着,bind
函数的作用是公布你的电话号码,这样其他人就能联系到你。然后,listen
函数相当于开启电话的铃声,这样当有人打电话给你时,你就能知道。connect
函数则是你根据对方的电话号码拨打电话。最后,accept
函数发生在你接听电话的那一刻。accept
函数返回的客户端标识,类似于电话的来电显示功能,它告诉你谁在给你打电话。不过,与电话系统不同的是,accept
函数是在你接听电话后才能告诉你来电者的信息,而在电话系统中,你可以在接听电话之前就看到来电者的号码。也就是说,只有处于
listen
状态下的服务端,才能接收SYN报文。accept
函数是在三次握手完成后,accept
才能获得链接(即accept
不参与三次握手)。客户端connect
函数发起了三次握手。
3、TCP 的四次挥手
TCP建立一个连接需要3个报文,终止一个连接需要4个报文。
1️⃣某一个应用进程首先调用close
,我们称该端执行主动关闭(active close)。该端的TCP是发送一个FIN报文,表示数据发送完毕。
2️⃣接收到这个FIN
的对端执行被动关闭(passive close)。这个FIN
由TCP确认。它的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程(放在已排队等待该应用进程接收任何其他数据之后),因为FIN的接收意味着接收端应用进程在相应的连接上再无额外数据可以接收。
3️⃣一段时间后,接收到这个文件结束符的应用进程将吊close
关闭它的套接字。这导致它的TCP也发送一个FIN
。
4️⃣接收这个FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN
。
由于每个分组都需要一个FIN
和ACK
,因此通常需要四个报文。
为什么是通常?
因为某些情况下,第一步的FIN会随着数据一起发送;另外,第二步和第三步发送的报文都出自执行被动关闭那一端,有可能被合并成一个报文一起发送。
如下图所示:(下图虽是客户端主动关闭,但任何一方都可以主动关闭)
类似SYN
,一个FIN
也占据1个字节的序列号空间。因此,每个FIN
的ACK确认号就是这个FIN
的序列号+1。
在第二步和第三步之间,从执行被动关闭一端到执行主动关闭的一端流动数据是可能的。我们称之为半关闭(shutdown
函数)。
当套接字被关闭时,其所在端的TCP各自发送了一个FIN
。我们在图中指出,这是由应用进程调用close
而发生的。不过应当一个Linux进程无论是否是资源终止时,所有打开的文件描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN
。
4、TCP 的全双工通信
TCP 允许通信的双方的应用进程在任何时候都能发送数据。TCP 连接的两端都设有发送缓冲区和接收缓冲区,用来临时存放双向通信的数据。在发送时,应用程序把数据发送给 TCP 的缓存后,就可以做自己的事,而 TCP 在合适的时候把数据发送出去。在接受时,TCP 把收到的缓存数据放入缓存,上层的应用程序在合适的时候读取缓存中的数据。
每一个 TCP 套接字有一个发送缓冲区,我们可以使用SO_SNDBUF
选项来更改缓冲区的大小。当某个进程调用write
时,内核从该应用进程的缓冲区中复制所有数据到所写套接字的发送缓冲区。如果该套接字的发送缓冲区容不下该应用进程的所有数据(或是应用进程的缓冲区大于套接字的发送缓冲区,或是套接字的发送缓冲区已经满了),该应用进程将被投入睡眠。这里假设该套接字是阻塞的,它是通常的默认设置。内核将不从write
系统调用返回,直到应用进程缓冲区中所有的数据都复制到套接字发送缓冲区。因此,从写一个TCP套接字的write
调用成功仅仅表示我们可以重新使用原来的应用进程缓冲区,并不表明对端的 TCP 或应用进程已经收到数据。
这一端的 TCP 提取套接字发送缓冲区中的数据并把它发送给对端TCP ,其过程基于 TCP 的所有规则。对端 TCP 必须确认已经收到数据,只有来自对端的ACK
的到达,本端 TCP 才能从套接字的发送缓冲区丢失已经对端已经确认收到的数据。
也就是说,TCP必须为已经发送的数据保留一个副本,直到确认它被对端收到。
那么TCP是如何与IP层协作的呢?
- TCP数据传输:
- TCP将应用层的数据分割成一定大小的块,
MSS
是在TCP连接建立过程中,通过TCP选项由对端通告的值。如果对端没有发送MSS选项,则默认的MSS
大小为536字节。这个值是基于IPv4的最小重组缓冲区大小(576字节)减去IPv4首部(20字节)和TCP首部(20字节)的大小。
- TCP将应用层的数据分割成一定大小的块,
- TCP分节的构成:
- 每个数据块会被附加一个 TCP 首部,从而形成一个 TCP 分节。TCP 首部包含了用于数据传输控制的信息,如序列号、确认号、窗口大小等。
- IP层处理:
- IP层接收来自TCP层的分节,并为每个分节附加一个IP首部,形成IP数据报。IP首部包含了目的IP地址和源IP地址等信息。
- IP层根据数据报的目的IP地址查找路由表,确定数据报应该通过哪个外出接口发送。
- 在发送数据报之前,IP 层可能会根据数据链路的
MTU
(最大传输单元)对数据报进行分片。然而,通过使用MSS
选项和路径MTU
发现功能,TCP 试图避免 IP 层的分片,因为分片会增加网络的开销和复杂性。
- 数据链路层的输出队列:
- 每个网络接口都有一个输出队列,用于暂存即将发送到网络的数据报。
- 如果输出队列已满,新到达的数据报将会被丢弃,并且错误信息会沿着协议栈向上传递,从数据链路层到 IP 层,再从 IP 层到 TCP 层。
- 错误处理:
- 当 TCP 层接收到错误信息时,它会意识到某个分节没有被成功发送。TCP 将负责在以后的某个时刻重传这个分节。
- 对于应用进程来说,这种分节重传的过程是透明的。应用进程不会直接感知到这种暂时性的网络问题,因为 TCP 协议会自动处理这些情况,确保数据的可靠传输。
TCP 可以将数据可靠地从一台主机传输到另一台主机,并且在网络出现问题时进行适当的错误处理和重传。
TCP 全双工通信的实现与用户端和服务端各自拥有独立的发送缓冲区和接收缓冲区密切相关。这些缓冲区的存在是实现全双工通信的重要基础。
全双工通信(Full_Duplex Communication)是指通信的双方可以同时进行数据发送和接收。这意味着,用户端和服务端可以在同一时刻互相发送和接收数据,而不会发生冲突。
在 TCP 中,每个连接的双方(客户端和服务端)都有两个缓冲区:
- 发送缓冲区(Send Buffer):用于存储待发送的数据。
- 接收缓冲区(Receive Buffer):用于存储已接收到但尚未被应用程序处理的数据。
这些缓冲区的存在使得数据在发送和接收过程中能够被暂时存储,从而实现流量控制和数据处理的解耦。
由于 TCP 连接的双方都有独立的发送缓冲区和接收缓冲区,每一方都可以在以下情况下独立操作:
- 发送数据:当用户端想要发送数据时,会将数据写入发送缓冲区,TCP 协议栈会从该缓冲区中读取数据并通过网络发送给服务端。同样地,服务端也可以将数据写入其发送缓冲区,通过网络发送给用户端。
- 接收数据:当数据到达接收端时,数据会被放入接收缓冲区,等待应用程序进行处理。用户端和服务端的接收缓冲区独立存在,彼此之间互不影响。
TCP 实现全双工通信的原因在于每一端(客户端和服务端端)都拥有独立的发送缓冲区和接收缓冲区,这使得双方可以在同一时刻独立地发送和接收数据。通过滑动窗口、确认机制和流量控制等机制,TCP 确保了数据传输的可靠性和有序性,同时实现了高效的全双工通信。
TCP通过一个套接字描述符,可以既读又写
TCP 套接字是全双工的通信通道,允许数据在两个方向上同时传输。
TCP 本质上是一种面向连接的、可靠的、全双工的数据传输协议。全双工意味着数据可以同时在两个方向上传输,即:
- 读操作:从套接字中接收数据。
- 写操作:向套接字中发送数据。
在Linux操作系统中,文件描述符是一个抽象的指针,用于标识内核为进程打开的文件、设备、管道或套接字。每个打开的文件或套接字都分配一个唯一的文件描述符。
当创建一个 TCP 套接字时,操作系统分配一个文件描述符给这个套接字。这时,套接字作为文件描述符的一种特殊形式,具备以下特点:
- 读写操作支持:同一个文件描述符可以被用来调用
read
(或recv
)和write
(或send
)函数。读取操作从套接字接收数据,写入操作通过套接字发送数据。 - 面向连接:在建立连接后,套接字两端都可以进行读和写操作,不需要再区分是读套接字还是写套接字。
tcp中读到的报文可能读到一个半或半个等,称为tcp粘包问题。
TCP粘包问题是指由于TCP本身是面向流的协议,所以在传输数据时,没有边界,这就导致应用层在读取TCP数据时可能会读取到一个完整的消息,也可能读取到多个消息的部分,或者一个消息的部分,这就是所谓的粘包问题。
向网络里写的是添加了报头的数据,如果本地把数据写到文本文件里,写到文件里,解析文件时,就边解析边处理。
三、TCP 的状态转换
下图是 TCP 涉及连接建立和连接终止的状态转换图。
1、TCP 连接的建立(三次握手)状态
TCP连接(三次握手):
客户端:CLOSED
→ SYN_SENT
→ ESTABLISHED
服务端:CLOSED
→ SYN_RECEIVED
→ ESTABLISHED
CLOSED:初始状态,表示没有任何连接或活动。当客户端或服务端尚未开始通信时,TCP套接字处于这个状态。
SYN_SENT:当客户端主动发起连接请求时(发送 SYN
包),它进入此状态。此时,客户端已经发送了 SYN
报文并等待服务端的响应。
SYN_RCVD:服务端收到客户端的 SYN
请求后,它回复一个 SYN_ACK
报文并进入此状态。它表示服务端正在等待客户端的确认。
ESTABLISHED:客户端收到服务端的 SYN_ACK
并发送 ACK
报文后,服务端进入 ESTABLISHED
状态,客户端同时也进入此状态。这是一个表示连接已成功建立的状态,双方可以开始数据传输。
- 应用进程执行主动打开:
- 当应用进程想要建立一个 TCP 连接时,它会指示 TCP 执行主动打开操作。
- 在
CLOSED
状态下,TCP 为即将建立的连接分配资源,并发送一个SYN
(同步)报文到目标服务端。 - 发送
SYN
之后,TCP 的状态从CLOSED
变为SYN_SENT
。
- 接收带ACK的SYN:
- 目标服务端在收到客户端发送的
SYN
后,会发送一个SYN_ACK
报文作为响应。 SYN_ACK
报文中既包含SYN
也包含ACK
,SYN
用于初始化服务端端的序列号,ACK
用于确认客户端的SYN
。- 当客户端 TCP 接收到这个
SYN_ACK
报文时,它确认了服务端已经收到了它的SYN
,并且服务端也准备好建立连接。
- 目标服务端在收到客户端发送的
- 发送ACK并进入ESTABLISHED状态:
- 为了完成三次握手,客户端需要发送一个
ACK
报文来确认服务端的SYN
。 - 在发送
ACK
报文之后,客户端TCP的状态从SYN_SENT
变为ESTABLISHED
。 - 此时,服务端也接收到客户端的
ACK
,确认客户端已经准备好进行数据传输,服务端端的状态也从SYN_RCVD
变为ESTABLISHED
。
- 为了完成三次握手,客户端需要发送一个
从上面的过程我们可以发现,前两次握手是不可以携带数据的,而第三次握手是可以携带数据的。因为主动连接的一方已经收到了ACK
,认为自己已经成功建立了连接,可以开始发送数据。
但客户端认为连接建立好的时间与服务端认为连接建立好的时间有差别。
客户端和服务端认为连接建立好的时间存在差异,是由于TCP三次握手的顺序和状态转换所导致的。
客户端视角:客户端发送SYN
后进入SYN_SENT
状态。当客户端收到来自服务端的SYN_ACK
响应时,它发送ACK
作为确认,并且立即将状态从SYN_SENT
变为ESTABLISHED
。在客户端的角度,连接在发送完ACK
之后就被认为是建立好的,因为它已经完成了它需要做的所有握手步骤。
服务端视角:服务端在收到客户端的SYN
后,发送SYN_ACK
并进入SYN_RCVD
状态。服务端需要等待客户端的ACK
响应,一旦收到ACK
,服务端才将状态从SYN_RCVD
变为ESTABLISHED
。在服务端的角度,连接在收到客户端的ACK
之后才被认为是建立好的。
因此,客户端和服务端认为连接建立好的时间点不同:
- 客户端认为连接建立好的时间是它发送
ACK
之后。 - 服务端认为连接建立好的时间是它收到
ACK
之后。
从上面的描述过程,我们可以得知客户端 connect
成功返回是在第二次握手,服务端 accept
成功返回是在三次握手成功之后。
这种时间上的差异是由于网络延迟造成的。ACK
报文从客户端到服务端可能需要一些时间,这取决于网络条件。尽管存在这种时间差异,但它通常很小,不会对实际的数据传输造成影响,因为 TCP 协议设计时已经考虑到了网络延迟和网络的不确定性。
三次握手的目的是让通信的双方都确认连接已经建立,但这个过程并不保证100%成功。
第一次握手:服务端接收客户端发来的SYN
。如果服务端接收到了,服务端就可以认为:客户端的发送能力,和服务端的接收能力是正常。
第二次握手:服务端发送SYN_ACK
给客户端。如果客户端接收到了,客户端此时就可以认为:客户端的收发能力是正常的,服务端的收发能力也是正常的。但是服务端此时仍需要确认客户端的接收能力是否正常。
第三次握手:服务端接收客户端发来的ACK
。此时服务端就可以认为:客户端的收发能力是正常。
- 如果客户端没有收到服务端的
SYN_ACK
,它会重试发送SYN
,直到达到一定的重试次数,然后放弃。 - 如果服务端没有收到客户端的
ACK
,它会等待一段时间后重新发送SYN_ACK
,直到达到一定的重传次数,然后放弃。
半连接队列和全连接队列
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
- 半连接队列:用于存储尚未完成TCP三次握手的连接请求。具体来说,它包含以下状态的连接
SYN_RCVD
。服务器已经收到了客户端的SYN
请求,并且已经回复了SYN_ACK
,但还没有收到客户端的ACK
确认。
当服务器收到一个SYN
请求时,它会创建一个半开连接,并将该连接添加到半连接队列中。服务器会等待客户端的ACK
报文来完成握手过程。如果服务器没有在预定的超时时间内收到ACK
,它将删除该连接并释放资源。
- 全连接队列:用于存储已经完成TCP三次握手但尚未被应用程序的
accept
调用取走的连接。具体来说,它包含以下状态的连接ESTABLISHED
。客户端和服务器已经完成了三次握手,连接已经建立,但服务器应用程序尚未调用accept
函数来处理这个连接。
当服务器收到客户端的ACK
报文并完成握手后,连接将从半连接队列移动到全连接队列。在全连接队列中,连接等待被应用程序处理。如果全连接队列满了,新的完成握手的连接可能会被丢弃,导致客户端连接失败。
面试题:为什么建立连接是三次握手
一次握手:如果仅有一次握手,比如只发送 SYN
报文,那么服务端无法得知客户端是否已经收到它的响应(SYN_ACK
),也无法确定客户端是否准备好进行数据传输。这可能导致服务端资源的浪费,因为服务端会为未确认的连接保留资源。容易受到SYN
洪水攻击。
SYN洪水攻击:攻击者向目标服务器发送大量的SYN请求,但并不发送后续的ACK报文来完成握手过程。
服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的。服务器为每个收到的SYN请求分配资源(如内存中的连接记录),并等待客户端的ACK报文。由于攻击者不发送ACK,服务器会保留这些资源一段时间,等待可能永远不会到达的ACK报文。如果攻击者持续发送大量的SYN请求,服务器资源(如半开连接队列)将被迅速耗尽。
SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。
两次握手:如果是两次握手(例如客户端发送 SYN
,服务端发送 SYN_ACK
),客户端无法确认服务端是否已准备好接收数据。两次握手缺乏对通信双方的确认,可能导致一些问题:
- 旧的重复连接:在网络中,可能存在一些延迟的、失效的报文。如果只进行两次握手,一个旧的 SYN 报文可能会导致服务端错误地建立连接,从而导致数据的不一致。
- 半开连接和资源浪费:如果客户端因网络问题没有收到服务端的 SYN_ACK报文,那么服务端会一直等待客户端的确认,而不会释放资源,可能会导致资源浪费和服务不可用。
三次握手时客户端需要先于服务端建立好连接,服务端在第三次握手之前不会为连接分配大量资源,避免了资源浪费。因此,需要三次握手才能确认双方的接收与发送能力是否正常。
理由一:保证信道(网络)是健康的。
双方都能发送和接收:在三次握手过程中,客户端和服务器都会发送和接收报文。这确保了双方都能正常地通过网络进行通信。
确认全双工:通过三次握手,双方都确认了对方能够发送和接收数据,这为全双工通信打下了基础。
理由二:确保双方操作系统(TCP)是健康的且愿意通信的。
三次握手确保了双方操作系统(TCP)的健康性和通信意愿。
资源分配:在三次握手的过程中,双方都会分配资源(如TCP控制块)来管理这个连接。这确保了双方都有资源来处理这个连接。
状态确认:通过三次握手,双方都确认了对方的状态(如客户端发送SYN
,服务器发送SYN_ACK
,客户端发送ACK
)。这确保了双方都处于准备通信的状态。
理由三:阻止重复历史连接的初始化。 防止旧的重复连接初始化造成混乱。
历史连接指的是那些已经完成但尚未从网络中完全消失的旧连接。如果一个新的连接请求与一个旧连接的序列号相同,那么可能会发生以下情况:旧的数据包可能因为网络延迟而晚到达,如果这些数据包被错误地认为是属于新连接的,那么它们可能会干扰新连接的数据传输。
假设客户端尝试与服务端建立TCP连接,但由于网络问题,可能会发生以下情况:
- 客户端发送初始SYN报文:客户端首先发送一个
SYN
报文(序列号为90)来请求建立连接,但这个报文在网络中阻塞了。 - 客户端宕机并重启:在发送
SYN
报文后,客户端宕机。当客户端重启后,它忘记了之前的连接尝试,并再次发送一个新的SYN
报文(序列号为100)来建立连接。 - 网络拥堵导致旧SYN报文晚到达:由于网络拥堵,原先的
SYN
报文(序列号为90)在客户端发送新的SYN
报文之后才到达服务端。
客户端连续发送多次 SYN
(都是同一个四元组)建立连接的报文,在网络拥堵情况下:
- 旧SYN报文到达服务端:旧
SYN
报文(序列号为90)先于新的SYN报文到达服务端。服务端响应这个旧SYN
报文,发送一个SYN_ACK
报文,确认号为91(90+1)。 - 客户端识别并拒绝旧连接:客户端收到服务端的
SYN_ACK
报文,但期望的确认号是101(100+1),而不是91。因此,客户端识别出这是一个不属于当前连接尝试的报文,并发送一个RST
(重置)报文给服务端。 - 服务端释放连接:服务端收到
RST
报文后,会释放这个半开连接,不再等待客户端的ACK。 - 新SYN报文到达服务端:随后,新的
SYN
报文(序列号为100)到达服务端。这次,客户端和服务端可以正常完成三次握手过程,建立新的连接。
通过这个过程,TCP的三次握手机制有效地防止了历史连接的初始化,确保了新连接的建立不会受到旧连接数据包的干扰。这是TCP设计中的一个关键特性,保证了连接的可靠性和数据传输的安全性。
RST(reset):通常用于重置连接。为了解决建立链接出现异常的问题。连接重置就是让双方重新进行三次握手。
注:每次建立 TCP 连接时,初始化的序列号都要求不一样。
如果客户端和服务端在每次建立连接时都使用相同的初始化序列号,那么就无法区分新连接和历史连接的数据包。
- 数据包混淆:如果两个连接使用相同的序列号,接收方将无法区分哪个数据包属于哪个连接。这会导致数据包混淆,使得接收方无法正确地按照顺序处理数据包。
- 历史连接干扰:历史连接的数据包可能会被错误地认为是新连接的数据包,从而干扰新连接的数据传输。这可能导致数据包的乱序、重复或丢失。
- 安全性问题:如果攻击者能够预测新连接的序列号,他们可能会尝试利用这个信息来干扰或窃取新连接的数据。这可能会导致数据泄露或服务中断。
为了防止这些问题,TCP协议设计了一个随机化初始序列号的机制。每个TCP实现都会选择一个随机的初始序列号,以确保每次连接的序列号都是唯一的。
若三次握手最后一次ACK丢失 会发生什么?
三次握手最后一次ACK
,即客户端发送给服务端的确认收到服务端SYN_ACK
的ACK
。
客户端收到服务端的 SYN_ACK
报文后,就会给服务端回一个 ACK
报文,也就是第三次握手,此时客户端状态进入到 ESTABLISHED
状态。如果第三次握手丢失,服务端就认为并没有建立好连接。触发超时重传机制,重发SYN_ACK报文,直到第三次握手成功,或者达到最大重传次数。达到最大重传次数后,服务器最终会放弃连接。
若第三次握手丢失了,但客户端认为连接已经成功建立,向服务端发送数据?
服务端在收到数据包时,发现该包不属于任何已建立的连接,因为服务端还未收到第三次握手的 ACK
报文。 发送 RST。服务端会认为收到的包是一个错误的包(即在未建立的连接上发送的数据),因此根据 TCP 协议,会发送一个带有 RST
标志的报文来重置连接。这个RST
报文会告诉发送方(客户端),当前尝试的连接是不存在的,或者不应该发送数据。
资源释放:发送 RST
报文会立即释放与连接相关的资源,但可能会丢失未传输的部分数据。
对等方的处理:接收到 RST
报文的一方会立即关闭连接,不会尝试重新传输未确认的数据。
2、TCP 连接的终止(四次挥手)状态
TCP连接终止(四次挥手):
主动关闭方:ESTABLISHED
→ FIN_WAIT_1
→ FIN_WAIT_2
→ TIME_WAIT
→ CLOSED
被动关闭方:ESTABLISHED
→ CLOSE_WAIT
→ LAST_ACK
→ CLOSED
FIN_WAIT_1:当一方(通常是客户端)主动关闭连接时,它发送 FIN
报文并进入此状态。这表示它已经发出了连接终止请求,但还没有收到对方的确认。
FIN_WAIT_2:当对方收到 FIN
报文并发送了 ACK
进行确认,发起关闭的一方进入此状态。此时,它在等待对方发送自己的 FIN
报文。
TIME_WAIT:当主动关闭的一方收到对方的 FIN
并发送 ACK
进行确认后,进入 TIME_WAIT
状态。该状态持续一段时间(通常是2倍的最大报文段寿命2MSL,约几秒钟),确保对方已经收到最后的 ACK
并防止旧的报文在网络中被重新传输。
CLOSE_WAIT:被动关闭的一方在收到对方的 FIN
后,进入 CLOSE_WAIT
状态,表示连接正在关闭,但该方可能还需要处理一些剩余的事务(如发送剩余数据)。
LAST_ACK:被动关闭的一方在发送 FIN
后,等待对方的最后确认(ACK
),进入 LAST_ACK
状态。一旦收到确认,连接终止。
CLOSE:最终状态。双方都完成了 FIN
和 ACK
的交换,连接彻底关闭,TCP进入初始的 CLOSE
状态。
- 客户端发送FIN报文:客户端首先发送一个
FIN
报文,表示它没有更多的数据要发送。发送FIN
的客户端进入FIN_WAIT_1
状态,等待服务端的确认。 - 服务端发送ACK报文:服务端收到
FIN
报文后,发送一个ACK
报文作为确认。服务端进入CLOSE_WAIT
状态,表示它已经准备关闭本地应用程序的连接,但还在等待应用程序的通知。 - 客户端收到ACK报文:客户端收到服务端的
ACK
报文后,进入FIN_WAIT_2
状态。 - 服务端发送FIN报文:服务端在应用程序准备好关闭连接后,发送一个
FIN
报文给客户端。服务端进入LAST_ACK
状态,等待客户端的确认。 - 客户端发送ACK报文:客户端收到服务端的
FIN
报文后,发送一个ACK
报文作为最后的确认。客户端进入TIME_WAIT
状态,等待2MSL
以确保服务端收到ACK
报文。 - 服务端收到ACK报文:服务端收到客户端的
ACK
报文后,进入CLOSE
状态,表示连接已经完全关闭。 - 客户端进入CLOSE状态:客户端在经过
2MSL
一段时间后,自动进入CLOSE
状态,表示客户端也完成连接的关闭。
每个方向都需要一个 FIN
和一个 ACK
,因此通常被称为四次挥手。 只有主动关闭连接的,才有 TIME_WAIT
状态。
如果第一次挥手丢失:如果客户端发送的FIN
报文丢失,客户端会等待一段时间(通常由TCP的超时重传机制决定),如果没有收到服务端的确认(ACK
报文),客户端会重传FIN
报文。
如果第二次挥手丢失:当服务端收到客户端的第一次挥手后,就会先回一个 ACK
确认报文,此时服务端的连接进入到 CLOSE_WAIT
状态。但ACK
不会重传。客户端就会触发超时重传机制,重传 FIN
报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。
如果第三次挥手丢失:当服务端(被动关闭方)收到客户端(主动关闭方)的FIN
报文后,内核会自动回复一个ACK
报文,并且将连接状态设置为CLOSE_WAIT
。这个状态的含义是,服务端已经准备好关闭本地应用程序的连接,但是它还在等待应用程序的通知来完成实际的关闭操作。即等待进程调用close
函数来触发服务端发送FIN
报文。当服务端处于CLOSE_WAIT
状态时,如果应用程序调用了close
函数,内核会自动发送一个FIN
报文,同时连接状态会更新为LAST_ACK
。在这个状态下,服务端等待客户端返回ACK
报文以确认连接关闭。如果服务端迟迟收不到客户端的ACK
报文,服务端会重发FIN
报文,以确保客户端能够接收到这个关闭请求(超时重传机制)。
如果第四次挥手丢失:服务端(被动关闭方)没有收到 ACK
报文前,还是处于 LAST_ACK
状态。如果第四次挥手的 ACK
报文没有到达服务端,服务端就会重发 FIN
报文。如果服务端重传的FIN
到达客户端,客户端会认为这是一个新的FIN
请求,并发送ACK
确认。
在特定情况下,四次挥手是可以变成三次挥手的。
详解TIME_WAIT 状态
在断开连接的状态图中,当一端执行主动关闭操作时,它将进入一个称为TIME_WAIT
的状态。这个状态的持续时间被设定为2倍的最大报文生存期(2MSL,Maximum Segment Lifetime)。
TIME_WAIT
状态确保在一个TCP连接关闭后,一段时间内不会立即重新使用相同的四元组(源IP、源端口、目标IP、目标端口)进行新的连接。这段时间通常是两个最大段生存时间(2MSL),即数据包在网络中生存的最长时间。这有助于确保所有旧连接的延迟数据包在网络中完全消失,不会误与新连接混淆。
为什么需要TIME_WAIT
状态?
理由一:可靠地实现TCP全双工连接的终止。
- TCP连接是全双工的,意味着数据可以在两个方向上独立传输。当需要终止这样的连接(即关闭全双工)时,每个方向的数据流都需要被独立地关闭,即它必须正确处连接中止过程4个报文任何一个报文丢失的情况。
- 也就是说,必须确保
FIN
报文被对端正确接收。如果最后一个ACK
在传输过程中丢失,那么发送FIN
报文的一端会认为没有收到确认,并会重新发送FIN
报文。为了能够正确地响应这个重发的FIN
报文,执行主动关闭的端点(即发送最后一个ACK
的端点)必须维护状态信息(即维护TIME_WAIT
状态信息),以便能够重新发送ACK
报文。如果它不维护这些信息,它可能会响应一个RST
报文,这会被对端解释为一个错误。因此,执行主动关闭的端点需要进入TIME_WAIT
状态,以保持足够的状态信息来处理这种可能性。客户端在收到服务端重传的FIN
报文时,TIME_WAIT
状态的等待时间,会重置回2MSL
。 - 本例子也说明了为什么执行主动关闭的那一端是处于TIME_WAIT状态的那一端:由于主动关闭连接的一端需要负责确保所有报文都被正确处理,因此它需要进入
TIME_WAIT
状态。
理由二:允许在网络中残留的旧报文过期,防止它们对新连接产生影响。
TIME_WAIT
状态允许网络中可能存在的旧报文过期,从而避免这些旧报文干扰随后建立的新连接。这样可以确保新连接的数据不会受到之前连接残留数据的影响,保证了网络通信的稳定性。- 当一个TCP连接关闭后,如果在相同的IP地址和端口之间很快又建立了新的连接,这个新连接被称为前一个连接的“化身”。为了避免来自旧连接的重复分组(可能在网络中长时间徘徊)被新连接错误地解释,TCP需要确保这些旧的分组在网络中已经消逝。
TIME_WAIT
状态通过保持2MSL
的时间长度来实现这一点。MSL
是最大报文生存期,即一个TCP报文在网络中可能存在的最长时间。通过保持TIME_WAIT
状态2MSL
的时间,可以确保在这个时间内,任何在网络中徘徊的旧分组都会被丢弃,从而不会干扰到新的连接。这保证了当一个新连接被建立时,它不会受到来自之前连接的老分组的干扰。
这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。
如果TIME_WAIT
状态过多会怎么样?
如果系统中的TIME_WAIT
状态过多,可能会出现以下几种情况:
-
消耗系统资源:每个处于
TIME_WAIT
状态的连接都会占用系统的一些资源,包括内存和文件描述符。如果TIME_WAIT
状态过多,可能会导致资源耗尽,影响新连接的建立。 -
端口耗尽:由于
TIME_WAIT
状态会占用本地端口一段时间,如果短时间内大量连接进入TIME_WAIT
状态,可能会耗尽可用的端口资源,导致新的连接无法建立。-
如果客户端(主动发起关闭连接方)的
TIME_WAIT
状态过多,占满了所有端口资源,那么就无法对目的 IP+ 目的 PORT都一样的服务端发起连接了,但是被使用的端口,还是可以继续对另外一个服务端发起连接的。这是因为每个外向连接都需要一个唯一的源端口来区分不同的连接。也就说,对于已经处于
TIME_WAIT
状态的端口,如果目的服务端的目的 IP+ 目的 PORT组合不同,客户端仍然可以使用这些端口来建立新的连接。这是因为TCP连接是由四元组(源IP、源端口、目的IP、目的PORT)唯一标识的。只要这四元组中的任何一个元素不同,就可以认为是一个新的连接。 -
服务端通常使用固定的端口来监听客户端的连接请求。如果(并发处理大量连接的)服务端主动关闭连接,很多连接进入
TIME_WAIT
状态,那么服务器也不能立即使用相同的四元组建立新连接。
-
如果客户端希望断开连接而服务端仍然有数据要发送,TCP允许通信双方有序地关闭连接,同时允许未完成的数据传输。但客户端已经关闭,服务端发送的数据如何被客户端得到呢?
当客户端已经关闭连接,服务端尝试发送的数据将无法被接收,并最终导致服务端意识到连接的终止。TCP 协议会通过 RST 报文或其他机制通知服务端,随后服务端会关闭连接并停止发送数据。所有未成功传输的数据都将丢失,因此应用层应具备处理这种情况的逻辑。
但是若客户端只想收信息,不发信息。套接字文件描述符是全双工的,我们可以使用shutdown()
。shutdown()
函数提供了比直接调用close()
函数更细粒度的控制,允许在关闭连接的不同阶段处理特定情况。例如,可以关闭写入端来结束发送数据而不立即关闭读取端,以继续处理收到的数据。
我们进行一下实验,我们实现的是一个并发的 TCP-echo 服务端。
服务端关闭accpet
得到的文件描述符,那么在客户端退出,服务端未退出时,我们可以发现客户端会处于time_wait
:
由ESTABLISHED
转变为TIME_WAIT
。
若服务端主动断开连接,四次挥手并未完成。netstat ntap
服务端进行处理数据而启动的进程(accept
后启动新进程或线程处理任务)处于TIME_WAIT
服务端主动断开连接,四次挥手并未完全完成,处于TIME_WAIT
状态。因此,我们再次启动服务端时,8888端口仍被占用,bind失败。
可以使用setsockopt
解决此问题。
如果服务端出现大量
CLOSE_WAIT
状态。
我们将一个并发的 TCP-echo 服务端的close
注释掉。
再次连接:
如果被动关闭方没有调用 close
函数关闭连接,那么就无法发出 FIN
报文,从而无法使得 CLOSE_WAIT
状态的连接转变为 LAST_ACK
状态。所以,当服务端出现大量 CLOSE_WAIT
状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接。
3、TCP 异常情况
在TCP网络通信中,会存在多种异常情况,这些异常情况会导致连接的非正常终止。
当进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使进程退出了,还是能与对端完成 TCP四次挥手的过程。
-
进程终止:当进程终止时,操作系统会释放该进程所持有的所有资源,包括文件描述符。对于TCP连接而言,这通常会导致以下行为:
- 发送FIN:如果进程终止时,TCP 连接仍然打开,操作系统会自动发送一个
FIN
包来开始正常的四次挥手关闭过程。这与其他正常关闭连接的情况类似。
- 发送FIN:如果进程终止时,TCP 连接仍然打开,操作系统会自动发送一个
-
机器重启:机器重启的情况与进程终止类似,因为所有进程都会被终止,相应的文件描述符也会被释放:
- 发送FIN:在重启过程中,操作系统会关闭所有打开的连接,并发送
FIN
包来关闭 TCP 连接。
- 发送FIN:在重启过程中,操作系统会关闭所有打开的连接,并发送
当客户端 突然掉线/主机崩溃 时,TCP 协议栈无法立即感知连接的中断,因此会保持连接状态,直到保活探测机制触发。也就是下面的情况:
-
机器掉电/网线断开:当发生机器掉电或网线断开的情况时,连接的双方可能会以不同的方式响应:
-
接收端:接收端可能不会立即意识到连接已经断开。如果接收端尝试发送数据,它最终会收到一个由网络层返回的
ICMP
错误消息,表明目标主机不可达或者网络路径有问题。 -
TCP保活机制:TCP协议中有一个保活机制,用于检测连接是否仍然有效。保活机制会定期发送探测包来检查对方是否仍然在线。探测包的发送和确认应答可以帮助服务端检测到客户端的掉线情况。如果客户端收到保活探测包,会返回一个 ACK 包,表示仍然在线。
-
接收端reset:如果接收端在尝试发送数据时发现连接已经断开,它会发送一个RST(Reset)包来快速关闭连接,而不是正常的四次挥手过程。
-
应用层保活机制:某些应用层协议(如HTTP长连接、QQ等)实现了自己的保活或心跳机制,用于定期检测连接的对端是否仍然可用。如果检测到连接断开,应用层协议会尝试重新建立连接或者采取其他恢复措施。
-
TCP保活机制(TCP keepalive)用于检测一个空闲的TCP连接是否仍然有效。