TCP:与 UDP 区别、三次握手、四次挥手、Python Socket 编程
1. TCP 基本认识
- TCP 头部格式
- 为什么需要 TCP 协议?TCP 工作在哪一层?
- 什么是 TCP ?
- 什么是 TCP 连接?
- 如何唯一确定一个 TCP 连接呢?
- 有一个 IP 的服务器监听了一个端口,它的 TCP 的最大连接数是多少?
2. TCP 与 UDP 区别
- UDP 基本认识
- TCP 与 UDP 区别
- TCP 与 UDP 应用场景
3. TCP 三次握手
- 三次握手过程和状态变迁
- 如何在 Linux 系统中查看 TCP 状态?
- 为什么是三次握手?不是两次、四次?
4. TCP 四次挥手
- 四次挥手过程和状态变迁
- 为什么挥手需要四次?
- 为什么 TIME_WAIT 等待的时间是 2MSL?
- 为什么需要 TIME_WAIT 状态?
- TIME_WAIT 过多有什么危害?
5. Socket 编程
- 针对 TCP 应该如何 Socket 编程?
- listen 时参数 backlog 的意义?
- accept 发送在三次握手的哪一步?
- 客户端调用 close 了,连接是断开的流程是什么?
1. TCP 基本认识
TCP 头部格式
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。序列号是可靠传输的一个关键因素,它的作用:
-
接收方可以去除重复的数据(解决网络包重复问题);
-
接收方可以根据数据包的序列号按序接收(解决网络包乱序问题);
-
可以标识发送出去的数据包中, 哪些是已经被对方收到的;
确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收(解决丢包问题)。
控制位:
-
ACK:该位为
1
时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的SYN
包之外,该位必须设置为1
。 -
RST:该位为
1
时,表示 TCP 连接中出现异常必须强制断开连接。 -
SYC:该位为
1
时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。 -
FIN:该位为
1
时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN
位置为 1 的 TCP 段。
为什么需要 TCP 协议?TCP 工作在哪一层?
IP 层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。
如果需要保障网络数据包传输的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。
因为 TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、非冗余和按序的。
什么是 TCP ?
TCP(Transmission Control Protocol)是面向连接的、可靠的、基于字节流的传输层通信协议。
-
面向连接:一定是「一对一」才能连接,不能像 UDP 协议 可以一个主机同时向多 个主机发送消息,也就是一对多是无法做到的;
-
可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;并且消息是「有序的」,当「前一个」消息没有收到的时候,即使它先收到了后面的字节已经收到,那么也不能扔给应用层去处理;同时对「重复」的报文会自动丢弃。
-
字节流:消息是「没有边界」的,所以无论我们消息有多大都可以进行传输。
什么是 TCP 连接?
简单来说就是,用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合(包括 Socket、序列号和窗口大小)称为 TCP 连接。
所以我们可以知道,建立一个 TCP 连接是需要客户端与服务器端达成上述三个信息的共识。
-
Socket:由 IP 地址和端口号组成
-
序列号:用来解决乱序问题等
-
窗口大小:用来做流量控制
如何唯一确定一个 TCP 连接呢?
TCP 四元组可以唯一的确定一个连接,四元组包括如下:
- 源地址
- 源端口
- 目标地址
- 目标端口
源地址和目标地址的字段(32 位)是在 IP 头部中,作用是告诉 IP 协议应该把报文发送给哪个主机。
源端口和目标端口的字段(16 位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。
有一个 IP 的服务器监听了一个端口,它的 TCP 的最大连接数是多少?
服务器通常固定在某个本地端口上监听,等待客户端的连接请求。
而客户端 IP 和 端口是可变的,其理论值计算公式如下:
最大 TCP 连接数 = 客户端的 IP 数 * 客户端的端口数
对于 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数约为 2 的 48 次方。
当然,服务端最大并发 TCP 连接数远不能达到理论上限。
- 首先主要是文件描述符限制,Socket 都是文件,所以首先要通过 ulimit 配置文件描述符的数目;
- 另一个是内存限制,每个 TCP 连接都要占用一定内存,操作系统是有限的。
2. TCP 与 UDP 区别
UDP 基本认识
UDP 不提供复杂的控制机制,利用 IP 提供面向「无连接」的通信服务。
UDP 协议真的非常简单,头部只有 8 个字节( 64 位),UDP 的头部格式如下:
-
目标和源端口:主要是告诉 UDP 协议应该把报文发给哪个进程。
-
包长度:该字段保存了 UDP 首部的长度跟数据的长度之和。
-
校验和:校验和是为了提供可靠的 UDP 首部和数据而设计。
TCP 与 UDP 区别
- TCP:Transimission Control Protocol(传输控制协议)
- UDP:User Datagram Protocol(用户数据报协议)
1. 连接
- TCP 是面向连接的传输层协议,传输数据前先要建立连接。
- UDP 是不需要连接,即刻传输数据。
2. 服务对象
- TCP 是一对一的两点服务,即一条连接只有两个端点。
- UDP 支持一对一、一对多、多对多的交互通信。
3. 可靠性
- TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。
- UDP 是尽最大努力交付,不保证可靠交付数据。
4. 拥塞控制、流量控制
什么是限流?
我们日常开发中,经常听到接口限流,QPS多少等等这些词。那么,什么是限流呢?在计算机网络中,限流就是控制网络接口发送或接收请求的速率。
举个生活的例子:一些热门的旅游景区,一般会对每日的旅游参观人数有限制的,每天只会卖出固定数目的门票,比如5000张。假设在五一、国庆假期,你去晚了,可能当天的票就已经卖完了,就无法进去游玩了。即使你最后能进去,排队也排到你怀疑人生。
- TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
- UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
TCP 的流量窗口如何控制流量?
我们来看课堂上这么一个场景,老师讲课,学生做笔记。假设老师念一段话,要求学生孩子们做笔记,记录下来。
第一种模式:
- 老师说:“从前有个人,她叫马冬梅。她喜欢夏洛,而夏洛却喜欢秋雅。”
- 学生写:“从前有...”。
- 学生说:“老师您说得太快啦,我跟不上。”
于是他们换了模式二:
- 老师说:“从。”
- 学生写:“从”。
- 学生说:“嗯。”
- 老师说:“前。”
- 学生写:“前”。
- 学生说:“嗯。”
- 老师说:“今天我还想早点下班呢。”
于是他们又换了一种模式,模式三:
- 老师说:“从前有个人。”
- 学生写:“从前有个人”。
- 学生说:“嗯。”
- 老师说:“她叫马冬梅。”
- 学生写:“她叫马...梅”。
- 学生说:“马什么梅?”
- 老师说:“她叫马冬梅。”
- 学生写:“她叫马冬...”。
- 学生说:“马冬什么?”
- 老师:“....."
- 学生说:“有的时候状态好我能把5个字都记下来,有的时候状态不好就记不下来。我状态不好的时候,您能不能慢一点呢?”
于是他们换了模式四:
- 老师说:“从前有个人。”
- 学生写:“从前有个人”。
- 学生说:“嗯, 再来5个。”
- 老师说:“她叫马冬梅。”
- 学生写:“她叫马...梅”。
- 学生说:“啥?重来,来2个。”
- 老师说:“她叫。”
- 学生写:“她叫”。
- 学生说:“嗯,再来3个。”
- 老师说:“马冬梅。”
- 学生写:“马冬梅”。
- 学生说:“嗯,给我来20个。”
- 老师说:“她喜欢夏洛,而夏洛却喜欢秋雅。”
- 学生写...
因此:
- 第一种模式简单粗暴,发的只管发,收的可能跟不上。
- 第二种模式稳定却低效,每发一个,必须等到确认才再次发送,等待时间比较多。
- 第三种模式提高了效率,分组进行发送,但是分组的大小该怎么决定呢?
- 第四种模式才真正起到了流控的作用,接收方认为状态好的时候,让发送方每次多发一点。接收方认为状态不好的时候(阻塞),让发送方每次少发送一点。
5. 首部开销
- TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。
- UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
6. 数据格式
- TCP 基于字节流:在不考虑流量控制(接受缓存)时,理论上每一次传输的数据量可以是无限大的,且双方的收发次数可任意定义。
- UDP 基于数据报:发送端调用了几次 write,接收端必须用相同次数的 read 读完;UPD 是基于报文的,在接收的时候,每次最多只能读取一个报文,报文和报文是不会合并的;如果缓冲区小于报文长度,则多出的部分会被丢弃。
TCP 与 UDP 应用场景
由于 TCP 是面向连接的,能保证数据的可靠性交付,因此经常用于:
- FTP 文件传输
- HTTP / HTTPS
由于 UDP 面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于:
- 包总量较少的通信,如 DNS 、SNMP 等
- 视频、音频等多媒体通信
- 广播通信
3. TCP 三次握手
三次握手过程和状态变迁
TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手而进行的。
1. 一开始,客户端和服务端都处于 CLOSED
状态。
2. 先是服务端主动监听某个端口,处于 LISTEN
状态
3. 客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序列号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
4. 服务端收到客户端的 SYN
报文后,首先服务端也随机初始化自己的序号(server_isn
),将此序号填入 TCP 首部的「序列号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1
, 接着把 SYN
和 ACK
标志位置为 1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD
状态。
5. 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK
标志位置为 1
,其次「确认应答号」字段填入 server_isn + 1
,最后把报文发送给服务端,这次报文可以携带客户端到服务器的数据,之后客户端处于 ESTABLISHED
状态。
6. 服务器收到客户端的应答报文后,也进入 ESTABLISHED
状态。
从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题。
一旦完成三次握手,双方都处于 ESTABLISHED
状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
如何在 Linux 系统中查看 TCP 状态?
TCP 的连接状态查看,在 Linux 可以通过 netstat -napt
命令查看。
为什么是三次握手?不是两次、四次?
相信大家比较常回答的是:“因为三次握手才能保证双方具有接收和发送的能力。”
这回答是没问题,但这回答是片面的,并没有说出主要的原因。
在前面我们知道了什么是 TCP 连接:
-
用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合(包括 Socket、序列号和窗口大小)称为连接。
所以,重要的是为什么三次握手才可以初始化 Socket、序列号和窗口大小并建立 TCP 连接。
接下来以三个方面分析三次握手的原因:
-
三次握手才可以阻止历史重复连接的初始化(主要原因)
-
三次握手才可以同步双方的初始序列号
-
三次握手才可以避免资源浪费
原因一:避免历史连接
我们来看看 RFC 793 指出的 TCP 连接使用三次握手的首要原因:
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。
网络环境是错综复杂的,往往并不是如我们期望的一样,先发送的数据包,就先到达目标主机,反而它很骚,可能会由于网络拥堵等乱七八糟的原因,会使得旧的数据包,先到达目标主机,那么这种情况下 TCP 三次握手是如何避免的呢?
客户端连续发送多次 SYN 建立连接的报文,在网络拥堵等情况下:
-
一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端;
-
那么此时服务端就会回一个
SYN + ACK
报文给客户端; -
客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送
RST
报文给服务端,表示中止这一次连接。
如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接:
-
如果是历史连接(序列号过期或超时),则第三次握手发送的报文是
RST
报文,以此中止历史连接; -
如果不是历史连接,则第三次发送的报文是
ACK
报文,通信双方就会成功建立连接。
所以, TCP 使用三次握手建立连接的最主要原因是防止历史连接初始化了连接。
原因二:同步双方初始序列号
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
-
接收方可以去除重复的数据;
-
接收方可以根据数据包的序列号按序接收;
-
可以标识发送出去的数据包中, 哪些是已经被对方收到的;
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN
报文的时候,需要服务端回一个 ACK
应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。
而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
原因三:避免资源浪费
如果只有「两次握手」,当客户端的 SYN
请求连接在网络中阻塞,导致后续没有接收到服务端的 ACK
报文时,客户端就会重新发送 SYN
。由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的
ACK
确认信号,所以每收到一个 SYN
就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端的 SYN
阻塞了,重复发送多次 SYN
报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
即两次握手会造成消息滞留情况下,服务器重复接受无用的连接请求 SYN
报文,而造成重复分配资源。
小结
TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号(序列号能够保证数据包不重复、不丢弃和按序传输)。
不使用「两次握手」和「四次握手」的原因:
-
「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
-
「四次握手」:三次握手已经是理论上最少的可靠连接建立次数,所以不需要使用更多的通信次数。
4. TCP 四次挥手
四次挥手过程和状态变迁
TCP 断开连接是通过四次挥手方式。双方都可以主动断开连接,断开连接后主机中的「资源」将被释放。
- 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
- 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。
- 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
- 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
- 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
- 服务器收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
- 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。
你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。
这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。
为什么挥手需要四次?
再来回顾下四次挥手双方发 FIN
包的过程,就能理解为什么需要四次了。
-
关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。 -
服务器收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK
和 FIN
一般都会分开发送,从而比三次握手多了一次。
为什么 TIME_WAIT 等待的时间是 2MSL?
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。
2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。在 linux 系统中停留在 TIME_WAIT 的时间为固定的 60 秒。
为什么需要 TIME_WAIT 状态?
主动发起关闭连接的一方,才会有 TIME-WAIT 状态。
需要 TIME-WAIT 状态,主要是两个原因:
- 防止具有相同「四元组」的「旧」数据包被收到;
- 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。
原因一:防止旧连接的数据包
假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢?
- 如上图黄色框框服务端在关闭连接之前发送的
SEQ = 301
报文,被网络延迟了。 - 这时有相同端口的 TCP 连接被复用后,被延迟的
SEQ = 301
抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题。
所以,TCP 就设计出了这么一个机制,经过 2MSL
这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
原因二:保证连接正确关闭
在 RFC 793 指出 TIME-WAIT 另一个重要的作用是:
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 没有等待时间或时间过短,断开连接会造成什么问题呢?
-
如上图红色框框客户端四次挥手的最后一个
ACK
报文如果在网络中被丢失了,此时如果客户端TIME-WAIT
过短或没有,则就直接进入了CLOSE
状态了,那么服务端则会一直处在LASE-ACK
状态。 -
当客户端发起建立连接的
SYN
请求报文后,服务端会发送RST
报文给客户端,连接建立的过程就会被终止。
如果 TIME-WAIT 等待足够长的情况就会遇到两种情况:
-
服务端正常收到四次挥手的最后一个
ACK
报文,则服务端正常关闭连接。 -
服务端没有收到四次挥手的最后一个
ACK
报文时,则会重发FIN
关闭连接报文并等待新的ACK
报文。
所以客户端在 TIME-WAIT
状态等待 2MSL
时间后,就可以保证双方的连接都可以正常的关闭。
TIME_WAIT 过多有什么危害?
如果服务器有处于 TIME-WAIT 状态的 TCP,则说明是由服务器方主动发起的断开请求。
过多的 TIME-WAIT 状态主要的危害有两种:
- 内存资源占用;
- 对端口资源的占用,一个 TCP 连接至少消耗一个本地端口。
第二个危害是会造成严重的后果的,要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000。如果服务端 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。
5. Socket 编程
针对 TCP 应该如何 Socket 编程?
-
服务端和客户端初始化
socket
,得到文件描述符; -
服务端调用
bind
,绑定 IP 地址和端口; -
服务端调用
listen
,进行监听; -
服务端调用
accept
,等待客户端连接; -
客户端调用
connect
,向服务器端的地址和端口发起连接请求; -
服务端
accept
返回用于传输的socket
的文件描述符; -
客户端调用
write
写入数据;服务端调用read
读取数据; -
客户端断开连接时,会调用
close
,那么服务端read
读取数据的时候,就会读取到了EOF
,待处理完数据后,服务端调用close
,表示连接关闭。
所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。
成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。
listen 时参数 backlog 的意义?
Linux内核中会维护两个队列:
-
未完成连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;
-
已完成连接队列(ACCEPT 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;
int listen (int socketfd, int backlog)
-
参数一 socketfd:为 socketfd 文件描述符。
-
参数二 backlog:这参数在历史有一定的变化。
在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。
在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列。
accept 发送在三次握手的哪一步?
我们先看看客户端连接服务端时,发送了什么?
-
客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 client_isn,客户端进入 SYNC_SENT 状态;
-
服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 client_isn+1,表示对 SYN 包 client_isn 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 server_isn,服务器端进入 SYNC_RCVD 状态;
-
客户端协议栈收到 ACK 之后,使得应用程序从
connect
调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 server_isn+1; -
应答包到达服务器端后,服务器端协议栈使得
accept
阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态。
从上面的描述过程,我们可以得知客户端 connect 成功返回是在第二次握手,服务端 accept 成功返回是在三次握手成功之后。
客户端调用 close 了,连接是断开的流程是什么?
我们看看客户端主动调用了 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 状态。