一、TCP基本认识
1、TCP头部格式
- 序列号:在建⽴连接时由计算机⽣成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送⼀次数据,就「累加」⼀次该「数据字节数」的⼤⼩。⽤来解决⽹络包乱序问题。
- 确认应答号:指下⼀次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。⽤来解决不丢包的问题。
-
控制位:
- ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建⽴连接时的 SYN 包之外该位必须设置为 1 。
- RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
- SYN:该位为 1 时,表示希望建⽴连接,并在其「序列号」的字段进⾏序列号初始值的设定。
- FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双⽅的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
2、为什么需要 TCP 协议? TCP工作在哪⼀层?
IP 层是「不可靠」的,它不保证⽹络包的交付、不保证⽹络包的按序交付、也不保证⽹络包中的数据的完整性。
如果需要保障⽹络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。
因为 TCP 是⼀个工作在传输层的可靠数据传输的服务,它能确保接收端接收的⽹络包是无损坏、无间隔、非冗余和按序的。
3、什么是 TCP ?
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
- 面向连接:⼀定是「⼀对⼀」才能连接,不能像 UDP 协议可以⼀个主机同时向多个主机发送消息,也就是⼀对多是⽆法做到的;
- 可靠的:⽆论的⽹络链路中出现了怎样的链路变化,TCP 都可以保证⼀个报⽂⼀定能够到达接收端;
- 字节流:消息是「没有边界」的,所以⽆论我们消息有多⼤都可以进⾏传输。并且消息是「有序的」,当「前⼀个」消息没有收到的时候,即使它先收到了后⾯的字节,那么也不能扔给应⽤层去处理,同时对「重复」的报⽂会⾃动丢弃。
4、什么是 TCP 连接?
连接:简单来说就是,⽤于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗⼝⼤⼩称为连接。
所以,建⽴⼀个 TCP 连接是需要客户端与服务器端达成上述三个信息的共识。
- Socket:由 IP 地址和端⼝号组成
- 序列号:⽤来解决乱序问题等
- 窗⼝⼤⼩:⽤来做流ᰁ控制
5、如何唯⼀确定⼀个 TCP 连接呢?
TCP 四元组可以唯⼀的确定⼀个连接,四元组包括如下:
- 源地址
- 源端⼝
- ⽬的地址
- ⽬的端⼝
- 源地址和⽬的地址的字段(32位)是在IP头部中,作⽤是通过IP协议发送报⽂给对⽅主机。
- 源端⼝和⽬的端⼝的字段(16位)是在TCP头部中,作⽤是告诉TCP协议应该把报⽂发给哪个进程。
6、有⼀个IP的服务器监听了⼀个端⼝,它的TCP的最⼤连接数是多少?
服务器通常固定在某个本地端⼝上监听,等待客户端的连接请求。
因此,客户端 IP 和 端⼝是可变的,其理论值计算公式如下:
对 IPv4,客户端的 IP 数最多为 2 的 32 次⽅,客户端的端⼝数最多为 2 的 16 次⽅,也就是服务端单机最⼤TCP 连接数,约为 2 的 48 次⽅。
7、UDP 和 TCP 有什么区别呢?分别的应用场景是?
UDP 不提供复杂的控制机制,利⽤ IP 提供⾯向「⽆连接」的通信服务。
UDP 协议真的⾮常简单,头部只有 8 个字节( 64 位),UDP 的头部格式如下:
- ⽬标和源端⼝:主要是告诉 UDP 协议应该把报⽂发给哪个进程。
- 包⻓度:该字段保存了 UDP ⾸部的⻓度跟数据的⻓度之和。
- 校验和:校验和是为了提供可靠的 UDP ⾸部和数据⽽设计。
TCP 和 UDP 区别:
1、连接
- TCP 是⾯向连接的传输层协议,传输数据前先要建⽴连接。
- UDP 是不需要连接,即刻传输数据。
2、 服务对象
- TCP 是⼀对⼀的两点服务,即⼀条连接只有两个端点。
- UDP ⽀持⼀对⼀、⼀对多、多对多的交互通信
3、可靠性
- TCP 是可靠交付数据的,数据可以⽆差错、不丢失、不重复、按需到达。
- UDP 是尽最⼤努⼒交付,不保证可靠交付数据。
4、拥塞控制、流量控制
- TCP 有拥塞控制和流ᰁ控制机制,保证数据传输的安全性。
- UDP 则没有,即使⽹络⾮常拥堵了,也不会影响 UDP 的发送速率。
5、⾸部开销
- TCP ⾸部⻓度较⻓,会有⼀定的开销,⾸部在没有使⽤「选项」字段时是 20 个字节,如果使⽤了「选项」字段则会变⻓的。
- UDP ⾸部只有 8 个字节,并且是固定不变的,开销较⼩。
6、传输⽅式
- TCP 是流式传输,没有边界,但保证顺序和可靠。
- UDP 是⼀个包⼀个包的发送,是有边界的,但可能会丢包和乱序。
7、分⽚不同
- TCP 的数据⼤⼩如果⼤于 MSS ⼤⼩,则会在传输层进⾏分⽚,⽬标主机收到后,也同样在传输层组装 TCP数据包,如果中途丢失了⼀个分⽚,只需要传输丢失的这个分⽚。
- UDP 的数据⼤⼩如果⼤于 MTU ⼤⼩,则会在 IP 层进⾏分⽚,⽬标主机收到后,在 IP 层组装完数据,接着再传给传输层,但是如果中途丢了⼀个分⽚,在实现可靠传输的 UDP 时则就需要重传所有的数据包,这样传输效率⾮常差,所以通常 UDP 的报⽂应该⼩于 MTU。
TCP 和 UDP 应⽤场景:
1、由于 TCP 是⾯向连接,能保证数据的可靠性交付,因此经常⽤于:
- FTP ⽂件传输
- HTTP / HTTPS
2、由于 UDP ⾯向⽆连接,它可以随时发送数据,再加上UDP本身的处理既简单⼜⾼效,因此经常⽤于:
- 包总量较少的通信,如 DNS 、SNMP 等
- 视频、⾳频等多媒体通信
- ⼴播通信
8、为什么 UDP 头部没有「⾸部⻓度」字段,⽽ TCP 头部有「⾸部⻓度」字段呢?
TCP 有可变⻓的「选项」字段,而 UDP 头部⻓度则是不会变化的,无需多⼀个字段去记录 UDP 的⾸部⻓度。
9、为什么 UDP 头部有「包⻓度」字段,⽽ TCP 头部则没有「包⻓度」字段呢?
其中 IP 总⻓度 和 IP ⾸部⻓度,在 IP ⾸部格式是已知的。TCP ⾸部⻓度,则是在 TCP ⾸部格式已知的,所以就可以求得 TCP 数据的⻓度。
⼤家这时就奇怪了问:“ UDP 也是基于 IP 层的呀,那 UDP 的数据⻓度也可以通过这个公式计算呀? 为何还要有「包⻓度」呢?”
这么⼀问,确实感觉 UDP 「包⻓度」是冗余的。
因为为了⽹络设备硬件设计和处理⽅便,⾸部⻓度需要是 4 字节的整数倍。
如果去掉 UDP 「包⻓度」字段,那 UDP ⾸部⻓度就不是 4 字节的整数倍了,所以这可能是为了补全UDP ⾸部⻓度是 4 字节的整数倍,才补充了「包⻓度」字段。
二、TCP 连接建立(三次握手)
-
⼀开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端⼝,处于 LISTEN 状态;
-
客户端会随机初始化序号( client_isn ),将此序号置于 TCP ⾸部的「序号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报⽂。接着把第⼀个 SYN 报⽂发送给服务端,表示向服务端发起连接,该报⽂不包含应⽤层数据,之后客户端处于 SYN-SENT 状态。
-
服务端收到客户端的 SYN 报⽂后,⾸先服务端也随机初始化⾃⼰的序号( server_isn ),将此序号填⼊TCP ⾸部的「序号」字段中,其次把 TCP ⾸部的「确认应答号」字段填⼊ client_isn + 1 , 接着把 SYN和 ACK 标志位置为 1 。最后把该报⽂发给客户端,该报⽂也不包含应⽤层数据,之后服务端处于 SYNRCVD 状态。
-
客户端收到服务端报⽂后,还要向服务端回应最后⼀个应答报⽂,⾸先应答报⽂TCP ⾸部ACK标志位置为1,其次「确认应答号」字段填⼊server_isn + 1,最后把报⽂发送给服务端,这次报⽂可以携带客户到服务器的数据,之后客户端处于ESTABLISHED 状态。
-
服务器收到客户端的应答报⽂后,也进⼊ ESTABLISHED 状态。
1、如何在 Linux 系统中查看 TCP 状态?
TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。
2、为什么是三次握手?不是两次、四次?
在前⾯我们知道了什么是 TCP 连接:
- ⽤于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗⼝⼤⼩称为连接。
- 所以,重要的是为什么三次握⼿才可以初始化Socket、序列号和窗⼝⼤⼩并建⽴ TCP 连接。
接下来以三个⽅⾯分析三次握⼿的原因:
- 三次握⼿才可以阻⽌重复历史连接的初始化(主要原因)
- 三次握⼿才可以同步双⽅的初始序列号
- 三次握⼿才可以避免资源浪费
(1)避免历史连接
简单来说,三次握手的首要原因是为了防⽌旧的重复连接初始化造成混乱。
⽹络环境是错综复杂的,往往并不是如我们期望的⼀样,先发送的数据包,就先到达⽬标主机,可能会由于⽹络拥堵等乱七⼋糟的原因,会使得旧的数据包,先到达⽬标主机。
三次握手情况下
客户端连续发送多次 SYN 建⽴连接的报⽂,在⽹络拥堵情况下:
- ⼀个「旧 SYN 报⽂」比「最新的 SYN 」 报⽂早到达了服务端;
- 那么此时服务端就会回⼀个 SYN + ACK 报⽂给客户端;
- 客户端收到后可以根据⾃身的上下⽂,判断这是⼀个历史连接(序列号过期或超时),那么客户端就会发送RST 报⽂给服务端,表示中⽌这⼀次连接。
两次握手情况下
如果是两次握⼿连接,就不能判断当前连接是否是历史连接,三次握⼿则可以在客户端(发送⽅)准备发送第三次报⽂时,客户端因有⾜够的上下⽂来判断当前连接是否是历史连接:
- 如果是历史连接(序列号过期或超时),则第三次握⼿发送的报⽂是 RST 报⽂,以此中⽌历史连接;
- 如果不是历史连接,则第三次发送的报⽂是 ACK 报⽂,通信双⽅就会成功建⽴连接;
所以,TCP 使⽤三次握⼿建⽴连接的最主要原因是防止历史连接初始化了连接。
(2)同步双方初始序列号
TCP 协议的通信双方, 都必须维护⼀个「序列号」,序列号是可靠传输的⼀个关键因素,序列号的作⽤:
- 接收⽅可以去除重复的数据;
- 接收⽅可以根据数据包的序列号按序接收;
- 可以标识发送出去的数据包中,哪些是已经被对⽅收到的;
可⻅,序列号在 TCP 连接中占据着⾮常᯿要的作⽤,所以当客户端发送携带「初始序列号」的 SYN 报⽂的时候,需要服务端回⼀个 ACK 应答报⽂,表示客户端的 SYN 报⽂已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样⼀来⼀回,才能确保双⽅的初始序列号能被可靠的同步。
四次握⼿其实也能够可靠的同步双⽅的初始化序号,但由于第⼆步和第三步可以优化成⼀步,所以就成了「三次握⼿」。
⽽两次握⼿只保证了⼀⽅的初始序列号能被对⽅成功接收,没办法保证双⽅的初始序列号都能被确认接收。
(3)避免资源浪费
如果只有「两次握⼿」,当客户端的 SYN 请求连接在⽹络中阻塞,客户端没有接收到 ACK 报⽂,就会重新发送 SYN ,由于没有第三次握⼿,服务器不清楚客户端是否收到了⾃⼰发送的建⽴连接的 ACK 确认信号,所以每收到⼀个 SYN 就只能先主动建⽴⼀个连接,这会造成什么情况呢?
如果客户端的 SYN 阻塞了,重复发送多次 SYN 报⽂,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
即两次握⼿会造成消息滞留情况下,服务器重复接受⽆⽤的连接请求 SYN 报⽂,⽽造成重复复分配资源。
(4)小结
TCP 建⽴连接时,通过三次握⼿能防⽌历史连接的建⽴,能减少双⽅不必要的资源开销,能帮助双⽅同步初始化序列号。序列号能够保证数据包不᯿复、不丢弃和按序传输。
不使⽤「两次握⼿」和「四次握⼿」的原因:
- 「两次握⼿」:⽆法防⽌历史连接的建⽴,会造成双⽅资源的浪费,也⽆法可靠的同步双⽅序列号;
- 「四次握⼿」:三次握⼿就已经理论上最少可靠连接建⽴,所以不需要使⽤更多的通信次数。
3、什么是 SYN 攻击?如何避免 SYN 攻击?
SYN 攻击:
假设攻击者短时间伪造不同 IP 地址的 SYN 报⽂,服务端每接收到⼀个 SYN 报⽂,就进⼊ SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报⽂,⽆法得到未知 IP 主机的ACK 应答,久⽽久之就会占满服务端的 SYN 接收队列(未连接队列),使得服务器不能为正常⽤户服务。
(1)避免 SYN 攻击⽅式⼀
通过修改 Linux 内核参数,控制队列⼤⼩和当队列满时应做什么处理。
- 当⽹卡接收数据包的速度⼤于内核处理的速度时,会有⼀个队列保存这些数据包。控制该队列的最⼤值如下参数:net.core.netdev_max_backlog
- SYN_RCVD 状态连接的最⼤个数:net.ipv4.tcp_max_syn_backlog
- 超出处理能时,对新的 SYN 直接回报 RST,丢弃连接:net.ipv4.tcp_abort_on_overflow
(2)避免 SYN 攻击⽅式⼆
Linux 内核的 SYN (未完成连接建⽴)队列与 Accpet (已完成连接建⽴)队列工作方式
正常流程:
- 当服务端接收到客户端的 SYN 报⽂时,会将其加⼊到内核的「 SYN 队列」;
- 服务端接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报⽂;
- 服务端接收到 ACK 报⽂后,从「 SYN 队列」移除放⼊到「 Accept 队列」;
- 应⽤通过调⽤ accpet() socket 接⼝,从「 Accept 队列」取出连接。
应⽤程序过慢:
- 如果应⽤程序过慢时,就会导致「 Accept 队列」被占满。
受到 SYN 攻击:
- 如果不断受到 SYN 攻击,就会导致「 SYN 队列」被占满。
tcp_syncookies 的⽅式可以应对 SYN 攻击的⽅法:net.ipv4.tcp_syncookies = 1
- 当 「 SYN 队列」满之后,后续服务器收到 SYN 包,不进⼊「 SYN 队列」;
- 计算出⼀个 cookie 值,再以 SYN + ACK 中的「序列号」返回客户端,
- 服务端接收到客户端的应答报⽂时,服务器会检查这个 ACK 包的合法性。如果合法,直接放⼊到「 Accept队列」。
- 最后应⽤通过调⽤ accpet() socket 接⼝,从「 Accept 队列」取出的连接。
三、TCP 连接断开
1、TCP 四次挥⼿过程和状态变迁
- 客户端打算关闭连接,此时会发送⼀个 TCP ⾸部 FIN 标志位被置为 1 的报⽂,也即 FIN 报⽂,之后客户端进⼊ FIN_WAIT_1 状态。
- 服务端收到该报⽂后,就向客户端发送 ACK 应答报⽂,接着服务端进⼊ CLOSED_WAIT 状态。
- 客户端收到服务端的 ACK 应答报⽂后,之后进⼊ FIN_WAIT_2 状态。
- 等待服务端处理完数据后,也向客户端发送FIN报⽂,之后服务端进⼊LAST_ACK状态。
- 客户端收到服务端的 FIN 报⽂后,回⼀个 ACK 应答报⽂,之后进⼊ TIME_WAIT 状
- 服务器收到了 ACK 应答报⽂后,就进⼊了 CLOSED 状态,⾄此服务端已经完成连接的关闭。
- 客户端在经过 2MSL ⼀段时间后,⾃动进⼊ CLOSED 状态,⾄此客户端也完成连接的关闭。
这⾥⼀点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。
2、为什么挥手需要四次?
- 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
- 服务器收到客户端的 FIN 报⽂时,先回⼀个 ACK 应答报⽂,⽽服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报⽂给客户端来表示同意现在关闭连接。
服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN ⼀般都会分开发送,从⽽⽐三次握⼿导致多了⼀次。
3、为什么 TIME_WAIT 等待的时间是 2MSL?
**MSL 是 Maximum Segment Lifetime,报⽂最⼤⽣存时间,它是任何报⽂在⽹络上存在的最⻓时间,超过这个时间报⽂将被丢弃。**因为 TCP 报⽂基于是 IP 协议的,⽽ IP 头中有⼀个 TTL 字段,是 IP 数据报可以经过的最⼤路由数,每经过⼀个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报⽂通知源主机。
TIME_WAIT 等于2倍的 MSL,⽐较合理的解释是: ⽹络中可能存在来⾃发送⽅的数据包,当这些发送⽅的数据包被接收⽅处理后⼜会向对⽅发送响应,所以⼀来⼀回需要等待 2 倍的时间。
2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK没有传输到服务端,客户端⼜接收到了服务端᯿发的 FIN 报⽂,那么 2MSL 时间将重新计时。
4、为什么需要 TIME_WAIT 状态?
主动发起关闭连接的⼀⽅,才会有 TIME-WAIT 状态。
需要 TIME-WAIT 状态,主要是两个原因:
- 防⽌具有相同「四元组」的「旧」数据包被收到;
- 保证「被动关闭连接」的⼀⽅能被正确的关闭,即保证最后的 ACK 能让被动关闭⽅接收,从⽽帮助其正常关闭;
5、TIME_WAIT 过多有什么危害?
过多的 TIME-WAIT 状态主要的危害有两种:
-
第⼀是内存资源占⽤;
-
第⼆是对端⼝资源的占⽤,⼀个 TCP 连接⾄少消耗⼀个本地端⼝;
如果发起连接⼀⽅的 TIME_WAIT 状态过多,占满了所有端⼝资源,则会导致⽆法创建新连接。
-
客户端受端⼝资源限制:
客户端TIME_WAIT过多,就会导致端⼝资源被占⽤,因为端⼝就65536个,被占满就会导致⽆法创建新的连接。
-
服务端受系统资源限制:
由于⼀个四元组表示 TCP 连接,理论上服务端可以建⽴很多连接,服务端确实只监听⼀个端⼝,但是会把连接扔给处理线程,所以理论上监听的端⼝可以继续监听。但是线程池处理不了那么多⼀直不断的连接了。所以当服务端出现大量TIME_WAIT 时,系统资源被占满时,会导致处理不过来新的连接。
-
四、Socket 编程
1、针对 TCP 应该如何 Socket 编程?
- 服务端和客户端初始化 socket ,得到⽂件描述符;
- 服务端调⽤ bind ,将绑定在 IP 地址和端⼝
- 服务端调⽤ listen ,进⾏监听;
- 服务端调⽤ accept ,等待客户端连接;
- 客户端调⽤ connect ,向服务器端的地址和端⼝发起连接请求;
- 服务端 accept 返回⽤于传输的 socket 的⽂件描述符;
- 客户端调⽤ write 写⼊数据;服务端调⽤ read 读取数据;
- 客户端断开连接时,会调⽤ close ,那么服务端 read 读取数据的时候,就会读取到了 EOF ,待处理完数据后,服务端调⽤ close ,表示连接关闭。
服务端调⽤ accept 时,连接成功了会返回⼀个已完成连接的 socket,后续⽤来传输数据。**监听的 socket 和真正⽤来传送数据的 socket,是「两个」 socket,**⼀个叫作监听 socket,⼀个叫作已完成连接 socket。
成功连接建⽴之后,双⽅开始通过 read 和 write 函数来读写数据,就像往⼀个⽂件流⾥⾯写东⻄⼀样。
2、listen 时候参数 backlog 的意义?
Linux内核中会维护两个队列:
- 未完成连接队列(SYN队列):接收到⼀个SYN建⽴连接请求,处于SYN_RCVD状态;
- 已完成连接队列(Accpet队列):已完成TCP三次握⼿过程,处于ESTABLISHED状态;
在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建⽴的队列⻓度,所以现在通常认为backlog 是 accept 队列。
但是上限值是内核参数 somaxconn 的⼤⼩,也就说 accpet 队列⻓度 = min(backlog, somaxconn)。
3、accept 发⽣在三次握⼿的哪⼀步?
- 客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 client_isn,客户端进⼊SYN_SENT 状态;
- 服务器端的协议栈收到这个包之后,和客户端进⾏ ACK 应答,应答的值为 client_isn+1,表示对 SYN 包client_isn 的确认,同时服务器也发送⼀个 SYN 包,告诉客户端当前我的发送序列号为 server_isn,服务器端进⼊ SYN_RCVD 状态;
- 客户端协议栈收到 ACK 之后,使得应⽤程序从 connect 调⽤返回,表示客户端到服务器端的单向连接建⽴成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进⾏应答,应答数据为server_isn+1;
- 应答包到达服务器端后,服务器端协议栈使得 accept 阻塞调⽤返回,这个时候服务器端到客户端的单向连接也建⽴成功,服务器端也进⼊ ESTABLISHED 状态。
从上⾯的描述过程,我们可以得知客户端 connect 成功返回是在第⼆次握⼿,服务端 accept 成功返回是在三次握⼿成功之后。
4、客户端调⽤ close 了,连接断开的流程?
- 客户端调⽤ close ,表明客户端没有数据需要发送了,则此时会向服务端发送 FIN 报⽂,进⼊ FIN_WAIT_1状态;
- 服务端接收到了 FIN 报⽂,TCP 协议栈会为 FIN 包插⼊⼀个⽂件结束符 EOF 到接收缓冲区中,应⽤程序可以通过 read 调⽤来感知这个 FIN 包。这个 EOF 会被放在已排队等候的其他已接收的数据之后,这就意味着服务端需要处理这种异常情况,因为 EOF 表示在该连接上再⽆额外数据到达。此时,服务端进⼊CLOSE_WAIT 状态;
- 接着,当处理完数据后,⾃然就会读到 EOF ,于是也调⽤ close 关闭它的套接字,这会使得客户端会发出⼀个 FIN 包,之后处于 LAST_ACK 状态;
- 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进⼊ TIME_WAIT 状态;
- 服务端收到 ACK 确认包后,就进⼊了最后的 CLOSE 状态;
- 客户端经过 2MSL 时间之后,也进⼊ CLOSE 状态;
以上内容整理自小林coding所著的《图解网络》,仅做学习之用,侵删