文章目录
1、什么是TCP
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议,处于OSI模型的第四层传输层。
- 面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
- 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
- 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃。
2、什么是TCP连接
2.1、连接概念
我们来看看 RFC 793 是如何定义「连接」的:
Connections: The reliability and flow control mechanisms described above require that TCPs initialize and maintain certain status information for each data stream. The combination of this information, including sockets, sequence numbers, and window sizes, is called a connection.
简单来说就是,用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket、序列号和窗口大小称为连接。
所以我们可以知道,建立一个 TCP 连接是需要客户端与服务端达成上述三个信息的共识。
- Socket:由 IP 地址和端口号组成
- 序列号:用来解决乱序问题等
- 窗口大小:用来做流量控制
2.2、如何唯一确定一个TCP连接
TCP 四元组可以唯一的确定一个连接,四元组包括如下:
- 源地址
- 源端口
- 目的地址
- 目的端口
源地址和目的地址的字段(32 位)是在IP头部
中,作用是通过 IP 协议发送报文给对方主机。
源端口和目的端口的字段(16 位)是在TCP头部
中,作用是告诉 TCP 协议应该把报文发给哪个进程。
2.3、TCP最大连接数
服务端通常固定在某个本地端口上监听,等待客户端的连接请求。
因此,客户端 IP 和端口是可变的,其理论值计算公式如下:
最大TCP连接数 = 客户端的IP数 * 客户端的端口数
对 IPv4,客户端的 IP 数最多为2^32
,客户端的端口数最多为2^16
,也就是服务端单机最大 TCP 连接数,约为 2^48
。
当然,服务端最大并发 TCP 连接数远不能达到理论上限,会受以下因素影响:
- 文件描述符限制,每个 TCP 连接都是一个文件,如果文件描述符被占满了,会发生 Too many open files。Linux 对可打开的文件描述符的数量分别作了三个方面的限制:
- 系统级:当前系统可打开的最大数量,通过
cat /proc/sys/fs/file-max
查看; - 用户级:指定用户可打开的最大数量,通过
cat /etc/security/limits.conf
查看; - 进程级:单个进程可打开的最大数量,通过
cat /proc/sys/fs/nr_open
查看;
- 系统级:当前系统可打开的最大数量,通过
- 内存限制,每个 TCP 连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生
OOM
。
3、三次握手
3.1、为什么需要三次握手
我们假设客户端发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达服务端。
本来这是一个早已失效的报文段。但服务端收到此失效的连接请求报文段后,就误认为是客户端再次发出的一个新的连接请求。于是就向客户端发出确认报文段,同意建立连接。
假设不采用“三次握手”,那么只要服务端发出确认,新的连接就建立了。由于现在客户端并没有发出建立连接的请求,因此不会理睬服务端的确认,也不会向服务端发送数据。但服务端却以为新的运输连接已经建立,并一直等待客户端发来数据。这样,服务端的很多资源就白白浪费掉了。
所以,采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,客户端不会向服务端的确认发出确认。服务端由于收不到确认,就知道客户端并没有要求建立连接。
TCP 三次握手跟现实生活中的人与人打电话是很类似的:
三次握手:
“喂,你听得到吗?”
“我听得到呀,你听得到我吗?”
“我能听到你,今天 balabala……”
经过三次的互相确认,大家就会认为对方对听的到自己说话,并且愿意下一步沟通,否则,对话就不一定能正常下去了。在TCP连接建立时同样需要这三次的牵手。
3.2、三次握手过程
TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手来进行的。三次握手的过程如下图:
- 一开始,客户端和服务端都处于
CLOSE
状态。先是服务端主动监听某个端口,处于LISTEN
状态; - 客户端会生成随机初始化的序号(
client_isn
),将此序号置于 TCP 首部的「序列号」字段中,同时把SYN
标志位置为1
,表示SYN
报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT
状态。
- 服务端收到客户端的
SYN
报文后,首先服务端也会生成自己的随机初始化的序号(server_isn
),将此序号填入 TCP 首部的「序列号」字段中,其次把 TCP 首部的「确认应答号」字段填入client_isn + 1
, 接着把SYN
和ACK
标志位置为1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于SYN-RCVD
状态。
- 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部
ACK
标志位置为1
,其次「确认应答号」字段填入server_isn + 1
,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于ESTABLISHED
状态。 - 服务端收到客户端的应答报文后,也进入
ESTABLISHED
状态。
从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的。一旦完成三次握手,双方都处于 ESTABLISHED
状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
3.3、为什么一定是三次
3.3.1、避免历史连接
在 RFC 793 中指出了 TCP 连接使用三次握手的首要原因:
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。
我们考虑一个场景,客户端先发送了 SYN(seq = 90)
报文,然后客户端宕机了,而且这个 SYN
报文还被网络阻塞了,服务端并没有收到。接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100)
报文
注意!不是重传
SYN
,重传的SYN
的序列号是一样的
看看三次握手是如何阻止历史连接的:
客户端连续发送多次 SYN
(都是同一个四元组)建立连接的报文,在网络拥堵情况下:
- 一个「
旧SYN报文
」比「最新的SYN报文
」早到达了服务端,那么此时服务端就会回一个SYN + ACK
报文给客户端,此报文中的确认号是91(90+1)
。 - 客户端收到后,发现自己期望收到的确认号应该是
100 + 1
,而不是90 + 1
,于是就会回RST
报文。 - 服务端收到
RST
报文后,就会释放连接。 - 后续最新的
SYN
抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。
其中「旧 SYN 报文
」称为历史连接,TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接。
那么如果是两次握手或一次握手会发生什么呢?
答案是在两次握手或一次握手时,TCP建立连接无法避免历史连接的发生。主要是因为在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
大家可以尝试模拟这么一个场景,在两次握手的情况下,服务端在收到 SYN
报文后,就进入 ESTABLISHED
状态,意味着这时可以给对方发送数据,但是客户端此时还没有进入 ESTABLISHED
状态,假设这次是历史连接,客户端判断到此次连接为历史连接,那么就会回 RST
报文来断开连接,而服务端在第一次握手的时候就进入 ESTABLISHED
状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只有在收到 RST
报文后,才会断开连接。
可以看到,如果采用两次握手或一次握手建立 TCP 连接的场景下,服务端在向客户端发送数据前,并没有阻止掉历史连接,导致服务端建立了一个历史连接,又白白发送了数据,妥妥地浪费了服务端的资源。
因此,要解决这种现象,最好就是在服务端发送数据前,也就是建立连接之前,要阻止掉历史连接,这样就不会造成资源浪费,而要实现这个功能,就需要三次握手。
3.3.2、同步双方初始序列号
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
- 接收方可以去除重复的数据;
- 接收方可以根据数据包的序列号按序接收;
- 可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN
报文的时候,需要服务端回一个 ACK
应答报文,表示客户端的 SYN
报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
而对于四次握手,也能够实现可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,本着能省则省的持家本领,所以就成了「三次握手」。但是两次握手就完全实现不了这个需求了,一次就更别想了。
3.3.3、避免资源浪费
如果只有「两次握手」,当客户端发生的 SYN
报文在网络中阻塞,客户端没有接收到 ACK
报文,就会重新发送 SYN
,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK
报文,所以服务端每收到一个 SYN
就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端发送的 SYN
报文在网络中阻塞了,重复发送多次 SYN
报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
即两次握手会造成消息滞留情况下,服务端重复接受无用的连接请求 SYN
报文,而造成重复分配资源。
3.3.4、总结
TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使用「两次握手」和「四次握手」的原因:
- 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
- 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
对了,比四次还多的握手就没那个分析的必要了,总不能一个
ACK
还要发个几百遍对吧,握个手握这么多遍就不礼貌了哈。
3.4、握手丢失
3.4.1、第一次握手丢失
当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN
报文,然后进入到 SYN_SENT
状态。
在这之后,如果客户端迟迟收不到服务端的 SYN-ACK
报文(第二次握手),就会触发「超时重传」机制,重传 SYN
报文,而且重传的 SYN 报文的序列号都是一样的。
不同版本的操作系统可能超时时间不同,有的1秒的,也有3秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。
当客户端在1秒后没收到服务端的 SYN-ACK
报文后,客户端就会重发 SYN
报文,那到底重发几次呢?
在 Linux 里,客户端的 SYN
报文最大重传次数由 tcp_syn_retries
内核参数控制,这个参数是可以自定义的,默认值一般是 5。
# cat /proc/sys/net/ipv4/tcp_syn_retries
5
通常,第一次超时重传是在1秒后,第二次超时重传是在2秒,第三次超时重传是在4秒后,第四次超时重传是在8秒后,第五次是在超时重传16秒后。没错,每次超时的时间是上一次的 2 倍。
当第五次超时重传后,会继续等待32秒,如果服务端仍然没有回应 ACK
,客户端就不再发送 SYN
包,然后断开 TCP 连接。
所以,总耗时是 1+2+4+8+16+32=63
秒,大约1分钟左右。
举个例子,假设 tcp_syn_retries
参数值为3,那么当客户端的 SYN 报文一直在网络中丢失时,便会发生以下流程:
- 当客户端超时重传3次
SYN
报文后,由于tcp_syn_retries
为3,已达到最大重传次数; - 于是再等待一段时间(时间为上一次超时时间的
2倍
); - 如果还是没能收到服务端的第二次握手(
SYN-ACK
报文),那么客户端就会断开连接。
3.4.2、第二次握手丢失
当服务端收到客户端的第一次握手后,就会回 SYN-ACK
报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD
状态。
第二次握手的 SYN-ACK
报文其实有两个目的 :
- 第二次握手里的
ACK
, 是对第一次握手的确认报文; - 第二次握手里的
SYN
,是服务端发起建立 TCP 连接的报文;
所以,如果第二次握手丢了,就会发生比较有意思的事情,具体会怎么样呢?
因为第二次握手报文里是包含对客户端的第一次握手的 ACK
确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN
报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文。
然后,因为第二次握手中包含服务端的 SYN
报文,所以当客户端收到后,需要给服务端发送 ACK
确认报文(第三次握手),服务端才会认为该 SYN
报文被客户端收到了。
那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文。
在 Linux 下,SYN-ACK
报的最大重传次数由 tcp_synack_retries
内核参数决定,默认值是 5。
# cat /proc/sys/net/ipv4/tcp_synack_retries
5
因此,当第二次握手丢失了,客户端和服务端都会重传:
- 客户端会重传
SYN 报文
,也就是第一次握手,最大重传次数由tcp_syn_retries
内核参数决定; - 服务端会重传
SYN-ACK
报文,也就是第二次握手,最大重传次数由tcp_synack_retries
内核参数决定。
举个例子,假设 tcp_syn_retries
参数值为1,tcp_synack_retries
参数值为2,那么当第二次握手一直丢失时便会发生以下流程:
- 当客户端超时重传1次
SYN
报文后,由于tcp_syn_retries
为1,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍
),如果还是没能收到服务端的第二次握手(SYN-ACK
报文),那么客户端就会断开连接; - 当服务端超时重传2次
SYN-ACK
报文后,由于tcp_synack_retries
为2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍
),如果还是没能收到客户端的第三次握手(ACK
报文),那么服务端就会断开连接。
3.4.3、第三次握手丢失
客户端收到服务端的 SYN-ACK
报文后,就会给服务端回一个 ACK
报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH
状态。
因为这个第三次握手的 ACK
是对第二次握手的 SYN
的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK
报文,直到收到第三次握手,或者达到最大重传次数。
注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
举个例子,假设 tcp_synack_retries
参数值为 2,那么当第三次握手一直丢失时便会发生以下流程:
- 当服务端超时重传2次
SYN-ACK
报文后,由于tcp_synack_retries
为2,已达到最大重传次数; - 于是再等待一段时间(时间为上一次超时时间的
2倍
); - 如果还是没能收到客户端的第三次握手(
ACK
报文),那么服务端就会断开连接。
4、四次挥手
4.1、四次挥手过程
天下没有不散的宴席,对于 TCP 连接也是这样, TCP 断开连接是通过四次挥手方式。
四次挥手即终止 TCP 连接,就是指断开一个 TCP 连接时,需要客户端和服务端总共发送4个包以确认连接的断开。由于 TCP 连接是全双工的,因此,每个方向都必须要单独进行关闭。
双方都可以主动断开连接,断开连接后主机中的「资源」将被释放,四次挥手的过程如下:
- 客户端打算关闭连接,此时会发送一个 TCP 首部
FIN
标志位被置为1
的报文,也即FIN
报文,之后客户端进入FIN_WAIT_1
状态。 - 服务端收到该报文后,就向客户端发送
ACK
应答报文,接着服务端进入CLOSE_WAIT
状态。 - 客户端收到服务端的
ACK
应答报文后,之后进入FIN_WAIT_2
状态。 - 等待服务端处理完数据后,也向客户端发送
FIN
报文,之后服务端进入LAST_ACK
状态。 - 客户端收到服务端的
FIN
报文后,回一个ACK
应答报文,之后进入TIME_WAIT
状态 - 服务端收到了
ACK
应答报文后,就进入了CLOSE
状态,至此服务端已经完成连接的关闭。 - 客户端在经过
2MSL
一段时间后,自动进入CLOSE
状态,至此客户端也完成连接的关闭。
可以看到,每个方向都需要一个 FIN
和一个 ACK
,因此通常被称为四次挥手。
有一点值得注意是:主动关闭连接的,才有 TIME_WAIT
状态。
4.2、为什么一定是四次
再来回顾下四次挥手双方发 FIN
包的过程,就能理解为什么需要四次了。
- 关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。 - 服务端收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK
和 FIN
一般都会分开发送,因此是需要四次挥手。
但也有特殊情况会变成三次分手:当被动关闭方(上图的服务端)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。
断开连接的过程顶破天也就三次或者四次了,太少的话达不到分手的目的,太多的话分手的开销也太大了。
4.3、挥手丢失
4.3.1、第一次分手丢失
当客户端(主动关闭方)调用 close
函数后,就会向服务端发送 FIN
报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1
状态。
正常情况下,如果能及时收到服务端(被动关闭方)的 ACK
,则会很快变为 FIN_WAIT2
状态。
如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK
的话,也就会触发超时重传机制,重传 FIN
报文,重发次数由 tcp_orphan_retries
参数控制。
当客户端重传 FIN
报文的次数超过 tcp_orphan_retries
后,就不再发送 FIN
报文,则会在等待一段时间(时间为上一次超时时间的2倍
),如果还是没能收到第二次挥手,那么直接进入到 close
状态。
举个例子,假设 tcp_orphan_retries 参数值为 3,当第一次挥手一直丢失时便会发生以下流程:
- 当客户端超时重传 3 次
FIN
报文后,由于tcp_orphan_retries 为
3,已达到最大重传次数; - 于是再等待一段时间(时间为上一次超时时间的
2倍
); - 如果还是没能收到服务端的第二次挥手(
ACK
报文),那么客户端就会断开连接。
4.3.2、第二次分手丢失
当服务端收到客户端的第一次挥手后,就会先回一个 ACK
确认报文,此时服务端的连接进入到 CLOSE_WAIT
状态。
在前面我们也提了,ACK
报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN
报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。
举个例子,假设 tcp_orphan_retries
参数值为2,当第二次挥手一直丢失时便会发生以下流程:
- 当客户端超时重传2次
FIN
报文后,由于tcp_orphan_retries
为2,已达到最大重传次数; - 于是再等待一段时间(时间为上一次超时时间的
2倍
); - 如果还是没能收到服务端的第二次挥手(
ACK
报文),那么客户端就会断开连接。
这里提一下,当客户端收到第二次挥手,也就是收到服务端发送的 ACK
报文后,客户端就会处于 FIN_WAIT2
状态,在这个状态需要等服务端发送第三次挥手,也就是服务端的 FIN
报文。
对于 close
函数关闭的连接,由于无法再发送和接收数据,所以FIN_WAIT2
状态不可以持续太久,而 tcp_fin_timeout
控制了这个状态下连接的持续时长,默认值是 60秒
。
这意味着对于调用 close
关闭的连接,如果在 60秒
后还没有收到 FIN
报文,客户端(主动关闭方)的连接就会直接关闭,如下图:
但是注意,如果主动关闭方使用 shutdown
函数关闭连接,指定了只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据的。
此时,如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于 FIN_WAIT2
状态(tcp_fin_timeout
无法控制 shutdown
关闭的连接)。如下图:
4.3.3、第三次分手丢失
当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN
报文后,内核会自动回复 ACK
,同时连接处于 CLOSE_WAIT
状态,顾名思义,它表示等待应用进程调用 close
函数关闭连接。
此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close
函数来触发服务端发送 FIN
报文。
服务端处于 CLOSE_WAIT
状态时,调用了 close
函数,内核就会发出 FIN
报文,同时连接进入 LAST_ACK
状态,等待客户端返回 ACK
来确认连接关闭。
如果迟迟收不到这个 ACK
,服务端就会重发 FIN
报文,重发次数仍然由 tcp_orphan_retries
参数控制,这与客户端重发 FIN
报文的重传次数控制方式是一样的。
举个例子,假设 tcp_orphan_retries
= 3,当第三次挥手一直丢失时便会发生以下流程:
- 当服务端重传第三次挥手报文的次数达到了3次后,由于
tcp_orphan_retries
为3,达到了重传最大次数,于是再等待一段时间(时间为上一次超时时间的2倍
),如果还是没能收到客户端的第四次挥手(ACK
报文),那么服务端就会断开连接; - 客户端因为是通过
close
函数关闭连接的,处于FIN_WAIT_2
状态是有时长限制的,如果tcp_fin_timeout
时间内还是没能收到服务端的第三次挥手(FIN
报文),那么客户端就会断开连接。
4.3.4、第四次分手丢失
当客户端收到服务端的第三次挥手的 FIN
报文后,就会回 ACK
报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT
状态。
在 Linux 系统,TIME_WAIT
状态会持续 2MSL
后才会进入关闭状态。
然后,服务端(被动关闭方)没有收到 ACK
报文前,还是处于 LAST_ACK
状态。
如果第四次挥手的 ACK
报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries
参数控制。
举个例子,假设 tcp_orphan_retries
为2,当第四次挥手一直丢失时便会发生以下流程:
- 当服务端重传第三次挥手报文达到2时,由于
tcp_orphan_retries
为2, 达到了最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍
),如果还是没能收到客户端的第四次挥手(ACK
报文),那么服务端就会断开连接。 - 客户端在收到第三次挥手后,就会进入
TIME_WAIT
状态,开启时长为2MSL
的定时器,如果途中再次收到第三次挥手(FIN
报文)后,就会重置定时器,当等待2MSL
时长后,客户端就会断开连接。
文章评论